Don't preload AsYouTypeFormatter

- This took 200ms of startup time at least, and a lot more under heavy load,
  especially when the flash is busy.  (200ms is mostly disk I/O.)

- Instead, make sure we always use AsyncTask to create
  PhoneNumberFormattingTextWatcher, which wass the only thing that uses
  AsYouTypeFormatter.

- DialpadFragment already had an AsnycTask.  Moved it to the new class UiUtils
  and use it in TextFieldsEditorView, which is the only other callsite.

- Also improved the logging for account loading.  We used to log only CPU
  time, but what we really care is the actual wall time.  Because
  account loading involves a lot of file access (e.g. loading 3rd party
  apks), only measuring CPU time is not too useful.
  (In fact, on my phone, loading accounts takes only 50ms CPU time but
  >500ms wall time.)

Bug 5195464

Change-Id: I2b51e864d75831bdbb9e424aa846133d49d6ef94
diff --git a/src/com/android/contacts/dialpad/DialpadFragment.java b/src/com/android/contacts/dialpad/DialpadFragment.java
index e3e8875..1ff16d2 100644
--- a/src/com/android/contacts/dialpad/DialpadFragment.java
+++ b/src/com/android/contacts/dialpad/DialpadFragment.java
@@ -21,6 +21,7 @@
 import com.android.contacts.SpecialCharSequenceMgr;
 import com.android.contacts.activities.DialtactsActivity;
 import com.android.contacts.activities.DialtactsActivity.ViewPagerVisibilityListener;
+import com.android.contacts.util.PhoneNumberFormatter;
 import com.android.internal.telephony.ITelephony;
 import com.android.phone.CallLogAsync;
 import com.android.phone.HapticFeedback;
@@ -150,33 +151,6 @@
 
     private String mCurrentCountryIso;
 
-    /**
-     * May be null for a moment and filled by AsyncTask. Must not be touched outside UI thread.
-     */
-    private PhoneNumberFormattingTextWatcher mTextWatcher;
-
-    /**
-     * Delays {@link PhoneNumberFormattingTextWatcher} creation as it may cause disk read operation.
-     */
-    private final AsyncTask<Void, Void, Void> mTextWatcherLoadAsyncTask =
-            new AsyncTask<Void, Void, Void>() {
-
-        private PhoneNumberFormattingTextWatcher mTemporaryWatcher;
-
-        @Override
-        protected Void doInBackground(Void... params) {
-            mTemporaryWatcher = new PhoneNumberFormattingTextWatcher(mCurrentCountryIso);
-            return null;
-        }
-
-        @Override
-        protected void onPostExecute(Void result) {
-            // Should be in UI thread.
-            mTextWatcher = mTemporaryWatcher;
-            mDigits.addTextChangedListener(mTextWatcher);
-        }
-    };
-
     private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
         /**
          * Listen for phone state changes so that we can take down the
@@ -261,14 +235,7 @@
         mDigits.setOnKeyListener(this);
         mDigits.addTextChangedListener(this);
 
-        if (mTextWatcher == null) {
-            if (mTextWatcherLoadAsyncTask.getStatus() == AsyncTask.Status.PENDING) {
-                // Start loading text watcher for phone number, which requires disk read.
-                mTextWatcherLoadAsyncTask.execute();
-            }
-        } else {
-            mDigits.addTextChangedListener(mTextWatcher);
-        }
+        PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(getActivity(), mDigits);
 
         // Soft menu button should appear only when there's no hardware menu button.
         final View overflowMenuButton = fragmentView.findViewById(R.id.overflow_menu);
diff --git a/src/com/android/contacts/editor/TextFieldsEditorView.java b/src/com/android/contacts/editor/TextFieldsEditorView.java
index a448f4e..a4ad53a 100644
--- a/src/com/android/contacts/editor/TextFieldsEditorView.java
+++ b/src/com/android/contacts/editor/TextFieldsEditorView.java
@@ -23,6 +23,7 @@
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.util.NameConverter;
+import com.android.contacts.util.PhoneNumberFormatter;
 
 import android.content.Context;
 import android.content.Entity;
@@ -185,8 +186,7 @@
             int inputType = field.inputType;
             fieldView.setInputType(inputType);
             if (inputType == InputType.TYPE_CLASS_PHONE) {
-                fieldView.addTextChangedListener(new PhoneNumberFormattingTextWatcher(
-                        ContactsUtils.getCurrentCountryIso(mContext)));
+                PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(mContext, fieldView);
             }
             fieldView.setMinLines(field.minLines);
 
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index 65af3ee..5cd500f 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -45,6 +45,7 @@
 import android.provider.ContactsContract;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.TimingLogger;
 
 import java.util.Collection;
 import java.util.Collections;
@@ -275,7 +276,9 @@
         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
             Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start");
         }
-        long startTime = SystemClock.currentThreadTimeMillis();
+        TimingLogger timings = new TimingLogger(TAG, "loadAccountsInBackground");
+        final long startTime = SystemClock.currentThreadTimeMillis();
+        final long startTimeWall = SystemClock.elapsedRealtime();
 
         // Account types, keyed off the account type and data set concatenation.
         Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet = Maps.newHashMap();
@@ -371,6 +374,7 @@
         } catch (RemoteException e) {
             Log.w(TAG, "Problem loading accounts: " + e.toString());
         }
+        timings.addSplit("Loaded account types");
 
         // Map in accounts to associate the account names with each account type entry.
         Account[] accounts = mAccountManager.getAccounts();
@@ -402,11 +406,7 @@
         Collections.sort(allAccounts, ACCOUNT_COMPARATOR);
         Collections.sort(writableAccounts, ACCOUNT_COMPARATOR);
 
-        // The UI will need a phone number formatter.  We can preload meta data for the
-        // current locale to prevent a delay later on.
-        PhoneNumberUtil.getInstance().getAsYouTypeFormatter(Locale.getDefault().getCountry());
-
-        long endTime = SystemClock.currentThreadTimeMillis();
+        timings.addSplit("Loaded accounts");
 
         synchronized (this) {
             mAccountTypesWithDataSets = accountTypesByTypeAndDataSet;
@@ -416,8 +416,13 @@
                     mContext, allAccounts, accountTypesByTypeAndDataSet);
         }
 
+        timings.dumpToLog();
+        final long endTimeWall = SystemClock.elapsedRealtime();
+        final long endTime = SystemClock.currentThreadTimeMillis();
+
         Log.i(TAG, "Loaded meta-data for " + mAccountTypesWithDataSets.size() + " account types, "
-                + mAccounts.size() + " accounts in " + (endTime - startTime) + "ms");
+                + mAccounts.size() + " accounts in " + (endTimeWall - startTimeWall) + "ms(wall) "
+                + (endTime - startTime) + "ms(cpu)");
 
         if (mInitializationLatch != null) {
             mInitializationLatch.countDown();
diff --git a/src/com/android/contacts/util/PhoneNumberFormatter.java b/src/com/android/contacts/util/PhoneNumberFormatter.java
new file mode 100644
index 0000000..6e63aac
--- /dev/null
+++ b/src/com/android/contacts/util/PhoneNumberFormatter.java
@@ -0,0 +1,75 @@
+/*
+ * 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.contacts.util;
+
+import com.android.contacts.ContactsUtils;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.telephony.PhoneNumberFormattingTextWatcher;
+import android.widget.TextView;
+
+public final class PhoneNumberFormatter {
+    private PhoneNumberFormatter() {}
+
+    /**
+     * Load {@link TextWatcherLoadAsyncTask} in a worker thread and set it to a {@link TextView}.
+     */
+    private static class TextWatcherLoadAsyncTask extends
+            AsyncTask<Void, Void, PhoneNumberFormattingTextWatcher> {
+        private final String mCountryCode;
+        private final TextView mTextView;
+
+        public TextWatcherLoadAsyncTask(String countryCode, TextView textView) {
+            mCountryCode = countryCode;
+            mTextView = textView;
+        }
+
+        @Override
+        protected PhoneNumberFormattingTextWatcher doInBackground(Void... params) {
+            return new PhoneNumberFormattingTextWatcher(mCountryCode);
+        }
+
+        @Override
+        protected void onPostExecute(PhoneNumberFormattingTextWatcher watcher) {
+            if (watcher == null || isCancelled()) {
+                return; // May happen if we cancel the task.
+            }
+            if (mTextView.getHandler() == null) {
+                return; // View is already detached.
+            }
+            mTextView.addTextChangedListener(watcher);
+
+            // Note changes the user made before onPostExecute() will not be formatted, but
+            // once they type the next letter we format the entire text, so it's not a big deal.
+            // (And loading PhoneNumberFormattingTextWatcher is usually fast enough.)
+            // We could use watcher.afterTextChanged(mTextView.getEditableText()) to force format
+            // the existing content here, but that could cause unwanted results.
+            // (e.g. the contact editor thinks the user changed the content, and would save
+            // when closed even when the user didn't make other changes.)
+        }
+    }
+
+    /**
+     * Delay-set {@link PhoneNumberFormattingTextWatcher} to a {@link TextView}.
+     */
+    public static final void setPhoneNumberFormattingTextWatcher(Context context,
+            TextView textView) {
+        new TextWatcherLoadAsyncTask(ContactsUtils.getCurrentCountryIso(context), textView)
+                .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
+    }
+}