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