Blanket copy of PhoneApp to services/Telephony.

First phase of splitting out InCallUI from PhoneApp.

Change-Id: I237341c4ff00e96c677caa4580b251ef3432931b
diff --git a/src/com/android/phone/CallerInfoCache.java b/src/com/android/phone/CallerInfoCache.java
new file mode 100644
index 0000000..76f79af
--- /dev/null
+++ b/src/com/android/phone/CallerInfoCache.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2012 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.phone;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.provider.ContactsContract.CommonDataKinds.Callable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Data;
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+/**
+ * Holds "custom ringtone" and "send to voicemail" information for each contact as a fallback of
+ * contacts database. The cached information is refreshed periodically and used when database
+ * lookup (via ContentResolver) takes longer time than expected.
+ *
+ * The data inside this class shouldn't be treated as "primary"; they may not reflect the
+ * latest information stored in the original database.
+ */
+public class CallerInfoCache {
+    private static final String LOG_TAG = CallerInfoCache.class.getSimpleName();
+    private static final boolean DBG =
+            (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+
+    /** This must not be set to true when submitting changes. */
+    private static final boolean VDBG = false;
+
+    /**
+     * Interval used with {@link AlarmManager#setInexactRepeating(int, long, long, PendingIntent)},
+     * which means the actually interval may not be very accurate.
+     */
+    private static final int CACHE_REFRESH_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours in millis.
+
+    public static final int MESSAGE_UPDATE_CACHE = 0;
+
+    // Assuming DATA.DATA1 corresponds to Phone.NUMBER and SipAddress.ADDRESS, we just use
+    // Data columns as much as we can. One exception: because normalized numbers won't be used in
+    // SIP cases, Phone.NORMALIZED_NUMBER is used as is instead of using Data.
+    private static final String[] PROJECTION = new String[] {
+        Data.DATA1,                  // 0
+        Phone.NORMALIZED_NUMBER,     // 1
+        Data.CUSTOM_RINGTONE,        // 2
+        Data.SEND_TO_VOICEMAIL       // 3
+    };
+
+    private static final int INDEX_NUMBER            = 0;
+    private static final int INDEX_NORMALIZED_NUMBER = 1;
+    private static final int INDEX_CUSTOM_RINGTONE   = 2;
+    private static final int INDEX_SEND_TO_VOICEMAIL = 3;
+
+    private static final String SELECTION = "("
+            + "(" + Data.CUSTOM_RINGTONE + " IS NOT NULL OR " + Data.SEND_TO_VOICEMAIL + "=1)"
+            + " AND " + Data.DATA1 + " IS NOT NULL)";
+
+    public static class CacheEntry {
+        public final String customRingtone;
+        public final boolean sendToVoicemail;
+        public CacheEntry(String customRingtone, boolean shouldSendToVoicemail) {
+            this.customRingtone = customRingtone;
+            this.sendToVoicemail = shouldSendToVoicemail;
+        }
+
+        @Override
+        public String toString() {
+            return "ringtone: " + customRingtone + ", " + sendToVoicemail;
+        }
+    }
+
+    private class CacheAsyncTask extends AsyncTask<Void, Void, Void> {
+
+        private PowerManager.WakeLock mWakeLock;
+
+        /**
+         * Call {@link PowerManager.WakeLock#acquire} and call {@link AsyncTask#execute(Object...)},
+         * guaranteeing the lock is held during the asynchronous task.
+         */
+        public void acquireWakeLockAndExecute() {
+            // Prepare a separate partial WakeLock than what PhoneApp has so to avoid
+            // unnecessary conflict.
+            PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+            mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG);
+            mWakeLock.acquire();
+            execute();
+        }
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            if (DBG) log("Start refreshing cache.");
+            refreshCacheEntry();
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            if (VDBG) log("CacheAsyncTask#onPostExecute()");
+            super.onPostExecute(result);
+            releaseWakeLock();
+        }
+
+        @Override
+        protected void onCancelled(Void result) {
+            if (VDBG) log("CacheAsyncTask#onCanceled()");
+            super.onCancelled(result);
+            releaseWakeLock();
+        }
+
+        private void releaseWakeLock() {
+            if (mWakeLock != null && mWakeLock.isHeld()) {
+                mWakeLock.release();
+            }
+        }
+    }
+
+    private final Context mContext;
+
+    /**
+     * The mapping from number to CacheEntry.
+     *
+     * The number will be:
+     * - last 7 digits of each "normalized phone number when it is for PSTN phone call, or
+     * - a full SIP address for SIP call
+     *
+     * When cache is being refreshed, this whole object will be replaced with a newer object,
+     * instead of updating elements inside the object.  "volatile" is used to make
+     * {@link #getCacheEntry(String)} access to the newer one every time when the object is
+     * being replaced.
+     */
+    private volatile HashMap<String, CacheEntry> mNumberToEntry;
+
+    /**
+     * Used to remember if the previous task is finished or not. Should be set to null when done.
+     */
+    private CacheAsyncTask mCacheAsyncTask;
+
+    public static CallerInfoCache init(Context context) {
+        if (DBG) log("init()");
+        CallerInfoCache cache = new CallerInfoCache(context);
+        // The first cache should be available ASAP.
+        cache.startAsyncCache();
+        cache.setRepeatingCacheUpdateAlarm();
+        return cache;
+    }
+
+    private CallerInfoCache(Context context) {
+        mContext = context;
+        mNumberToEntry = new HashMap<String, CacheEntry>();
+    }
+
+    /* package */ void startAsyncCache() {
+        if (DBG) log("startAsyncCache");
+
+        if (mCacheAsyncTask != null) {
+            Log.w(LOG_TAG, "Previous cache task is remaining.");
+            mCacheAsyncTask.cancel(true);
+        }
+        mCacheAsyncTask = new CacheAsyncTask();
+        mCacheAsyncTask.acquireWakeLockAndExecute();
+    }
+
+    /**
+     * Set up periodic alarm for cache update.
+     */
+    private void setRepeatingCacheUpdateAlarm() {
+        if (DBG) log("setRepeatingCacheUpdateAlarm");
+
+        Intent intent = new Intent(CallerInfoCacheUpdateReceiver.ACTION_UPDATE_CALLER_INFO_CACHE);
+        intent.setClass(mContext, CallerInfoCacheUpdateReceiver.class);
+        PendingIntent pendingIntent =
+                PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+        AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+        // We don't need precise timer while this should be power efficient.
+        alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
+                SystemClock.uptimeMillis() + CACHE_REFRESH_INTERVAL,
+                CACHE_REFRESH_INTERVAL, pendingIntent);
+    }
+
+    private void refreshCacheEntry() {
+        if (VDBG) log("refreshCacheEntry() started");
+
+        // There's no way to know which part of the database was updated. Also we don't want
+        // to block incoming calls asking for the cache. So this method just does full query
+        // and replaces the older cache with newer one. To refrain from blocking incoming calls,
+        // it keeps older one as much as it can, and replaces it with newer one inside a very small
+        // synchronized block.
+
+        Cursor cursor = null;
+        try {
+            cursor = mContext.getContentResolver().query(Callable.CONTENT_URI,
+                    PROJECTION, SELECTION, null, null);
+            if (cursor != null) {
+                // We don't want to block real in-coming call, so prepare a completely fresh
+                // cache here again, and replace it with older one.
+                final HashMap<String, CacheEntry> newNumberToEntry =
+                        new HashMap<String, CacheEntry>(cursor.getCount());
+
+                while (cursor.moveToNext()) {
+                    final String number = cursor.getString(INDEX_NUMBER);
+                    String normalizedNumber = cursor.getString(INDEX_NORMALIZED_NUMBER);
+                    if (normalizedNumber == null) {
+                        // There's no guarantee normalized numbers are available every time and
+                        // it may become null sometimes. Try formatting the original number.
+                        normalizedNumber = PhoneNumberUtils.normalizeNumber(number);
+                    }
+                    final String customRingtone = cursor.getString(INDEX_CUSTOM_RINGTONE);
+                    final boolean sendToVoicemail = cursor.getInt(INDEX_SEND_TO_VOICEMAIL) == 1;
+
+                    if (PhoneNumberUtils.isUriNumber(number)) {
+                        // SIP address case
+                        putNewEntryWhenAppropriate(
+                                newNumberToEntry, number, customRingtone, sendToVoicemail);
+                    } else {
+                        // PSTN number case
+                        // Each normalized number may or may not have full content of the number.
+                        // Contacts database may contain +15001234567 while a dialed number may be
+                        // just 5001234567. Also we may have inappropriate country
+                        // code in some cases (e.g. when the location of the device is inconsistent
+                        // with the device's place). So to avoid confusion we just rely on the last
+                        // 7 digits here. It may cause some kind of wrong behavior, which is
+                        // unavoidable anyway in very rare cases..
+                        final int length = normalizedNumber.length();
+                        final String key = length > 7
+                                ? normalizedNumber.substring(length - 7, length)
+                                        : normalizedNumber;
+                        putNewEntryWhenAppropriate(
+                                newNumberToEntry, key, customRingtone, sendToVoicemail);
+                    }
+                }
+
+                if (VDBG) {
+                    Log.d(LOG_TAG, "New cache size: " + newNumberToEntry.size());
+                    for (Entry<String, CacheEntry> entry : newNumberToEntry.entrySet()) {
+                        Log.d(LOG_TAG, "Number: " + entry.getKey() + " -> " + entry.getValue());
+                    }
+                }
+
+                mNumberToEntry = newNumberToEntry;
+
+                if (DBG) {
+                    log("Caching entries are done. Total: " + newNumberToEntry.size());
+                }
+            } else {
+                // Let's just wait for the next refresh..
+                //
+                // If the cursor became null at that exact moment, probably we don't want to
+                // drop old cache. Also the case is fairly rare in usual cases unless acore being
+                // killed, so we don't take care much of this case.
+                Log.w(LOG_TAG, "cursor is null");
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        if (VDBG) log("refreshCacheEntry() ended");
+    }
+
+    private void putNewEntryWhenAppropriate(HashMap<String, CacheEntry> newNumberToEntry,
+            String numberOrSipAddress, String customRingtone, boolean sendToVoicemail) {
+        if (newNumberToEntry.containsKey(numberOrSipAddress)) {
+            // There may be duplicate entries here and we should prioritize
+            // "send-to-voicemail" flag in any case.
+            final CacheEntry entry = newNumberToEntry.get(numberOrSipAddress);
+            if (!entry.sendToVoicemail && sendToVoicemail) {
+                newNumberToEntry.put(numberOrSipAddress,
+                        new CacheEntry(customRingtone, sendToVoicemail));
+            }
+        } else {
+            newNumberToEntry.put(numberOrSipAddress,
+                    new CacheEntry(customRingtone, sendToVoicemail));
+        }
+    }
+
+    /**
+     * Returns CacheEntry for the given number (PSTN number or SIP address).
+     *
+     * @param number OK to be unformatted.
+     * @return CacheEntry to be used. Maybe null if there's no cache here. Note that this may
+     * return null when the cache itself is not ready. BE CAREFUL. (or might be better to throw
+     * an exception)
+     */
+    public CacheEntry getCacheEntry(String number) {
+        if (mNumberToEntry == null) {
+            // Very unusual state. This implies the cache isn't ready during the request, while
+            // it should be prepared on the boot time (i.e. a way before even the first request).
+            Log.w(LOG_TAG, "Fallback cache isn't ready.");
+            return null;
+        }
+
+        CacheEntry entry;
+        if (PhoneNumberUtils.isUriNumber(number)) {
+            if (VDBG) log("Trying to lookup " + number);
+
+            entry = mNumberToEntry.get(number);
+        } else {
+            final String normalizedNumber = PhoneNumberUtils.normalizeNumber(number);
+            final int length = normalizedNumber.length();
+            final String key =
+                    (length > 7 ? normalizedNumber.substring(length - 7, length)
+                            : normalizedNumber);
+            if (VDBG) log("Trying to lookup " + key);
+
+            entry = mNumberToEntry.get(key);
+        }
+        if (VDBG) log("Obtained " + entry);
+        return entry;
+    }
+
+    private static void log(String msg) {
+        Log.d(LOG_TAG, msg);
+    }
+}