1 /*
2  * Copyright (C) 2017 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 package com.example.android.autofill.service.settings;
17 
18 import static com.example.android.autofill.service.util.Util.DalCheckRequirement.AllUrls;
19 import static com.example.android.autofill.service.util.Util.DalCheckRequirement.Disabled;
20 import static com.example.android.autofill.service.util.Util.DalCheckRequirement.LoginOnly;
21 import static com.example.android.autofill.service.util.Util.logd;
22 import static com.example.android.autofill.service.util.Util.logw;
23 
24 import android.content.Context;
25 import android.content.DialogInterface;
26 import android.content.Intent;
27 import android.content.SharedPreferences;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.provider.Settings;
31 import android.support.design.widget.Snackbar;
32 import android.support.v7.app.AlertDialog;
33 import android.support.v7.app.AppCompatActivity;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.view.autofill.AutofillManager;
38 import android.widget.CompoundButton;
39 import android.widget.EditText;
40 import android.widget.ImageView;
41 import android.widget.NumberPicker;
42 import android.widget.RadioGroup;
43 import android.widget.Switch;
44 import android.widget.TextView;
45 
46 import com.example.android.autofill.service.R;
47 import com.example.android.autofill.service.data.AutofillDataBuilder;
48 import com.example.android.autofill.service.data.DataCallback;
49 import com.example.android.autofill.service.data.FakeAutofillDataBuilder;
50 import com.example.android.autofill.service.data.source.DefaultFieldTypesSource;
51 import com.example.android.autofill.service.data.source.PackageVerificationDataSource;
52 import com.example.android.autofill.service.data.source.local.DefaultFieldTypesLocalJsonSource;
53 import com.example.android.autofill.service.data.source.local.LocalAutofillDataSource;
54 import com.example.android.autofill.service.data.source.local.SharedPrefsPackageVerificationRepository;
55 import com.example.android.autofill.service.data.source.local.dao.AutofillDao;
56 import com.example.android.autofill.service.data.source.local.db.AutofillDatabase;
57 import com.example.android.autofill.service.model.DatasetWithFilledAutofillFields;
58 import com.example.android.autofill.service.model.FieldTypeWithHeuristics;
59 import com.example.android.autofill.service.util.AppExecutors;
60 import com.example.android.autofill.service.util.Util;
61 import com.google.gson.GsonBuilder;
62 
63 import java.util.List;
64 
65 public class SettingsActivity extends AppCompatActivity {
66     private static final String TAG = "SettingsActivity";
67     private static final int REQUEST_CODE_SET_DEFAULT = 1;
68     private AutofillManager mAutofillManager;
69     private LocalAutofillDataSource mLocalAutofillDataSource;
70     private PackageVerificationDataSource mPackageVerificationDataSource;
71     private MyPreferences mPreferences;
72     private String mPackageName;
73 
74     @Override
onCreate(Bundle savedInstanceState)75     public void onCreate(Bundle savedInstanceState) {
76         super.onCreate(savedInstanceState);
77         setContentView(R.layout.multidataset_service_settings_activity);
78         SharedPreferences localAfDataSourceSharedPrefs =
79                 getSharedPreferences(LocalAutofillDataSource.SHARED_PREF_KEY, Context.MODE_PRIVATE);
80         DefaultFieldTypesSource defaultFieldTypesSource =
81                 DefaultFieldTypesLocalJsonSource.getInstance(getResources(),
82                         new GsonBuilder().create());
83         AutofillDao autofillDao = AutofillDatabase.getInstance(
84                 this, defaultFieldTypesSource, new AppExecutors()).autofillDao();
85         mPackageName = getPackageName();
86         mLocalAutofillDataSource = LocalAutofillDataSource.getInstance(localAfDataSourceSharedPrefs,
87                 autofillDao, new AppExecutors());
88         mAutofillManager = getSystemService(AutofillManager.class);
89         mPackageVerificationDataSource =
90                 SharedPrefsPackageVerificationRepository.getInstance(this);
91         mPreferences = MyPreferences.getInstance(this);
92         setupSettingsSwitch(R.id.settings_auth_responses_container,
93                 R.id.settings_auth_responses_label,
94                 R.id.settings_auth_responses_switch,
95                 mPreferences.isResponseAuth(),
96                 (compoundButton, isResponseAuth) -> mPreferences.setResponseAuth(isResponseAuth));
97         setupSettingsSwitch(R.id.settings_auth_datasets_container,
98                 R.id.settings_auth_datasets_label,
99                 R.id.settings_auth_datasets_switch,
100                 mPreferences.isDatasetAuth(),
101                 (compoundButton, isDatasetAuth) -> mPreferences.setDatasetAuth(isDatasetAuth));
102         setupSettingsButton(R.id.settings_add_data_container,
103                 R.id.settings_add_data_label,
104                 R.id.settings_add_data_icon,
105                 (view) -> buildAddDataDialog().show());
106         setupSettingsButton(R.id.settings_clear_data_container,
107                 R.id.settings_clear_data_label,
108                 R.id.settings_clear_data_icon,
109                 (view) -> buildClearDataDialog().show());
110         setupSettingsButton(R.id.settings_auth_credentials_container,
111                 R.id.settings_auth_credentials_label,
112                 R.id.settings_auth_credentials_icon,
113                 (view) -> {
114                     if (mPreferences.getMainPassword() != null) {
115                         buildCurrentCredentialsDialog().show();
116                     } else {
117                         buildNewCredentialsDialog().show();
118                     }
119                 });
120         setupSettingsSwitch(R.id.settingsSetServiceContainer,
121                 R.id.settingsSetServiceLabel,
122                 R.id.settingsSetServiceSwitch,
123                 mAutofillManager.hasEnabledAutofillServices(),
124                 (compoundButton, serviceSet) -> setService(serviceSet));
125         RadioGroup loggingLevelContainer = findViewById(R.id.loggingLevelContainer);
126         Util.LogLevel loggingLevel = mPreferences.getLoggingLevel();
127         Util.setLoggingLevel(loggingLevel);
128         switch (loggingLevel) {
129             case Off:
130                 loggingLevelContainer.check(R.id.loggingOff);
131                 break;
132             case Debug:
133                 loggingLevelContainer.check(R.id.loggingDebug);
134                 break;
135             case Verbose:
136                 loggingLevelContainer.check(R.id.loggingVerbose);
137                 break;
138         }
139         loggingLevelContainer.setOnCheckedChangeListener((group, checkedId) -> {
140             switch (checkedId) {
141                 case R.id.loggingOff:
142                     mPreferences.setLoggingLevel(Util.LogLevel.Off);
143                     break;
144                 case R.id.loggingDebug:
145                     mPreferences.setLoggingLevel(Util.LogLevel.Debug);
146                     break;
147                 case R.id.loggingVerbose:
148                     mPreferences.setLoggingLevel(Util.LogLevel.Verbose);
149                     break;
150             }
151         });
152         RadioGroup dalCheckRequirementContainer = findViewById(R.id.dalCheckRequirementContainer);
153         Util.DalCheckRequirement dalCheckRequirement = mPreferences.getDalCheckRequirement();
154         switch (dalCheckRequirement) {
155             case Disabled:
156                 dalCheckRequirementContainer.check(R.id.dalDisabled);
157                 break;
158             case LoginOnly:
159                 dalCheckRequirementContainer.check(R.id.dalLoginOnly);
160                 break;
161             case AllUrls:
162                 dalCheckRequirementContainer.check(R.id.dalAllUrls);
163                 break;
164         }
165         dalCheckRequirementContainer.setOnCheckedChangeListener((group, checkedId) -> {
166             switch (checkedId) {
167                 case R.id.dalDisabled:
168                     mPreferences.setDalCheckRequired(Disabled);
169                     break;
170                 case R.id.dalLoginOnly:
171                     mPreferences.setDalCheckRequired(LoginOnly);
172                     break;
173                 case R.id.dalAllUrls:
174                     mPreferences.setDalCheckRequired(AllUrls);
175                     break;
176             }
177         });
178     }
179 
buildClearDataDialog()180     private AlertDialog buildClearDataDialog() {
181         return new AlertDialog.Builder(SettingsActivity.this)
182                 .setMessage(R.string.settings_clear_data_confirmation)
183                 .setTitle(R.string.settings_clear_data_confirmation_title)
184                 .setNegativeButton(R.string.settings_cancel, null)
185                 .setPositiveButton(R.string.settings_ok, (dialog, which) -> {
186                     mLocalAutofillDataSource.clear();
187                     mPackageVerificationDataSource.clear();
188                     mPreferences.clearCredentials();
189                     dialog.dismiss();
190                 })
191                 .create();
192     }
193 
buildAddDataDialog()194     private AlertDialog buildAddDataDialog() {
195         NumberPicker numberOfDatasetsPicker = LayoutInflater
196                 .from(SettingsActivity.this)
197                 .inflate(R.layout.multidataset_service_settings_add_data_dialog, null)
198                 .findViewById(R.id.number_of_datasets_picker);
199         numberOfDatasetsPicker.setMinValue(0);
200         numberOfDatasetsPicker.setMaxValue(10);
201         numberOfDatasetsPicker.setWrapSelectorWheel(false);
202         return new AlertDialog.Builder(SettingsActivity.this)
203                 .setTitle(R.string.settings_add_data_title)
204                 .setNegativeButton(R.string.settings_cancel, null)
205                 .setMessage(R.string.settings_select_number_of_datasets)
206                 .setView(numberOfDatasetsPicker)
207                 .setPositiveButton(R.string.settings_ok, (dialog, which) -> {
208                     int numOfDatasets = numberOfDatasetsPicker.getValue();
209                     mPreferences.setNumberDatasets(numOfDatasets);
210                     mLocalAutofillDataSource.getFieldTypes(new DataCallback<List<FieldTypeWithHeuristics>>() {
211                         @Override
212                         public void onLoaded(List<FieldTypeWithHeuristics> fieldTypes) {
213                             boolean saved = buildAndSaveMockedAutofillFieldCollections(
214                                     fieldTypes, numOfDatasets);
215                             dialog.dismiss();
216                             if (saved) {
217                                 Snackbar.make(findViewById(R.id.settings_layout),
218                                         getResources().getQuantityString(
219                                                 R.plurals.settings_add_data_success,
220                                                 numOfDatasets, numOfDatasets),
221                                         Snackbar.LENGTH_SHORT).show();
222                             }
223                         }
224 
225                         @Override
226                         public void onDataNotAvailable(String msg, Object... params) {
227 
228                         }
229                     });
230                 })
231                 .create();
232     }
233 
234     public boolean buildAndSaveMockedAutofillFieldCollections(List<FieldTypeWithHeuristics> fieldTypes,
235             int numOfDatasets) {
236         if (numOfDatasets < 0 || numOfDatasets > 10) {
237             logw("Number of Datasets (%d) out of range.", numOfDatasets);
238         }
239         for (int i = 0; i < numOfDatasets; i++) {
240             int datasetNumber = mLocalAutofillDataSource.getDatasetNumber();
241             AutofillDataBuilder autofillDataBuilder =
242                     new FakeAutofillDataBuilder(fieldTypes, mPackageName, datasetNumber);
243             List<DatasetWithFilledAutofillFields> datasetsWithFilledAutofillFields =
244                     autofillDataBuilder.buildDatasetsByPartition(datasetNumber);
245             // Save datasets to database.
246             mLocalAutofillDataSource.saveAutofillDatasets(datasetsWithFilledAutofillFields);
247         }
248         return true;
249     }
250 
251     private AlertDialog.Builder prepareCredentialsDialog() {
252         return new AlertDialog.Builder(SettingsActivity.this)
253                 .setTitle(R.string.settings_auth_change_credentials_title)
254                 .setNegativeButton(R.string.settings_cancel, null);
255     }
256 
257     private AlertDialog buildCurrentCredentialsDialog() {
258         final EditText currentPasswordField = LayoutInflater
259                 .from(SettingsActivity.this)
260                 .inflate(R.layout.multidataset_service_settings_authentication_dialog, null)
261                 .findViewById(R.id.main_password_field);
262         return prepareCredentialsDialog()
263                 .setMessage(R.string.settings_auth_enter_current_password)
264                 .setView(currentPasswordField)
265                 .setPositiveButton(R.string.settings_ok, new DialogInterface.OnClickListener() {
266                     @Override
267                     public void onClick(DialogInterface dialog, int which) {
268                         String password = currentPasswordField.getText().toString();
269                         if (mPreferences.getMainPassword()
270                                 .equals(password)) {
271                             buildNewCredentialsDialog().show();
272                             dialog.dismiss();
273                         }
274                     }
275                 })
276                 .create();
277     }
278 
279     private AlertDialog buildNewCredentialsDialog() {
280         final EditText newPasswordField = LayoutInflater
281                 .from(SettingsActivity.this)
282                 .inflate(R.layout.multidataset_service_settings_authentication_dialog, null)
283                 .findViewById(R.id.main_password_field);
284         return prepareCredentialsDialog()
285                 .setMessage(R.string.settings_auth_enter_new_password)
286                 .setView(newPasswordField)
287                 .setPositiveButton(R.string.settings_ok, (dialog, which) -> {
288                     String password = newPasswordField.getText().toString();
289                     mPreferences.setMainPassword(password);
290                     dialog.dismiss();
291                 })
292                 .create();
293     }
294 
295     private void setupSettingsSwitch(int containerId, int labelId, int switchId, boolean checked,
296             CompoundButton.OnCheckedChangeListener checkedChangeListener) {
297         ViewGroup container = findViewById(containerId);
298         String switchLabel = ((TextView) container.findViewById(labelId)).getText().toString();
299         final Switch switchView = container.findViewById(switchId);
300         switchView.setContentDescription(switchLabel);
301         switchView.setChecked(checked);
302         container.setOnClickListener((view) -> switchView.performClick());
303         switchView.setOnCheckedChangeListener(checkedChangeListener);
304     }
305 
306     private void setupSettingsButton(int containerId, int labelId, int imageViewId,
307             final View.OnClickListener onClickListener) {
308         ViewGroup container = findViewById(containerId);
309         TextView buttonLabel = container.findViewById(labelId);
310         String buttonLabelText = buttonLabel.getText().toString();
311         ImageView imageView = container.findViewById(imageViewId);
312         imageView.setContentDescription(buttonLabelText);
313         container.setOnClickListener(onClickListener);
314     }
315 
316     private void setService(boolean enableService) {
317         if (enableService) {
318             startEnableService();
319         } else {
320             disableService();
321         }
322     }
323 
324     private void disableService() {
325         if (mAutofillManager != null && mAutofillManager.hasEnabledAutofillServices()) {
326             mAutofillManager.disableAutofillServices();
327             Snackbar.make(findViewById(R.id.settings_layout),
328                     R.string.settings_autofill_disabled_message, Snackbar.LENGTH_SHORT).show();
329         } else {
330             logd("Sample service already disabled.");
331         }
332     }
333 
334     private void startEnableService() {
335         if (mAutofillManager != null && !mAutofillManager.hasEnabledAutofillServices()) {
336             Intent intent = new Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE);
337             intent.setData(Uri.parse("package:com.example.android.autofill.service"));
338             logd(TAG, "enableService(): intent=%s", intent);
339             startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT);
340         } else {
341             logd("Sample service already enabled.");
342         }
343     }
344 
345     @Override
346     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
347         logd(TAG, "onActivityResult(): req=%s", requestCode);
348         switch (requestCode) {
349             case REQUEST_CODE_SET_DEFAULT:
350                 onDefaultServiceSet(resultCode);
351                 break;
352         }
353     }
354 
355     private void onDefaultServiceSet(int resultCode) {
356         logd(TAG, "resultCode=%d", resultCode);
357         switch (resultCode) {
358             case RESULT_OK:
359                 logd("Autofill service set.");
360                 Snackbar.make(findViewById(R.id.settings_layout),
361                         R.string.settings_autofill_service_set, Snackbar.LENGTH_SHORT)
362                         .show();
363                 break;
364             case RESULT_CANCELED:
365                 logd("Autofill service not selected.");
366                 Snackbar.make(findViewById(R.id.settings_layout),
367                         R.string.settings_autofill_service_not_set, Snackbar.LENGTH_SHORT)
368                         .show();
369                 break;
370         }
371     }
372 }
373