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

Commit 20509255 authored by Aleksandar Kiridžić's avatar Aleksandar Kiridžić Committed by Android (Google) Code Review
Browse files

Merge "speech: Add on-device speech recognition settings entry"

parents 3993aa56 31473663
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -6837,6 +6837,14 @@
        behalf.  It comes from the <xliff:g id="voice_input_service_app_name">%s</xliff:g>
        application.  Enable the use of this service?</string>
    <!-- On-device recognition settings --><skip />
    <!-- [CHAR_LIMIT=NONE] Name of the settings item to open the on-device recognition settings. -->
    <string name="on_device_recognition_settings">On-device recognition settings</string>
    <!-- [CHAR_LIMIT=NONE] Title of the on-device recognition settings -->
    <string name="on_device_recognition_settings_title">On-device recognition</string>
    <!-- [CHAR_LIMIT=NONE] Summary of the on-device recognition settings -->
    <string name="on_device_recognition_settings_summary">On-device speech recognition</string>
    <!-- [CHAR LIMIT=50] The text for the settings section that is used to set a preferred text to speech engine -->
    <string name="tts_engine_preference_title">Preferred engine</string>
    <!-- [CHAR LIMIT=50] The text for a settings screen of the currently set text to speech engine -->
+7 −0
Original line number Diff line number Diff line
@@ -63,6 +63,13 @@
            android:title="@string/voice_input_settings_title"
            android:fragment="com.android.settings.language.DefaultVoiceInputPicker" />

        <Preference
            android:key="on_device_recognition_settings"
            android:title="@string/on_device_recognition_settings_title"
            android:summary="@string/on_device_recognition_settings_summary"
            settings:controller=
                "com.android.settings.language.OnDeviceRecognitionPreferenceController" />

        <Preference
            android:key="tts_settings_summary"
            android:title="@string/tts_settings_title"
+14 −3
Original line number Diff line number Diff line
@@ -50,6 +50,7 @@ public class LanguageAndInputSettings extends DashboardFragment {

    private static final String KEY_KEYBOARDS_CATEGORY = "keyboards_category";
    private static final String KEY_SPEECH_CATEGORY = "speech_category";
    private static final String KEY_ON_DEVICE_RECOGNITION = "odsr_settings";
    private static final String KEY_TEXT_TO_SPEECH = "tts_settings_summary";
    private static final String KEY_POINTER_CATEGORY = "pointer_category";

@@ -123,11 +124,21 @@ public class LanguageAndInputSettings extends DashboardFragment {
                new DefaultVoiceInputPreferenceController(context, lifecycle);
        final TtsPreferenceController ttsPreferenceController =
                new TtsPreferenceController(context, KEY_TEXT_TO_SPEECH);
        final OnDeviceRecognitionPreferenceController onDeviceRecognitionPreferenceController =
                new OnDeviceRecognitionPreferenceController(context, KEY_ON_DEVICE_RECOGNITION);

        controllers.add(defaultVoiceInputPreferenceController);
        controllers.add(ttsPreferenceController);
        controllers.add(new PreferenceCategoryController(context,
                KEY_SPEECH_CATEGORY).setChildren(
                Arrays.asList(defaultVoiceInputPreferenceController, ttsPreferenceController)));
        List<AbstractPreferenceController> speechCategoryChildren = new ArrayList<>(
                List.of(defaultVoiceInputPreferenceController, ttsPreferenceController));

        if (onDeviceRecognitionPreferenceController.isAvailable()) {
            controllers.add(onDeviceRecognitionPreferenceController);
            speechCategoryChildren.add(onDeviceRecognitionPreferenceController);
        }

        controllers.add(new PreferenceCategoryController(context, KEY_SPEECH_CATEGORY)
                .setChildren(speechCategoryChildren));

        // Pointer
        final PointerSpeedController pointerController = new PointerSpeedController(context);
+133 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.settings.language;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.preference.Preference;

import com.android.internal.R;
import com.android.settings.core.BasePreferenceController;

import java.util.ArrayList;
import java.util.Optional;

/** Controller of the On-device recognition preference. */
public class OnDeviceRecognitionPreferenceController extends BasePreferenceController {

    private static final String TAG = "OnDeviceRecognitionPreferenceController";

    private Optional<Intent> mIntent;

    public OnDeviceRecognitionPreferenceController(Context context, String preferenceKey) {
        super(context, preferenceKey);
    }

    @Override
    public int getAvailabilityStatus() {
        if (mIntent == null) {
            mIntent = Optional.ofNullable(onDeviceRecognitionIntent());
        }
        return mIntent.isPresent()
                ? AVAILABLE
                : CONDITIONALLY_UNAVAILABLE;
    }

    @Override
    public void updateState(Preference preference) {
        super.updateState(preference);
        if (mIntent != null && mIntent.isPresent()) {
            preference.setIntent(mIntent.get());
        }
    }

    /**
     * Create an {@link Intent} for the activity in the default on-device recognizer service if
     * there is a properly defined speech recognition xml meta-data for that service.
     *
     * @return {@link Intent} if the proper activity is fount, {@code null} otherwise.
     */
    @Nullable
    private Intent onDeviceRecognitionIntent() {
        final String resString = mContext.getString(
                R.string.config_defaultOnDeviceSpeechRecognitionService);

        if (resString == null) {
            Log.v(TAG, "No on-device recognizer, intent not created.");
            return null;
        }

        final ComponentName defaultOnDeviceRecognizerComponentName =
                ComponentName.unflattenFromString(resString);

        if (defaultOnDeviceRecognizerComponentName == null) {
            Log.v(TAG, "Invalid on-device recognizer string format, intent not created.");
            return null;
        }

        final ArrayList<VoiceInputHelper.RecognizerInfo> validRecognitionServices =
                VoiceInputHelper.validRecognitionServices(mContext);

        if (validRecognitionServices.isEmpty()) {
            Log.v(TAG, "No speech recognition services"
                    + "with proper `recognition-service` meta-data found.");
            return null;
        }

        // Filter the recognizer services which are in the same package as the default on-device
        // speech recognizer and have a settings activity defined in the meta-data.
        final ArrayList<VoiceInputHelper.RecognizerInfo> validOnDeviceRecognitionServices =
                new ArrayList<>();
        for (VoiceInputHelper.RecognizerInfo recognizerInfo: validRecognitionServices) {
            if (!defaultOnDeviceRecognizerComponentName.getPackageName().equals(
                    recognizerInfo.mService.packageName)) {
                Log.v(TAG, String.format("Recognition service not in the same package as the "
                        + "default on-device recognizer: %s.",
                        recognizerInfo.mComponentName.flattenToString()));
            } else if (recognizerInfo.mSettings == null) {
                Log.v(TAG, String.format("Recognition service with no settings activity: %s.",
                        recognizerInfo.mComponentName.flattenToString()));
            } else {
                validOnDeviceRecognitionServices.add(recognizerInfo);
                Log.v(TAG, String.format("Recognition service in the same package as the default "
                                + "on-device recognizer with settings activity: %s.",
                        recognizerInfo.mSettings.flattenToString()));
            }
        }

        if (validOnDeviceRecognitionServices.isEmpty()) {
            Log.v(TAG, "No speech recognition services with proper `recognition-service` "
                    + "meta-data found in the same package as the default on-device recognizer.");
            return null;
        }

        // Not more than one proper recognition services should be found in the same
        // package as the default on-device recognizer. If that happens,
        // the first one which passed the filter will be selected.
        if (validOnDeviceRecognitionServices.size() > 1) {
            Log.w(TAG, "More than one recognition services with proper `recognition-service` "
                    + "meta-data found in the same package as the default on-device recognizer.");
        }
        VoiceInputHelper.RecognizerInfo chosenRecognizer = validOnDeviceRecognitionServices.get(0);

        return new Intent(Intent.ACTION_MAIN).setComponent(chosenRecognizer.mSettings);
    }
}
+115 −62
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.provider.Settings;
import android.speech.RecognitionService;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.util.Xml;

import org.xmlpull.v1.XmlPullParser;
@@ -44,12 +45,11 @@ public final class VoiceInputHelper {
    static final String TAG = "VoiceInputHelper";
    final Context mContext;

    final List<ResolveInfo> mAvailableRecognition;

    /**
     * Base info of the Voice Input provider.
     *
     * TODO: Remove this superclass as we only have 1 class now (RecognizerInfo).
     * TODO: Group recognition service xml meta-data attributes in a single class.
     */
    public static class BaseInfo implements Comparable<BaseInfo> {
        public final ServiceInfo mService;
@@ -90,16 +90,12 @@ public final class VoiceInputHelper {
        }
    }

    final ArrayList<RecognizerInfo> mAvailableRecognizerInfos = new ArrayList<>();
    ArrayList<RecognizerInfo> mAvailableRecognizerInfos = new ArrayList<>();

    ComponentName mCurrentRecognizer;

    public VoiceInputHelper(Context context) {
        mContext = context;

        mAvailableRecognition = mContext.getPackageManager().queryIntentServices(
                new Intent(RecognitionService.SERVICE_INTERFACE),
                PackageManager.GET_META_DATA);
    }

    /** Draws the UI of the Voice Input picker page. */
@@ -113,63 +109,120 @@ public final class VoiceInputHelper {
            mCurrentRecognizer = null;
        }

        // Iterate through all the available recognizers and load up their info to show
        // in the preference.
        int size = mAvailableRecognition.size();
        for (int i = 0; i < size; i++) {
            ResolveInfo resolveInfo = mAvailableRecognition.get(i);
            ComponentName comp = new ComponentName(resolveInfo.serviceInfo.packageName,
                    resolveInfo.serviceInfo.name);
            ServiceInfo si = resolveInfo.serviceInfo;
            String settingsActivity = null;
            // Always show in voice input settings unless specifically set to False.
        final ArrayList<RecognizerInfo> validRecognitionServices =
                validRecognitionServices(mContext);

        // Filter all recognizers which can be selected as default or are the current recognizer.
        mAvailableRecognizerInfos = new ArrayList<>();
        for (RecognizerInfo recognizerInfo: validRecognitionServices) {
            if (recognizerInfo.mSelectableAsDefault || new ComponentName(
                    recognizerInfo.mService.packageName, recognizerInfo.mService.name)
                    .equals(mCurrentRecognizer)) {
                mAvailableRecognizerInfos.add(recognizerInfo);
            }
        }

        Collections.sort(mAvailableRecognizerInfos);
    }

    /**
     * Query all services with {@link RecognitionService#SERVICE_INTERFACE} intent. Filter only
     * those which have proper xml meta-data which start with a `recognition-service` tag.
     * Filtered services are sorted by their labels in the ascending order.
     *
     * @param context {@link Context} inside which the settings app is run.
     *
     * @return {@link ArrayList}&lt;{@link RecognizerInfo}&gt;
     * containing info about the filtered speech recognition services.
     */
    static ArrayList<RecognizerInfo> validRecognitionServices(Context context) {
        final List<ResolveInfo> resolvedRecognitionServices =
                context.getPackageManager().queryIntentServices(
                        new Intent(RecognitionService.SERVICE_INTERFACE),
                        PackageManager.GET_META_DATA);

        final ArrayList<RecognizerInfo> validRecognitionServices = new ArrayList<>();

        for (ResolveInfo resolveInfo: resolvedRecognitionServices) {
            final ServiceInfo serviceInfo = resolveInfo.serviceInfo;

            final Pair<String, Boolean> recognitionServiceAttributes =
                    parseRecognitionServiceXmlMetadata(context, serviceInfo);

            if (recognitionServiceAttributes != null) {
                validRecognitionServices.add(new RecognizerInfo(
                        context.getPackageManager(),
                        serviceInfo,
                        recognitionServiceAttributes.first      /* settingsActivity */,
                        recognitionServiceAttributes.second     /* selectableAsDefault */));
            }
        }

        return validRecognitionServices;
    }

    /**
     * Load recognition service's xml meta-data and parse it. Return the meta-data attributes,
     * namely, `settingsActivity` {@link String} and `selectableAsDefault` {@link Boolean}.
     *
     * <p>Parsing fails if the meta-data for the given service is not found
     * or the found meta-data does not start with a `recognition-service`.</p>
     *
     * @param context {@link Context} inside which the settings app is run.
     * @param serviceInfo {@link ServiceInfo} containing info
     * about the speech recognition service in question.
     *
     * @return {@link Pair}&lt;{@link String}, {@link Boolean}&gt;  containing `settingsActivity`
     * and `selectableAsDefault` attributes if the parsing was successful, {@code null} otherwise.
     */
    private static Pair<String, Boolean> parseRecognitionServiceXmlMetadata(
            Context context, ServiceInfo serviceInfo) {
        // Default recognition service attribute values.
        // Every recognizer can be selected unless specified otherwise.
        String settingsActivity;
        boolean selectableAsDefault = true;
            try (XmlResourceParser parser = si.loadXmlMetaData(mContext.getPackageManager(),
                    RecognitionService.SERVICE_META_DATA)) {

        // Parse xml meta-data.
        try (XmlResourceParser parser = serviceInfo.loadXmlMetaData(
                context.getPackageManager(), RecognitionService.SERVICE_META_DATA)) {
            if (parser == null) {
                    throw new XmlPullParserException("No " + RecognitionService.SERVICE_META_DATA
                            + " meta-data for " + si.packageName);
                throw new XmlPullParserException(String.format("No %s meta-data for %s package",
                        RecognitionService.SERVICE_META_DATA, serviceInfo.packageName));
            }

                Resources res = mContext.getPackageManager().getResourcesForApplication(
                        si.applicationInfo);

                AttributeSet attrs = Xml.asAttributeSet(parser);
            final Resources res = context.getPackageManager().getResourcesForApplication(
                    serviceInfo.applicationInfo);
            final AttributeSet attrs = Xml.asAttributeSet(parser);

            // Xml meta-data must start with a `recognition-service tag`.
            int type;
            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                    && type != XmlPullParser.START_TAG) {
                // Intentionally do nothing.
            }

                String nodeName = parser.getName();
            final String nodeName = parser.getName();
            if (!"recognition-service".equals(nodeName)) {
                    throw new XmlPullParserException(
                            "Meta-data does not start with recognition-service tag");
                throw new XmlPullParserException(String.format(
                        "%s package meta-data does not start with a `recognition-service` tag",
                        serviceInfo.packageName));
            }

                TypedArray array = res.obtainAttributes(attrs,
            final TypedArray array = res.obtainAttributes(attrs,
                    com.android.internal.R.styleable.RecognitionService);
            settingsActivity = array.getString(
                    com.android.internal.R.styleable.RecognitionService_settingsActivity);
            selectableAsDefault = array.getBoolean(
                    com.android.internal.R.styleable.RecognitionService_selectableAsDefault,
                        true);
                    selectableAsDefault);
            array.recycle();
            } catch (XmlPullParserException e) {
                Log.e(TAG, "error parsing recognition service meta-data", e);
            } catch (IOException e) {
                Log.e(TAG, "error parsing recognition service meta-data", e);
            } catch (PackageManager.NameNotFoundException e) {
                Log.e(TAG, "error parsing recognition service meta-data", e);
        } catch (XmlPullParserException | IOException
                | PackageManager.NameNotFoundException e) {
            Log.e(TAG, String.format("Error parsing %s package recognition service meta-data",
                    serviceInfo.packageName), e);
            return null;
        }
            // The current recognizer must always be shown in the settings, whatever its
            // selectableAsDefault value is.
            if (selectableAsDefault || comp.equals(mCurrentRecognizer)) {
                mAvailableRecognizerInfos.add(new RecognizerInfo(mContext.getPackageManager(),
                        resolveInfo.serviceInfo, settingsActivity, selectableAsDefault));
            }
        }
        Collections.sort(mAvailableRecognizerInfos);

        return Pair.create(settingsActivity, selectableAsDefault);
    }
}