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