/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.browser; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Message; import android.preference.PreferenceManager; import android.provider.ContactsContract; import android.util.Log; import android.webkit.WebSettingsClassic.AutoFillProfile; import java.util.concurrent.CountDownLatch; public class AutofillHandler { private AutoFillProfile mAutoFillProfile; // Default to zero. In the case no profile is set up, the initial // value will come from the AutoFillSettingsFragment when the user // creates a profile. Otherwise, we'll read the ID of the last used // profile from the prefs db. private int mAutoFillActiveProfileId; private static final int NO_AUTOFILL_PROFILE_SET = 0; private CountDownLatch mLoaded = new CountDownLatch(1); private Context mContext; private static final String LOGTAG = "AutofillHandler"; public AutofillHandler(Context context) { mContext = context.getApplicationContext(); } /** * Load settings from the browser app's database. It is performed in * an AsyncTask as it involves plenty of slow disk IO. * NOTE: Strings used for the preferences must match those specified * in the various preference XML files. */ public void asyncLoadFromDb() { // Run the initial settings load in an AsyncTask as it hits the // disk multiple times through SharedPreferences and SQLite. We // need to be certain though that this has completed before we start // to load pages though, so in the worst case we will block waiting // for it to finish in BrowserActivity.onCreate(). new LoadFromDb().start(); } private void waitForLoad() { try { mLoaded.await(); } catch (InterruptedException e) { Log.w(LOGTAG, "Caught exception while waiting for AutofillProfile to load."); } } private class LoadFromDb extends Thread { @Override public void run() { // Note the lack of synchronization over mAutoFillActiveProfileId and // mAutoFillProfile here. This is because we control all other access // to these members through the public functions of this class, and they // all wait for this thread via the mLoaded CountDownLatch. SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(mContext); // Read the last active AutoFill profile id. mAutoFillActiveProfileId = p.getInt( PreferenceKeys.PREF_AUTOFILL_ACTIVE_PROFILE_ID, mAutoFillActiveProfileId); // Load the autofill profile data from the database. We use a database separate // to the browser preference DB to make it easier to support multiple profiles // and switching between them. Note that this may block startup if this DB lookup // is extremely slow. We do this to ensure that if there's a profile set, the // user never sees the "setup Autofill" option. AutoFillProfileDatabase autoFillDb = AutoFillProfileDatabase.getInstance(mContext); Cursor c = autoFillDb.getProfile(mAutoFillActiveProfileId); if (c.getCount() > 0) { c.moveToFirst(); String fullName = c.getString(c.getColumnIndex( AutoFillProfileDatabase.Profiles.FULL_NAME)); String email = c.getString(c.getColumnIndex( AutoFillProfileDatabase.Profiles.EMAIL_ADDRESS)); String company = c.getString(c.getColumnIndex( AutoFillProfileDatabase.Profiles.COMPANY_NAME)); String addressLine1 = c.getString(c.getColumnIndex( AutoFillProfileDatabase.Profiles.ADDRESS_LINE_1)); String addressLine2 = c.getString(c.getColumnIndex( AutoFillProfileDatabase.Profiles.ADDRESS_LINE_2)); String city = c.getString(c.getColumnIndex( AutoFillProfileDatabase.Profiles.CITY)); String state = c.getString(c.getColumnIndex( AutoFillProfileDatabase.Profiles.STATE)); String zip = c.getString(c.getColumnIndex( AutoFillProfileDatabase.Profiles.ZIP_CODE)); String country = c.getString(c.getColumnIndex( AutoFillProfileDatabase.Profiles.COUNTRY)); String phone = c.getString(c.getColumnIndex( AutoFillProfileDatabase.Profiles.PHONE_NUMBER)); mAutoFillProfile = new AutoFillProfile(mAutoFillActiveProfileId, fullName, email, company, addressLine1, addressLine2, city, state, zip, country, phone); } c.close(); autoFillDb.close(); // At this point we've loaded the profile if there was one, so let any thread // waiting on initialization continue. mLoaded.countDown(); // Synchronization note: strictly speaking, it's possible that mAutoFillProfile // may get a value after we check below, but that's OK. This check is only an // optimisation, and we do a proper synchronized check further down when it comes // to actually setting the inferred profile. if (mAutoFillProfile == null) { // We did not load a profile from disk. Try to infer one from the user's // "me" contact. final Uri profileUri = Uri.withAppendedPath(ContactsContract.Profile.CONTENT_URI, ContactsContract.Contacts.Data.CONTENT_DIRECTORY); String name = getContactField(profileUri, ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE); // Only attempt to read other data and set a profile if we could successfully // get a name. if (name != null) { String email = getContactField(profileUri, ContactsContract.CommonDataKinds.Email.ADDRESS, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE); String phone = getContactField(profileUri, ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE); String company = getContactField(profileUri, ContactsContract.CommonDataKinds.Organization.COMPANY, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE); // Can't easily get structured postal address information (even using // CommonDataKinds.StructuredPostal) so omit prepopulating that for now. // When querying structured postal data, it often all comes back as a string // inside the "street" field. synchronized(AutofillHandler.this) { // Only use this profile if one hasn't been set inbetween the // inital import and this thread getting to this point. if (mAutoFillProfile == null) { setAutoFillProfile(new AutoFillProfile(1, name, email, company, null, null, null, null, null, null, phone), null); } } } } } private String getContactField(Uri uri, String field, String itemType) { String result = null; Cursor c = mContext.getContentResolver().query(uri, new String[] { field }, ContactsContract.Data.MIMETYPE + "=?", new String[] { itemType }, null); if (c == null) { return null; } try { // Just use the first returned value if we get more than one. if (c.moveToFirst()) { result = c.getString(0); } } finally { c.close(); } return result; } } public synchronized void setAutoFillProfile(AutoFillProfile profile, Message msg) { waitForLoad(); int profileId = NO_AUTOFILL_PROFILE_SET; if (profile != null) { profileId = profile.getUniqueId(); // Update the AutoFill DB with the new profile. new SaveProfileToDbTask(msg).execute(profile); } else { // Delete the current profile. if (mAutoFillProfile != null) { new DeleteProfileFromDbTask(msg).execute(mAutoFillProfile.getUniqueId()); } } // Make sure we set mAutoFillProfile before calling setActiveAutoFillProfileId // Calling setActiveAutoFillProfileId will trigger an update of WebViews // which will expect a new profile to be set mAutoFillProfile = profile; setActiveAutoFillProfileId(profileId); } public synchronized AutoFillProfile getAutoFillProfile() { waitForLoad(); return mAutoFillProfile; } private synchronized void setActiveAutoFillProfileId(int activeProfileId) { mAutoFillActiveProfileId = activeProfileId; Editor ed = PreferenceManager. getDefaultSharedPreferences(mContext).edit(); ed.putInt(PreferenceKeys.PREF_AUTOFILL_ACTIVE_PROFILE_ID, activeProfileId); ed.apply(); } private abstract class AutoFillProfileDbTask<T> extends AsyncTask<T, Void, Void> { AutoFillProfileDatabase mAutoFillProfileDb; Message mCompleteMessage; public AutoFillProfileDbTask(Message msg) { mCompleteMessage = msg; } @Override protected void onPostExecute(Void result) { if (mCompleteMessage != null) { mCompleteMessage.sendToTarget(); } mAutoFillProfileDb.close(); } @Override abstract protected Void doInBackground(T... values); } private class SaveProfileToDbTask extends AutoFillProfileDbTask<AutoFillProfile> { public SaveProfileToDbTask(Message msg) { super(msg); } @Override protected Void doInBackground(AutoFillProfile... values) { mAutoFillProfileDb = AutoFillProfileDatabase.getInstance(mContext); synchronized (AutofillHandler.this) { assert mAutoFillActiveProfileId != NO_AUTOFILL_PROFILE_SET; AutoFillProfile newProfile = values[0]; mAutoFillProfileDb.addOrUpdateProfile(mAutoFillActiveProfileId, newProfile); } return null; } } private class DeleteProfileFromDbTask extends AutoFillProfileDbTask<Integer> { public DeleteProfileFromDbTask(Message msg) { super(msg); } @Override protected Void doInBackground(Integer... values) { mAutoFillProfileDb = AutoFillProfileDatabase.getInstance(mContext); int id = values[0]; assert id > 0; mAutoFillProfileDb.dropProfile(id); return null; } } }