Home | History | Annotate | Download | only in settings
      1 /*
      2  * Copyright (C) 2018 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.tv.settings;
     18 
     19 import android.annotation.Nullable;
     20 import android.content.Context;
     21 import android.content.pm.PackageManager;
     22 import android.content.res.Resources;
     23 import android.database.ContentObserver;
     24 import android.database.Cursor;
     25 import android.net.Uri;
     26 import android.os.AsyncTask;
     27 import android.support.v14.preference.SwitchPreference;
     28 import android.support.v7.preference.Preference;
     29 import android.text.TextUtils;
     30 import android.util.Log;
     31 
     32 import com.android.settingslib.core.AbstractPreferenceController;
     33 
     34 /**
     35  * Controller for the hotword switch preference.
     36  */
     37 public class HotwordSwitchController extends AbstractPreferenceController {
     38 
     39     private static final String TAG = "HotwordController";
     40     private static final Uri URI = Uri.parse("content://com.google.android.katniss.search."
     41             + "searchapi.VoiceInteractionProvider/sharedvalue");
     42     static final String ASSISTANT_PGK_NAME = "com.google.android.katniss";
     43     static final String ACTION_HOTWORD_ENABLE =
     44             "com.google.android.assistant.HOTWORD_ENABLE";
     45     static final String ACTION_HOTWORD_DISABLE =
     46             "com.google.android.assistant.HOTWORD_DISABLE";
     47 
     48     static final String KEY_HOTWORD_SWITCH = "hotword_switch";
     49 
     50     /** Listen to hotword state events. */
     51     public interface HotwordStateListener {
     52         /** hotword state has changed */
     53         void onHotwordStateChanged();
     54         /** request to enable hotwording */
     55         void onHotwordEnable();
     56         /** request to disable hotwording */
     57         void onHotwordDisable();
     58     }
     59 
     60     private ContentObserver mHotwordSwitchObserver = new ContentObserver(null) {
     61         @Override
     62         public void onChange(boolean selfChange) {
     63             onChange(selfChange, null);
     64         }
     65 
     66         @Override
     67         public void onChange(boolean selfChange, Uri uri) {
     68             new HotwordLoader().execute();
     69         }
     70     };
     71 
     72     private static class HotwordState {
     73         private boolean mHotwordEnabled;
     74         private boolean mHotwordSwitchVisible;
     75         private boolean mHotwordSwitchDisabled;
     76         private String mHotwordSwitchTitle;
     77         private String mHotwordSwitchDescription;
     78     }
     79 
     80     /**
     81      * Task to retrieve state of the hotword switch from a content provider.
     82      */
     83     private class HotwordLoader extends AsyncTask<Void, Void, HotwordState> {
     84 
     85         @Override
     86         protected HotwordState doInBackground(Void... voids) {
     87             HotwordState hotwordState = new HotwordState();
     88             Context context = mContext.getApplicationContext();
     89             try (Cursor cursor = context.getContentResolver().query(URI, null, null, null,
     90                     null, null)) {
     91                 if (cursor != null) {
     92                     int idxKey = cursor.getColumnIndex("key");
     93                     int idxValue = cursor.getColumnIndex("value");
     94                     if (idxKey < 0 || idxValue < 0) {
     95                         return null;
     96                     }
     97                     while (cursor.moveToNext()) {
     98                         String key = cursor.getString(idxKey);
     99                         String value = cursor.getString(idxValue);
    100                         if (key == null || value == null) {
    101                             continue;
    102                         }
    103                         try {
    104                             switch (key) {
    105                                 case "is_listening_for_hotword":
    106                                     hotwordState.mHotwordEnabled = Integer.valueOf(value) == 1;
    107                                     break;
    108                                 case "is_hotword_switch_visible":
    109                                     hotwordState.mHotwordSwitchVisible =
    110                                             Integer.valueOf(value) == 1;
    111                                     break;
    112                                 case "is_hotword_switch_disabled":
    113                                     hotwordState.mHotwordSwitchDisabled =
    114                                             Integer.valueOf(value) == 1;
    115                                     break;
    116                                 case "hotword_switch_title":
    117                                     hotwordState.mHotwordSwitchTitle = getLocalizedStringResource(
    118                                             value, mContext.getString(R.string.hotwording_title));
    119                                     break;
    120                                 case "hotword_switch_description":
    121                                     hotwordState.mHotwordSwitchDescription =
    122                                             getLocalizedStringResource(value, null);
    123                                     break;
    124                                 default:
    125                             }
    126                         } catch (NumberFormatException e) {
    127                             Log.w(TAG, "Invalid value.", e);
    128                         }
    129                     }
    130                     return hotwordState;
    131                 }
    132             } catch (Exception e) {
    133                 Log.e(TAG, "Exception loading hotword state.", e);
    134             }
    135             return null;
    136         }
    137 
    138         @Override
    139         protected void onPostExecute(HotwordState hotwordState) {
    140             if (hotwordState != null) {
    141                 mHotwordState = hotwordState;
    142             }
    143             mHotwordStateListener.onHotwordStateChanged();
    144         }
    145     }
    146 
    147     private HotwordStateListener mHotwordStateListener = null;
    148     private HotwordState mHotwordState = new HotwordState();
    149 
    150     public HotwordSwitchController(Context context) {
    151         super(context);
    152     }
    153 
    154     /** Must be invoked to init controller and observe state changes. */
    155     public void init(HotwordStateListener listener) {
    156         mHotwordState.mHotwordSwitchTitle = mContext.getString(R.string.hotwording_title);
    157         mHotwordStateListener = listener;
    158         try {
    159             mContext.getContentResolver().registerContentObserver(URI, true,
    160                     mHotwordSwitchObserver);
    161             new HotwordLoader().execute();
    162         } catch (SecurityException e) {
    163             Log.w(TAG, "Hotword content provider not found.", e);
    164         }
    165     }
    166 
    167     /** Must be invoked by caller to unregister receivers. */
    168     public void unregister() {
    169         mContext.getContentResolver().unregisterContentObserver(mHotwordSwitchObserver);
    170     }
    171 
    172     @Override
    173     public boolean isAvailable() {
    174         return mHotwordState.mHotwordSwitchVisible;
    175     }
    176 
    177     @Override
    178     public String getPreferenceKey() {
    179         return KEY_HOTWORD_SWITCH;
    180     }
    181 
    182     @Override
    183     public void updateState(Preference preference) {
    184         super.updateState(preference);
    185         if (KEY_HOTWORD_SWITCH.equals(preference.getKey())) {
    186             ((SwitchPreference) preference).setChecked(mHotwordState.mHotwordEnabled);
    187             preference.setIcon(mHotwordState.mHotwordEnabled
    188                     ? R.drawable.ic_mic_on : R.drawable.ic_mic_off);
    189             preference.setEnabled(!mHotwordState.mHotwordSwitchDisabled);
    190             preference.setTitle(mHotwordState.mHotwordSwitchTitle);
    191             preference.setSummary(mHotwordState.mHotwordSwitchDescription);
    192         }
    193     }
    194 
    195     @Override
    196     public boolean handlePreferenceTreeClick(Preference preference) {
    197         if (KEY_HOTWORD_SWITCH.equals(preference.getKey())) {
    198             SwitchPreference hotwordSwitchPref = (SwitchPreference) preference;
    199             if (hotwordSwitchPref.isChecked()) {
    200                 hotwordSwitchPref.setChecked(false);
    201                 mHotwordStateListener.onHotwordEnable();
    202             } else {
    203                 hotwordSwitchPref.setChecked(true);
    204                 mHotwordStateListener.onHotwordDisable();
    205             }
    206         }
    207         return super.handlePreferenceTreeClick(preference);
    208     }
    209 
    210     /**
    211      * Extracts a string resource from a given package.
    212      *
    213      * @param resource fully qualified resource identifier,
    214      *        e.g. com.google.android.katniss:string/enable_ok_google
    215      * @param defaultValue returned if resource cannot be extracted
    216      */
    217     private String getLocalizedStringResource(String resource, @Nullable String defaultValue) {
    218         if (TextUtils.isEmpty(resource)) {
    219             return defaultValue;
    220         }
    221         try {
    222             String[] parts = TextUtils.split(resource, ":");
    223             if (parts.length == 0) {
    224                 return defaultValue;
    225             }
    226             final String pkgName = parts[0];
    227             Context targetContext = mContext.createPackageContext(pkgName, 0);
    228             int resId = targetContext.getResources().getIdentifier(resource, null, null);
    229             if (resId != 0) {
    230                 return targetContext.getResources().getString(resId);
    231             }
    232         } catch (Resources.NotFoundException | PackageManager.NameNotFoundException
    233                 | SecurityException e) {
    234             Log.w(TAG, "Unable to get string resource.", e);
    235         }
    236         return defaultValue;
    237     }
    238 }
    239