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