1/*
2 * Copyright (C) 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
17package com.android.test.voiceenrollment;
18
19import android.annotation.Nullable;
20import android.content.Context;
21import android.hardware.soundtrigger.KeyphraseEnrollmentInfo;
22import android.hardware.soundtrigger.SoundTrigger;
23import android.hardware.soundtrigger.SoundTrigger.Keyphrase;
24import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
25import android.os.RemoteException;
26import android.os.ServiceManager;
27import android.service.voice.AlwaysOnHotwordDetector;
28import android.util.Log;
29
30import com.android.internal.app.IVoiceInteractionManagerService;
31
32/**
33 * Utility class for the enrollment operations like enroll;re-enroll & un-enroll.
34 */
35public class EnrollmentUtil {
36    private static final String TAG = "TestEnrollmentUtil";
37
38    /**
39     * Activity Action: Show activity for managing the keyphrases for hotword detection.
40     * This needs to be defined by an activity that supports enrolling users for hotword/keyphrase
41     * detection.
42     */
43    public static final String ACTION_MANAGE_VOICE_KEYPHRASES =
44            KeyphraseEnrollmentInfo.ACTION_MANAGE_VOICE_KEYPHRASES;
45
46    /**
47     * Intent extra: The intent extra for the specific manage action that needs to be performed.
48     * Possible values are {@link AlwaysOnHotwordDetector#MANAGE_ACTION_ENROLL},
49     * {@link AlwaysOnHotwordDetector#MANAGE_ACTION_RE_ENROLL}
50     * or {@link AlwaysOnHotwordDetector#MANAGE_ACTION_UN_ENROLL}.
51     */
52    public static final String EXTRA_VOICE_KEYPHRASE_ACTION =
53            KeyphraseEnrollmentInfo.EXTRA_VOICE_KEYPHRASE_ACTION;
54
55    /**
56     * Intent extra: The hint text to be shown on the voice keyphrase management UI.
57     */
58    public static final String EXTRA_VOICE_KEYPHRASE_HINT_TEXT =
59            KeyphraseEnrollmentInfo.EXTRA_VOICE_KEYPHRASE_HINT_TEXT;
60    /**
61     * Intent extra: The voice locale to use while managing the keyphrase.
62     */
63    public static final String EXTRA_VOICE_KEYPHRASE_LOCALE =
64            KeyphraseEnrollmentInfo.EXTRA_VOICE_KEYPHRASE_LOCALE;
65
66    /** Simple recognition of the key phrase */
67    public static final int RECOGNITION_MODE_VOICE_TRIGGER =
68            SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER;
69    /** Trigger only if one user is identified */
70    public static final int RECOGNITION_MODE_USER_IDENTIFICATION =
71            SoundTrigger.RECOGNITION_MODE_USER_IDENTIFICATION;
72
73    private final IVoiceInteractionManagerService mModelManagementService;
74
75    public EnrollmentUtil() {
76        mModelManagementService = IVoiceInteractionManagerService.Stub.asInterface(
77                ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
78    }
79
80    /**
81     * Adds/Updates a sound model.
82     * The sound model must contain a valid UUID,
83     * exactly 1 keyphrase,
84     * and users for which the keyphrase is valid - typically the current user.
85     *
86     * @param soundModel The sound model to add/update.
87     * @return {@code true} if the call succeeds, {@code false} otherwise.
88     */
89    public boolean addOrUpdateSoundModel(KeyphraseSoundModel soundModel) {
90        if (!verifyKeyphraseSoundModel(soundModel)) {
91            return false;
92        }
93
94        int status = SoundTrigger.STATUS_ERROR;
95        try {
96            status = mModelManagementService.updateKeyphraseSoundModel(soundModel);
97        } catch (RemoteException e) {
98            Log.e(TAG, "RemoteException in updateKeyphraseSoundModel", e);
99        }
100        return status == SoundTrigger.STATUS_OK;
101    }
102
103    /**
104     * Gets the sound model for the given keyphrase, null if none exists.
105     * This should be used for re-enrollment purposes.
106     * If a sound model for a given keyphrase exists, and it needs to be updated,
107     * it should be obtained using this method, updated and then passed in to
108     * {@link #addOrUpdateSoundModel(KeyphraseSoundModel)} without changing the IDs.
109     *
110     * @param keyphraseId The keyphrase ID to look-up the sound model for.
111     * @param bcp47Locale The locale for with to look up the sound model for.
112     * @return The sound model if one was found, null otherwise.
113     */
114    @Nullable
115    public KeyphraseSoundModel getSoundModel(int keyphraseId, String bcp47Locale) {
116        if (keyphraseId <= 0) {
117            Log.e(TAG, "Keyphrase must have a valid ID");
118            return null;
119        }
120
121        KeyphraseSoundModel model = null;
122        try {
123            model = mModelManagementService.getKeyphraseSoundModel(keyphraseId, bcp47Locale);
124        } catch (RemoteException e) {
125            Log.e(TAG, "RemoteException in updateKeyphraseSoundModel");
126        }
127
128        if (model == null) {
129            Log.w(TAG, "No models present for the gien keyphrase ID");
130            return null;
131        } else {
132            return model;
133        }
134    }
135
136    /**
137     * Deletes the sound model for the given keyphrase id.
138     *
139     * @param keyphraseId The keyphrase ID to look-up the sound model for.
140     * @return {@code true} if the call succeeds, {@code false} otherwise.
141     */
142    @Nullable
143    public boolean deleteSoundModel(int keyphraseId, String bcp47Locale) {
144        if (keyphraseId <= 0) {
145            Log.e(TAG, "Keyphrase must have a valid ID");
146            return false;
147        }
148
149        int status = SoundTrigger.STATUS_ERROR;
150        try {
151            status = mModelManagementService.deleteKeyphraseSoundModel(keyphraseId, bcp47Locale);
152        } catch (RemoteException e) {
153            Log.e(TAG, "RemoteException in updateKeyphraseSoundModel");
154        }
155        return status == SoundTrigger.STATUS_OK;
156    }
157
158    private boolean verifyKeyphraseSoundModel(KeyphraseSoundModel soundModel) {
159        if (soundModel == null) {
160            Log.e(TAG, "KeyphraseSoundModel must be non-null");
161            return false;
162        }
163        if (soundModel.uuid == null) {
164            Log.e(TAG, "KeyphraseSoundModel must have a UUID");
165            return false;
166        }
167        if (soundModel.data == null) {
168            Log.e(TAG, "KeyphraseSoundModel must have data");
169            return false;
170        }
171        if (soundModel.keyphrases == null || soundModel.keyphrases.length != 1) {
172            Log.e(TAG, "Keyphrase must be exactly 1");
173            return false;
174        }
175        Keyphrase keyphrase = soundModel.keyphrases[0];
176        if (keyphrase.id <= 0) {
177            Log.e(TAG, "Keyphrase must have a valid ID");
178            return false;
179        }
180        if (keyphrase.recognitionModes < 0) {
181            Log.e(TAG, "Recognition modes must be valid");
182            return false;
183        }
184        if (keyphrase.locale == null) {
185            Log.e(TAG, "Locale must not be null");
186            return false;
187        }
188        if (keyphrase.text == null) {
189            Log.e(TAG, "Text must not be null");
190            return false;
191        }
192        if (keyphrase.users == null || keyphrase.users.length == 0) {
193            Log.e(TAG, "Keyphrase must have valid user(s)");
194            return false;
195        }
196        return true;
197    }
198}
199