From 09fba0a6d82a7f88ce3c8cbb74eddd4bb96eb7e4 Mon Sep 17 00:00:00 2001 From: Daniel Lehmann Date: Thu, 5 May 2011 18:25:17 -0700 Subject: Several renames for consistency. Should help with fragmentizing afterwards TwelveKeyDialer ==> DialpadActivity RecentCallsListActivity ==> CallLogActivity (be consistent with tab name) Renamed xml files to have a clearer connection to the object that they represent Change-Id: Ie4f123dc03a445596e5ecc216adfef61275dbea3 --- AndroidManifest.xml | 14 +- res/layout-land/dialpad_activity.xml | 57 + res/layout-land/twelve_key_dialer.xml | 57 - res/layout-long-land/dialpad_activity.xml | 57 + res/layout-long-land/twelve_key_dialer.xml | 57 - res/layout-long/dialpad_activity.xml | 64 + res/layout-long/twelve_key_dialer.xml | 64 - res/layout/call_log_activity.xml | 34 + res/layout/call_log_list_child_item.xml | 38 + res/layout/call_log_list_group_item.xml | 125 ++ res/layout/call_log_list_item.xml | 37 + res/layout/call_log_list_item_layout.xml | 93 ++ res/layout/dialer_activity.xml | 39 - res/layout/dialpad_activity.xml | 65 + res/layout/dialtacts_activity.xml | 39 + res/layout/recent_calls.xml | 34 - res/layout/recent_calls_list_child_item.xml | 38 - res/layout/recent_calls_list_group_item.xml | 125 -- res/layout/recent_calls_list_item.xml | 37 - res/layout/recent_calls_list_item_layout.xml | 93 -- res/layout/twelve_key_dialer.xml | 65 - src/com/android/contacts/DialtactsActivity.java | 374 ------ .../android/contacts/RecentCallsListActivity.java | 1283 ------------------- .../android/contacts/SpecialCharSequenceMgr.java | 2 +- src/com/android/contacts/TwelveKeyDialer.java | 1280 ------------------- .../contacts/activities/CallLogActivity.java | 1287 ++++++++++++++++++++ .../contacts/activities/ContactsFrontDoor.java | 1 - .../contacts/activities/DialpadActivity.java | 1284 +++++++++++++++++++ .../contacts/activities/DialtactsActivity.java | 376 ++++++ .../android/contacts/DialerLaunchPerformance.java | 2 +- .../contacts/RecentCallsListActivityTests.java | 358 ------ .../contacts/activities/CallLogActivityTests.java | 355 ++++++ 32 files changed, 3922 insertions(+), 3912 deletions(-) create mode 100644 res/layout-land/dialpad_activity.xml delete mode 100644 res/layout-land/twelve_key_dialer.xml create mode 100644 res/layout-long-land/dialpad_activity.xml delete mode 100644 res/layout-long-land/twelve_key_dialer.xml create mode 100644 res/layout-long/dialpad_activity.xml delete mode 100644 res/layout-long/twelve_key_dialer.xml create mode 100644 res/layout/call_log_activity.xml create mode 100644 res/layout/call_log_list_child_item.xml create mode 100644 res/layout/call_log_list_group_item.xml create mode 100644 res/layout/call_log_list_item.xml create mode 100644 res/layout/call_log_list_item_layout.xml delete mode 100644 res/layout/dialer_activity.xml create mode 100644 res/layout/dialpad_activity.xml create mode 100644 res/layout/dialtacts_activity.xml delete mode 100644 res/layout/recent_calls.xml delete mode 100644 res/layout/recent_calls_list_child_item.xml delete mode 100644 res/layout/recent_calls_list_group_item.xml delete mode 100644 res/layout/recent_calls_list_item.xml delete mode 100644 res/layout/recent_calls_list_item_layout.xml delete mode 100644 res/layout/twelve_key_dialer.xml delete mode 100644 src/com/android/contacts/DialtactsActivity.java delete mode 100644 src/com/android/contacts/RecentCallsListActivity.java delete mode 100644 src/com/android/contacts/TwelveKeyDialer.java create mode 100644 src/com/android/contacts/activities/CallLogActivity.java create mode 100644 src/com/android/contacts/activities/DialpadActivity.java create mode 100644 src/com/android/contacts/activities/DialtactsActivity.java delete mode 100644 tests/src/com/android/contacts/RecentCallsListActivityTests.java create mode 100644 tests/src/com/android/contacts/activities/CallLogActivityTests.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 458553e01..d3a9b63fa 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -45,9 +45,11 @@ android:hardwareAccelerated="true" > - - + @@ -56,9 +58,11 @@ - - + @@ -119,7 +123,7 @@ - + + + + + + + + + + + + + + + diff --git a/res/layout-land/twelve_key_dialer.xml b/res/layout-land/twelve_key_dialer.xml deleted file mode 100644 index 985d04743..000000000 --- a/res/layout-land/twelve_key_dialer.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/res/layout-long-land/dialpad_activity.xml b/res/layout-long-land/dialpad_activity.xml new file mode 100644 index 000000000..1cf96903c --- /dev/null +++ b/res/layout-long-land/dialpad_activity.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + diff --git a/res/layout-long-land/twelve_key_dialer.xml b/res/layout-long-land/twelve_key_dialer.xml deleted file mode 100644 index 1cf96903c..000000000 --- a/res/layout-long-land/twelve_key_dialer.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/res/layout-long/dialpad_activity.xml b/res/layout-long/dialpad_activity.xml new file mode 100644 index 000000000..25806251f --- /dev/null +++ b/res/layout-long/dialpad_activity.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout-long/twelve_key_dialer.xml b/res/layout-long/twelve_key_dialer.xml deleted file mode 100644 index 25806251f..000000000 --- a/res/layout-long/twelve_key_dialer.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/res/layout/call_log_activity.xml b/res/layout/call_log_activity.xml new file mode 100644 index 000000000..f054b7033 --- /dev/null +++ b/res/layout/call_log_activity.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/res/layout/call_log_list_child_item.xml b/res/layout/call_log_list_child_item.xml new file mode 100644 index 000000000..bb48fcf3b --- /dev/null +++ b/res/layout/call_log_list_child_item.xml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/res/layout/call_log_list_group_item.xml b/res/layout/call_log_list_group_item.xml new file mode 100644 index 000000000..79423fa86 --- /dev/null +++ b/res/layout/call_log_list_group_item.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/call_log_list_item.xml b/res/layout/call_log_list_item.xml new file mode 100644 index 000000000..4ea0563a0 --- /dev/null +++ b/res/layout/call_log_list_item.xml @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/res/layout/call_log_list_item_layout.xml b/res/layout/call_log_list_item_layout.xml new file mode 100644 index 000000000..faaa893dd --- /dev/null +++ b/res/layout/call_log_list_item_layout.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + diff --git a/res/layout/dialer_activity.xml b/res/layout/dialer_activity.xml deleted file mode 100644 index 14a6b39e6..000000000 --- a/res/layout/dialer_activity.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - diff --git a/res/layout/dialpad_activity.xml b/res/layout/dialpad_activity.xml new file mode 100644 index 000000000..2cae9aee2 --- /dev/null +++ b/res/layout/dialpad_activity.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/dialtacts_activity.xml b/res/layout/dialtacts_activity.xml new file mode 100644 index 000000000..14a6b39e6 --- /dev/null +++ b/res/layout/dialtacts_activity.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/res/layout/recent_calls.xml b/res/layout/recent_calls.xml deleted file mode 100644 index f054b7033..000000000 --- a/res/layout/recent_calls.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - diff --git a/res/layout/recent_calls_list_child_item.xml b/res/layout/recent_calls_list_child_item.xml deleted file mode 100644 index 527e25904..000000000 --- a/res/layout/recent_calls_list_child_item.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - diff --git a/res/layout/recent_calls_list_group_item.xml b/res/layout/recent_calls_list_group_item.xml deleted file mode 100644 index 79423fa86..000000000 --- a/res/layout/recent_calls_list_group_item.xml +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/res/layout/recent_calls_list_item.xml b/res/layout/recent_calls_list_item.xml deleted file mode 100644 index 2c519d6b6..000000000 --- a/res/layout/recent_calls_list_item.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - diff --git a/res/layout/recent_calls_list_item_layout.xml b/res/layout/recent_calls_list_item_layout.xml deleted file mode 100644 index faaa893dd..000000000 --- a/res/layout/recent_calls_list_item_layout.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/res/layout/twelve_key_dialer.xml b/res/layout/twelve_key_dialer.xml deleted file mode 100644 index 2cae9aee2..000000000 --- a/res/layout/twelve_key_dialer.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/com/android/contacts/DialtactsActivity.java b/src/com/android/contacts/DialtactsActivity.java deleted file mode 100644 index 5ad57e40d..000000000 --- a/src/com/android/contacts/DialtactsActivity.java +++ /dev/null @@ -1,374 +0,0 @@ -/* - * Copyright (C) 2008 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; - -import com.android.contacts.activities.ContactsFrontDoor; -import com.android.contacts.activities.ContactBrowserActivity; -import com.android.internal.telephony.ITelephony; - -import android.app.Activity; -import android.app.TabActivity; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Bundle; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.provider.CallLog.Calls; -import android.provider.ContactsContract.Intents.UI; -import android.util.Log; -import android.view.Window; -import android.widget.TabHost; - -/** - * The dialer activity that has one tab with the virtual 12key - * dialer, a tab with recent calls in it, a tab with the contacts and - * a tab with the favorite. This is the container and the tabs are - * embedded using intents. - * The dialer tab's title is 'phone', a more common name (see strings.xml). - */ -public class DialtactsActivity extends TabActivity implements TabHost.OnTabChangeListener { - private static final String TAG = "Dailtacts"; - - private static final int TAB_INDEX_DIALER = 0; - private static final int TAB_INDEX_CALL_LOG = 1; - private static final int TAB_INDEX_CONTACTS = 2; - private static final int TAB_INDEX_FAVORITES = 3; - - static final String EXTRA_IGNORE_STATE = "ignore-state"; - - /** Name of the dialtacts shared preferences */ - static final String PREFS_DIALTACTS = "dialtacts"; - /** If true, when handling the contacts intent the favorites tab will be shown instead */ - static final String PREF_FAVORITES_AS_CONTACTS = "favorites_as_contacts"; - static final boolean PREF_FAVORITES_AS_CONTACTS_DEFAULT = false; - - /** Last manually selected tab index */ - private static final String PREF_LAST_MANUALLY_SELECTED_TAB = "last_manually_selected_tab"; - private static final int PREF_LAST_MANUALLY_SELECTED_TAB_DEFAULT = TAB_INDEX_DIALER; - - private TabHost mTabHost; - private String mFilterText; - private Uri mDialUri; - - /** - * The index of the tab that has last been manually selected (the user clicked on a tab). - * This value does not keep track of programmatically set Tabs (e.g. Call Log after a Call) - */ - private int mLastManuallySelectedTab; - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - final Intent intent = getIntent(); - fixIntent(intent); - - requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(R.layout.dialer_activity); - - mTabHost = getTabHost(); - mTabHost.setOnTabChangedListener(this); - - // Setup the tabs - setupDialerTab(); - setupCallLogTab(); - setupContactsTab(); - setupFavoritesTab(); - - // Load the last manually loaded tab - final SharedPreferences prefs = getSharedPreferences(PREFS_DIALTACTS, MODE_PRIVATE); - mLastManuallySelectedTab = prefs.getInt(PREF_LAST_MANUALLY_SELECTED_TAB, - PREF_LAST_MANUALLY_SELECTED_TAB_DEFAULT); - - setCurrentTab(intent); - - if (UI.FILTER_CONTACTS_ACTION.equals(intent.getAction()) - && icicle == null) { - setupFilterText(intent); - } - } - - @Override - protected void onPause() { - super.onPause(); - - final int currentTabIndex = mTabHost.getCurrentTab(); - final SharedPreferences.Editor editor = - getSharedPreferences(PREFS_DIALTACTS, MODE_PRIVATE).edit(); - if (currentTabIndex == TAB_INDEX_CONTACTS || currentTabIndex == TAB_INDEX_FAVORITES) { - editor.putBoolean(PREF_FAVORITES_AS_CONTACTS, currentTabIndex == TAB_INDEX_FAVORITES); - } - editor.putInt(PREF_LAST_MANUALLY_SELECTED_TAB, mLastManuallySelectedTab); - - editor.apply(); - } - - private void fixIntent(Intent intent) { - // This should be cleaned up: the call key used to send an Intent - // that just said to go to the recent calls list. It now sends this - // abstract action, but this class hasn't been rewritten to deal with it. - if (Intent.ACTION_CALL_BUTTON.equals(intent.getAction())) { - intent.setDataAndType(Calls.CONTENT_URI, Calls.CONTENT_TYPE); - intent.putExtra("call_key", true); - setIntent(intent); - } - } - - private void setupCallLogTab() { - // Force the class since overriding tab entries doesn't work - Intent intent = new Intent("com.android.phone.action.RECENT_CALLS"); - intent.setClass(this, RecentCallsListActivity.class); - - mTabHost.addTab(mTabHost.newTabSpec("call_log") - .setIndicator(getString(R.string.recentCallsIconLabel), - getResources().getDrawable(R.drawable.ic_tab_recent)) - .setContent(intent)); - } - - private void setupDialerTab() { - Intent intent = new Intent("com.android.phone.action.TOUCH_DIALER"); - intent.setClass(this, TwelveKeyDialer.class); - - mTabHost.addTab(mTabHost.newTabSpec("dialer") - .setIndicator(getString(R.string.dialerIconLabel), - getResources().getDrawable(R.drawable.ic_tab_dialer)) - .setContent(intent)); - } - - private void setupContactsTab() { - Intent intent = new Intent(UI.LIST_ALL_CONTACTS_ACTION); - intent.setClass(this, ContactBrowserActivity.class); - - mTabHost.addTab(mTabHost.newTabSpec("contacts") - .setIndicator(getText(R.string.contactsIconLabel), - getResources().getDrawable(R.drawable.ic_tab_contacts)) - .setContent(intent)); - } - - private void setupFavoritesTab() { - Intent intent = new Intent(UI.LIST_STREQUENT_ACTION); - intent.setClass(this, ContactBrowserActivity.class); - - mTabHost.addTab(mTabHost.newTabSpec("favorites") - .setIndicator(getString(R.string.contactsFavoritesLabel), - getResources().getDrawable(R.drawable.ic_tab_starred)) - .setContent(intent)); - } - - /** - * Returns true if the intent is due to hitting the green send key while in a call. - * - * @param intent the intent that launched this activity - * @param recentCallsRequest true if the intent is requesting to view recent calls - * @return true if the intent is due to hitting the green send key while in a call - */ - private boolean isSendKeyWhileInCall(final Intent intent, final boolean recentCallsRequest) { - // If there is a call in progress go to the call screen - if (recentCallsRequest) { - final boolean callKey = intent.getBooleanExtra("call_key", false); - - try { - ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); - if (callKey && phone != null && phone.showCallScreen()) { - return true; - } - } catch (RemoteException e) { - Log.e(TAG, "Failed to handle send while in call", e); - } - } - - return false; - } - - /** - * Sets the current tab based on the intent's request type - * - * @param intent Intent that contains information about which tab should be selected - */ - private void setCurrentTab(Intent intent) { - // If we got here by hitting send and we're in call forward along to the in-call activity - final boolean recentCallsRequest = Calls.CONTENT_TYPE.equals(intent.getType()); - if (isSendKeyWhileInCall(intent, recentCallsRequest)) { - finish(); - return; - } - - // Dismiss menu provided by any children activities - Activity activity = getLocalActivityManager(). - getActivity(mTabHost.getCurrentTabTag()); - if (activity != null) { - activity.closeOptionsMenu(); - } - - // Tell the children activities that they should ignore any possible saved - // state and instead reload their state from the parent's intent - intent.putExtra(EXTRA_IGNORE_STATE, true); - - // Remember the old manually selected tab index so that it can be restored if it is - // overwritten by one of the programmatic tab selections - final int savedTabIndex = mLastManuallySelectedTab; - - // Choose the tab based on the inbound intent - if (intent.getBooleanExtra(ContactsFrontDoor.EXTRA_FRONT_DOOR, false)) { - // Launched through the contacts front door, set the proper contacts tab (sticky - // between favorites and contacts) - SharedPreferences prefs = getSharedPreferences(PREFS_DIALTACTS, MODE_PRIVATE); - boolean favoritesAsContacts = prefs.getBoolean(PREF_FAVORITES_AS_CONTACTS, - PREF_FAVORITES_AS_CONTACTS_DEFAULT); - if (favoritesAsContacts) { - mTabHost.setCurrentTab(TAB_INDEX_FAVORITES); - } else { - mTabHost.setCurrentTab(TAB_INDEX_CONTACTS); - } - } else { - // Not launched through the front door, look at the component to determine the tab - String componentName = intent.getComponent().getClassName(); - if (getClass().getName().equals(componentName)) { - if (recentCallsRequest) { - mTabHost.setCurrentTab(TAB_INDEX_CALL_LOG); - } else { - mTabHost.setCurrentTab(TAB_INDEX_DIALER); - } - } else { - mTabHost.setCurrentTab(mLastManuallySelectedTab); - } - } - - // Restore to the previous manual selection - mLastManuallySelectedTab = savedTabIndex; - - // Tell the children activities that they should honor their saved states - // instead of the state from the parent's intent - intent.putExtra(EXTRA_IGNORE_STATE, false); - } - - @Override - public void onNewIntent(Intent newIntent) { - setIntent(newIntent); - fixIntent(newIntent); - setCurrentTab(newIntent); - final String action = newIntent.getAction(); - if (UI.FILTER_CONTACTS_ACTION.equals(action)) { - setupFilterText(newIntent); - } else if (isDialIntent(newIntent)) { - setupDialUri(newIntent); - } - } - - /** Returns true if the given intent contains a phone number to populate the dialer with */ - private boolean isDialIntent(Intent intent) { - final String action = intent.getAction(); - if (Intent.ACTION_DIAL.equals(action)) { - return true; - } - if (Intent.ACTION_VIEW.equals(action)) { - final Uri data = intent.getData(); - if (data != null && "tel".equals(data.getScheme())) { - return true; - } - } - return false; - } - - /** - * Retrieves the filter text stored in {@link #setupFilterText(Intent)}. - * This text originally came from a FILTER_CONTACTS_ACTION intent received - * by this activity. The stored text will then be cleared after after this - * method returns. - * - * @return The stored filter text - */ - public String getAndClearFilterText() { - String filterText = mFilterText; - mFilterText = null; - return filterText; - } - - /** - * Stores the filter text associated with a FILTER_CONTACTS_ACTION intent. - * This is so child activities can check if they are supposed to display a filter. - * - * @param intent The intent received in {@link #onNewIntent(Intent)} - */ - private void setupFilterText(Intent intent) { - // If the intent was relaunched from history, don't apply the filter text. - if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { - return; - } - String filter = intent.getStringExtra(UI.FILTER_TEXT_EXTRA_KEY); - if (filter != null && filter.length() > 0) { - mFilterText = filter; - } - } - - /** - * Retrieves the uri stored in {@link #setupDialUri(Intent)}. This uri - * originally came from a dial intent received by this activity. The stored - * uri will then be cleared after after this method returns. - * - * @return The stored uri - */ - public Uri getAndClearDialUri() { - Uri dialUri = mDialUri; - mDialUri = null; - return dialUri; - } - - /** - * Stores the uri associated with a dial intent. This is so child activities can - * check if they are supposed to display new dial info. - * - * @param intent The intent received in {@link #onNewIntent(Intent)} - */ - private void setupDialUri(Intent intent) { - // If the intent was relaunched from history, don't reapply the intent. - if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { - return; - } - mDialUri = intent.getData(); - } - - @Override - public void onBackPressed() { - if (isTaskRoot()) { - // Instead of stopping, simply push this to the back of the stack. - // This is only done when running at the top of the stack; - // otherwise, we have been launched by someone else so need to - // allow the user to go back to the caller. - moveTaskToBack(false); - } else { - super.onBackPressed(); - } - } - - /** {@inheritDoc} */ - public void onTabChanged(String tabId) { - // Because we're using Activities as our tab children, we trigger - // onWindowFocusChanged() to let them know when they're active. This may - // seem to duplicate the purpose of onResume(), but it's needed because - // onResume() can't reliably check if a keyguard is active. - Activity activity = getLocalActivityManager().getActivity(tabId); - if (activity != null) { - activity.onWindowFocusChanged(true); - } - - // Remember this tab index. This function is also called, if the tab is set automatically - // in which case the setter (setCurrentTab) has to set this to its old value afterwards - mLastManuallySelectedTab = mTabHost.getCurrentTab(); - } -} diff --git a/src/com/android/contacts/RecentCallsListActivity.java b/src/com/android/contacts/RecentCallsListActivity.java deleted file mode 100644 index fb4ee10f8..000000000 --- a/src/com/android/contacts/RecentCallsListActivity.java +++ /dev/null @@ -1,1283 +0,0 @@ -/* - * Copyright (C) 2007 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; - -import com.android.common.widget.GroupingListAdapter; -import com.android.internal.telephony.CallerInfo; -import com.android.internal.telephony.ITelephony; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.ListActivity; -import android.app.ProgressDialog; -import android.content.ActivityNotFoundException; -import android.content.AsyncQueryHandler; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.DialogInterface.OnClickListener; -import android.database.CharArrayBuffer; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabaseCorruptException; -import android.database.sqlite.SQLiteDiskIOException; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteFullException; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.provider.CallLog; -import android.provider.CallLog.Calls; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.SipAddress; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.Intents.Insert; -import android.provider.ContactsContract.PhoneLookup; -import android.telephony.PhoneNumberUtils; -import android.telephony.TelephonyManager; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.util.Log; -import android.view.ContextMenu; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.ContextMenu.ContextMenuInfo; -import android.widget.AdapterView; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.TextView; - -import java.lang.ref.WeakReference; -import java.util.HashMap; -import java.util.LinkedList; - -/** - * Displays a list of call log entries. - */ -public class RecentCallsListActivity extends ListActivity - implements View.OnCreateContextMenuListener { - private static final String TAG = "RecentCallsList"; - - /** The projection to use when querying the call log table */ - static final String[] CALL_LOG_PROJECTION = new String[] { - Calls._ID, - Calls.NUMBER, - Calls.DATE, - Calls.DURATION, - Calls.TYPE, - Calls.CACHED_NAME, - Calls.CACHED_NUMBER_TYPE, - Calls.CACHED_NUMBER_LABEL, - Calls.COUNTRY_ISO}; - - static final int ID_COLUMN_INDEX = 0; - static final int NUMBER_COLUMN_INDEX = 1; - static final int DATE_COLUMN_INDEX = 2; - static final int DURATION_COLUMN_INDEX = 3; - static final int CALL_TYPE_COLUMN_INDEX = 4; - static final int CALLER_NAME_COLUMN_INDEX = 5; - static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 6; - static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 7; - static final int COUNTRY_ISO_COLUMN_INDEX = 8; - - /** The projection to use when querying the phones table */ - static final String[] PHONES_PROJECTION = new String[] { - PhoneLookup._ID, - PhoneLookup.DISPLAY_NAME, - PhoneLookup.TYPE, - PhoneLookup.LABEL, - PhoneLookup.NUMBER, - PhoneLookup.NORMALIZED_NUMBER}; - - static final int PERSON_ID_COLUMN_INDEX = 0; - static final int NAME_COLUMN_INDEX = 1; - static final int PHONE_TYPE_COLUMN_INDEX = 2; - static final int LABEL_COLUMN_INDEX = 3; - static final int MATCHED_NUMBER_COLUMN_INDEX = 4; - static final int NORMALIZED_NUMBER_COLUMN_INDEX = 5; - - private static final int MENU_ITEM_DELETE = 1; - private static final int MENU_ITEM_DELETE_ALL = 2; - private static final int MENU_ITEM_VIEW_CONTACTS = 3; - - private static final int QUERY_TOKEN = 53; - private static final int UPDATE_TOKEN = 54; - - private static final int DIALOG_CONFIRM_DELETE_ALL = 1; - - RecentCallsAdapter mAdapter; - private QueryHandler mQueryHandler; - String mVoiceMailNumber; - private String mCurrentCountryIso; - - private boolean mScrollToTop; - - static final class ContactInfo { - public long personId; - public String name; - public int type; - public String label; - public String number; - public String formattedNumber; - public String normalizedNumber; - - public static ContactInfo EMPTY = new ContactInfo(); - } - - public static final class RecentCallsListItemViews { - TextView line1View; - TextView labelView; - TextView numberView; - TextView dateView; - ImageView iconView; - View callView; - ImageView groupIndicator; - TextView groupSize; - } - - static final class CallerInfoQuery { - String number; - int position; - String name; - int numberType; - String numberLabel; - } - - /** Adapter class to fill in data for the Call Log */ - final class RecentCallsAdapter extends GroupingListAdapter - implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener { - HashMap mContactInfo; - private final LinkedList mRequests; - private volatile boolean mDone; - private boolean mLoading = true; - ViewTreeObserver.OnPreDrawListener mPreDrawListener; - private static final int REDRAW = 1; - private static final int START_THREAD = 2; - private boolean mFirst; - private Thread mCallerIdThread; - - private CharSequence[] mLabelArray; - - private Drawable mDrawableIncoming; - private Drawable mDrawableOutgoing; - private Drawable mDrawableMissed; - - /** - * Reusable char array buffers. - */ - private CharArrayBuffer mBuffer1 = new CharArrayBuffer(128); - private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128); - - public void onClick(View view) { - String number = (String) view.getTag(); - if (!TextUtils.isEmpty(number)) { - // Here, "number" can either be a PSTN phone number or a - // SIP address. So turn it into either a tel: URI or a - // sip: URI, as appropriate. - Uri callUri; - if (PhoneNumberUtils.isUriNumber(number)) { - callUri = Uri.fromParts("sip", number, null); - } else { - callUri = Uri.fromParts("tel", number, null); - } - startActivity(new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri)); - } - } - - public boolean onPreDraw() { - if (mFirst) { - mHandler.sendEmptyMessageDelayed(START_THREAD, 1000); - mFirst = false; - } - return true; - } - - private Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case REDRAW: - notifyDataSetChanged(); - break; - case START_THREAD: - startRequestProcessing(); - break; - } - } - }; - - public RecentCallsAdapter() { - super(RecentCallsListActivity.this); - - mContactInfo = new HashMap(); - mRequests = new LinkedList(); - mPreDrawListener = null; - - mDrawableIncoming = getResources().getDrawable( - R.drawable.ic_call_log_list_incoming_call); - mDrawableOutgoing = getResources().getDrawable( - R.drawable.ic_call_log_list_outgoing_call); - mDrawableMissed = getResources().getDrawable( - R.drawable.ic_call_log_list_missed_call); - mLabelArray = getResources().getTextArray(com.android.internal.R.array.phoneTypes); - } - - /** - * Requery on background thread when {@link Cursor} changes. - */ - @Override - protected void onContentChanged() { - // Start async requery - startQuery(); - } - - void setLoading(boolean loading) { - mLoading = loading; - } - - @Override - public boolean isEmpty() { - if (mLoading) { - // We don't want the empty state to show when loading. - return false; - } else { - return super.isEmpty(); - } - } - - public ContactInfo getContactInfo(String number) { - return mContactInfo.get(number); - } - - public void startRequestProcessing() { - mDone = false; - mCallerIdThread = new Thread(this); - mCallerIdThread.setPriority(Thread.MIN_PRIORITY); - mCallerIdThread.start(); - } - - public void stopRequestProcessing() { - mDone = true; - if (mCallerIdThread != null) mCallerIdThread.interrupt(); - } - - public void clearCache() { - synchronized (mContactInfo) { - mContactInfo.clear(); - } - } - - private void updateCallLog(CallerInfoQuery ciq, ContactInfo ci) { - // Check if they are different. If not, don't update. - if (TextUtils.equals(ciq.name, ci.name) - && TextUtils.equals(ciq.numberLabel, ci.label) - && ciq.numberType == ci.type) { - return; - } - ContentValues values = new ContentValues(3); - values.put(Calls.CACHED_NAME, ci.name); - values.put(Calls.CACHED_NUMBER_TYPE, ci.type); - values.put(Calls.CACHED_NUMBER_LABEL, ci.label); - - try { - RecentCallsListActivity.this.getContentResolver().update(Calls.CONTENT_URI, values, - Calls.NUMBER + "='" + ciq.number + "'", null); - } catch (SQLiteDiskIOException e) { - Log.w(TAG, "Exception while updating call info", e); - } catch (SQLiteFullException e) { - Log.w(TAG, "Exception while updating call info", e); - } catch (SQLiteDatabaseCorruptException e) { - Log.w(TAG, "Exception while updating call info", e); - } - } - - private void enqueueRequest(String number, int position, - String name, int numberType, String numberLabel) { - CallerInfoQuery ciq = new CallerInfoQuery(); - ciq.number = number; - ciq.position = position; - ciq.name = name; - ciq.numberType = numberType; - ciq.numberLabel = numberLabel; - synchronized (mRequests) { - mRequests.add(ciq); - mRequests.notifyAll(); - } - } - - private boolean queryContactInfo(CallerInfoQuery ciq) { - // First check if there was a prior request for the same number - // that was already satisfied - ContactInfo info = mContactInfo.get(ciq.number); - boolean needNotify = false; - if (info != null && info != ContactInfo.EMPTY) { - return true; - } else { - // Ok, do a fresh Contacts lookup for ciq.number. - boolean infoUpdated = false; - - if (PhoneNumberUtils.isUriNumber(ciq.number)) { - // This "number" is really a SIP address. - - // TODO: This code is duplicated from the - // CallerInfoAsyncQuery class. To avoid that, could the - // code here just use CallerInfoAsyncQuery, rather than - // manually running ContentResolver.query() itself? - - // We look up SIP addresses directly in the Data table: - Uri contactRef = Data.CONTENT_URI; - - // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent. - // - // Also note we use "upper(data1)" in the WHERE clause, and - // uppercase the incoming SIP address, in order to do a - // case-insensitive match. - // - // TODO: May also need to normalize by adding "sip:" as a - // prefix, if we start storing SIP addresses that way in the - // database. - String selection = "upper(" + Data.DATA1 + ")=?" - + " AND " - + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'"; - String[] selectionArgs = new String[] { ciq.number.toUpperCase() }; - - Cursor dataTableCursor = - RecentCallsListActivity.this.getContentResolver().query( - contactRef, - null, // projection - selection, // selection - selectionArgs, // selectionArgs - null); // sortOrder - - if (dataTableCursor != null) { - if (dataTableCursor.moveToFirst()) { - info = new ContactInfo(); - - // TODO: we could slightly speed this up using an - // explicit projection (and thus not have to do - // those getColumnIndex() calls) but the benefit is - // very minimal. - - // Note the Data.CONTACT_ID column here is - // equivalent to the PERSON_ID_COLUMN_INDEX column - // we use with "phonesCursor" below. - info.personId = dataTableCursor.getLong( - dataTableCursor.getColumnIndex(Data.CONTACT_ID)); - info.name = dataTableCursor.getString( - dataTableCursor.getColumnIndex(Data.DISPLAY_NAME)); - // "type" and "label" are currently unused for SIP addresses - info.type = SipAddress.TYPE_OTHER; - info.label = null; - - // And "number" is the SIP address. - // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent. - info.number = dataTableCursor.getString( - dataTableCursor.getColumnIndex(Data.DATA1)); - info.normalizedNumber = null; // meaningless for SIP addresses - - infoUpdated = true; - } - dataTableCursor.close(); - } - } else { - // "number" is a regular phone number, so use the - // PhoneLookup table: - Cursor phonesCursor = - RecentCallsListActivity.this.getContentResolver().query( - Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, - Uri.encode(ciq.number)), - PHONES_PROJECTION, null, null, null); - if (phonesCursor != null) { - if (phonesCursor.moveToFirst()) { - info = new ContactInfo(); - info.personId = phonesCursor.getLong(PERSON_ID_COLUMN_INDEX); - info.name = phonesCursor.getString(NAME_COLUMN_INDEX); - info.type = phonesCursor.getInt(PHONE_TYPE_COLUMN_INDEX); - info.label = phonesCursor.getString(LABEL_COLUMN_INDEX); - info.number = phonesCursor.getString(MATCHED_NUMBER_COLUMN_INDEX); - info.normalizedNumber = - phonesCursor.getString(NORMALIZED_NUMBER_COLUMN_INDEX); - - infoUpdated = true; - } - phonesCursor.close(); - } - } - - if (infoUpdated) { - // New incoming phone number invalidates our formatted - // cache. Any cache fills happen only on the GUI thread. - info.formattedNumber = null; - - mContactInfo.put(ciq.number, info); - - // Inform list to update this item, if in view - needNotify = true; - } - } - if (info != null) { - updateCallLog(ciq, info); - } - return needNotify; - } - - /* - * Handles requests for contact name and number type - * @see java.lang.Runnable#run() - */ - public void run() { - boolean needNotify = false; - while (!mDone) { - CallerInfoQuery ciq = null; - synchronized (mRequests) { - if (!mRequests.isEmpty()) { - ciq = mRequests.removeFirst(); - } else { - if (needNotify) { - needNotify = false; - mHandler.sendEmptyMessage(REDRAW); - } - try { - mRequests.wait(1000); - } catch (InterruptedException ie) { - // Ignore and continue processing requests - } - } - } - if (ciq != null && queryContactInfo(ciq)) { - needNotify = true; - } - } - } - - @Override - protected void addGroups(Cursor cursor) { - - int count = cursor.getCount(); - if (count == 0) { - return; - } - - int groupItemCount = 1; - - CharArrayBuffer currentValue = mBuffer1; - CharArrayBuffer value = mBuffer2; - cursor.moveToFirst(); - cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, currentValue); - int currentCallType = cursor.getInt(CALL_TYPE_COLUMN_INDEX); - for (int i = 1; i < count; i++) { - cursor.moveToNext(); - cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, value); - boolean sameNumber = equalPhoneNumbers(value, currentValue); - - // Group adjacent calls with the same number. Make an exception - // for the latest item if it was a missed call. We don't want - // a missed call to be hidden inside a group. - if (sameNumber && currentCallType != Calls.MISSED_TYPE) { - groupItemCount++; - } else { - if (groupItemCount > 1) { - addGroup(i - groupItemCount, groupItemCount, false); - } - - groupItemCount = 1; - - // Swap buffers - CharArrayBuffer temp = currentValue; - currentValue = value; - value = temp; - - // If we have just examined a row following a missed call, make - // sure that it is grouped with subsequent calls from the same number - // even if it was also missed. - if (sameNumber && currentCallType == Calls.MISSED_TYPE) { - currentCallType = 0; // "not a missed call" - } else { - currentCallType = cursor.getInt(CALL_TYPE_COLUMN_INDEX); - } - } - } - if (groupItemCount > 1) { - addGroup(count - groupItemCount, groupItemCount, false); - } - } - - protected boolean equalPhoneNumbers(CharArrayBuffer buffer1, CharArrayBuffer buffer2) { - - // TODO add PhoneNumberUtils.compare(CharSequence, CharSequence) to avoid - // string allocation - return PhoneNumberUtils.compare(new String(buffer1.data, 0, buffer1.sizeCopied), - new String(buffer2.data, 0, buffer2.sizeCopied)); - } - - - @Override - protected View newStandAloneView(Context context, ViewGroup parent) { - LayoutInflater inflater = - (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View view = inflater.inflate(R.layout.recent_calls_list_item, parent, false); - findAndCacheViews(view); - return view; - } - - @Override - protected void bindStandAloneView(View view, Context context, Cursor cursor) { - bindView(context, view, cursor); - } - - @Override - protected View newChildView(Context context, ViewGroup parent) { - LayoutInflater inflater = - (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View view = inflater.inflate(R.layout.recent_calls_list_child_item, parent, false); - findAndCacheViews(view); - return view; - } - - @Override - protected void bindChildView(View view, Context context, Cursor cursor) { - bindView(context, view, cursor); - } - - @Override - protected View newGroupView(Context context, ViewGroup parent) { - LayoutInflater inflater = - (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View view = inflater.inflate(R.layout.recent_calls_list_group_item, parent, false); - findAndCacheViews(view); - return view; - } - - @Override - protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize, - boolean expanded) { - final RecentCallsListItemViews views = (RecentCallsListItemViews) view.getTag(); - int groupIndicator = expanded - ? com.android.internal.R.drawable.expander_ic_maximized - : com.android.internal.R.drawable.expander_ic_minimized; - views.groupIndicator.setImageResource(groupIndicator); - views.groupSize.setText("(" + groupSize + ")"); - bindView(context, view, cursor); - } - - private void findAndCacheViews(View view) { - - // Get the views to bind to - RecentCallsListItemViews views = new RecentCallsListItemViews(); - views.line1View = (TextView) view.findViewById(R.id.line1); - views.labelView = (TextView) view.findViewById(R.id.label); - views.numberView = (TextView) view.findViewById(R.id.number); - views.dateView = (TextView) view.findViewById(R.id.date); - views.iconView = (ImageView) view.findViewById(R.id.call_type_icon); - views.callView = view.findViewById(R.id.call_icon); - views.callView.setOnClickListener(this); - views.groupIndicator = (ImageView) view.findViewById(R.id.groupIndicator); - views.groupSize = (TextView) view.findViewById(R.id.groupSize); - view.setTag(views); - } - - public void bindView(Context context, View view, Cursor c) { - final RecentCallsListItemViews views = (RecentCallsListItemViews) view.getTag(); - - String number = c.getString(NUMBER_COLUMN_INDEX); - String formattedNumber = null; - String callerName = c.getString(CALLER_NAME_COLUMN_INDEX); - int callerNumberType = c.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); - String callerNumberLabel = c.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); - String countryIso = c.getString(COUNTRY_ISO_COLUMN_INDEX); - // Store away the number so we can call it directly if you click on the call icon - views.callView.setTag(number); - - // Lookup contacts with this number - ContactInfo info = mContactInfo.get(number); - if (info == null) { - // Mark it as empty and queue up a request to find the name - // The db request should happen on a non-UI thread - info = ContactInfo.EMPTY; - mContactInfo.put(number, info); - enqueueRequest(number, c.getPosition(), - callerName, callerNumberType, callerNumberLabel); - } else if (info != ContactInfo.EMPTY) { // Has been queried - // Check if any data is different from the data cached in the - // calls db. If so, queue the request so that we can update - // the calls db. - if (!TextUtils.equals(info.name, callerName) - || info.type != callerNumberType - || !TextUtils.equals(info.label, callerNumberLabel)) { - // Something is amiss, so sync up. - enqueueRequest(number, c.getPosition(), - callerName, callerNumberType, callerNumberLabel); - } - - // Format and cache phone number for found contact - if (info.formattedNumber == null) { - info.formattedNumber = - formatPhoneNumber(info.number, info.normalizedNumber, countryIso); - } - formattedNumber = info.formattedNumber; - } - - String name = info.name; - int ntype = info.type; - String label = info.label; - // If there's no name cached in our hashmap, but there's one in the - // calls db, use the one in the calls db. Otherwise the name in our - // hashmap is more recent, so it has precedence. - if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(callerName)) { - name = callerName; - ntype = callerNumberType; - label = callerNumberLabel; - - // Format the cached call_log phone number - formattedNumber = formatPhoneNumber(number, null, countryIso); - } - // Set the text lines and call icon. - // Assumes the call back feature is on most of the - // time. For private and unknown numbers: hide it. - views.callView.setVisibility(View.VISIBLE); - - if (!TextUtils.isEmpty(name)) { - views.line1View.setText(name); - views.labelView.setVisibility(View.VISIBLE); - - // "type" and "label" are currently unused for SIP addresses. - CharSequence numberLabel = null; - if (!PhoneNumberUtils.isUriNumber(number)) { - numberLabel = Phone.getDisplayLabel(context, ntype, label, - mLabelArray); - } - views.numberView.setVisibility(View.VISIBLE); - views.numberView.setText(formattedNumber); - if (!TextUtils.isEmpty(numberLabel)) { - views.labelView.setText(numberLabel); - views.labelView.setVisibility(View.VISIBLE); - - // Zero out the numberView's left margin (see below) - ViewGroup.MarginLayoutParams numberLP = - (ViewGroup.MarginLayoutParams) views.numberView.getLayoutParams(); - numberLP.leftMargin = 0; - views.numberView.setLayoutParams(numberLP); - } else { - // There's nothing to display in views.labelView, so hide it. - // We can't set it to View.GONE, since it's the anchor for - // numberView in the RelativeLayout, so make it INVISIBLE. - // Also, we need to manually *subtract* some left margin from - // numberView to compensate for the right margin built in to - // labelView (otherwise the number will be indented by a very - // slight amount). - // TODO: a cleaner fix would be to contain both the label and - // number inside a LinearLayout, and then set labelView *and* - // its padding to GONE when there's no label to display. - views.labelView.setText(null); - views.labelView.setVisibility(View.INVISIBLE); - - ViewGroup.MarginLayoutParams labelLP = - (ViewGroup.MarginLayoutParams) views.labelView.getLayoutParams(); - ViewGroup.MarginLayoutParams numberLP = - (ViewGroup.MarginLayoutParams) views.numberView.getLayoutParams(); - // Equivalent to setting android:layout_marginLeft in XML - numberLP.leftMargin = -labelLP.rightMargin; - views.numberView.setLayoutParams(numberLP); - } - } else { - if (number.equals(CallerInfo.UNKNOWN_NUMBER)) { - number = getString(R.string.unknown); - views.callView.setVisibility(View.INVISIBLE); - } else if (number.equals(CallerInfo.PRIVATE_NUMBER)) { - number = getString(R.string.private_num); - views.callView.setVisibility(View.INVISIBLE); - } else if (number.equals(CallerInfo.PAYPHONE_NUMBER)) { - number = getString(R.string.payphone); - } else if (PhoneNumberUtils.extractNetworkPortion(number) - .equals(mVoiceMailNumber)) { - number = getString(R.string.voicemail); - } else { - // Just a raw number, and no cache, so format it nicely - number = formatPhoneNumber(number, null, countryIso); - } - - views.line1View.setText(number); - views.numberView.setVisibility(View.GONE); - views.labelView.setVisibility(View.GONE); - } - - long date = c.getLong(DATE_COLUMN_INDEX); - - // Set the date/time field by mixing relative and absolute times. - int flags = DateUtils.FORMAT_ABBREV_RELATIVE; - - views.dateView.setText(DateUtils.getRelativeTimeSpanString(date, - System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, flags)); - - if (views.iconView != null) { - int type = c.getInt(CALL_TYPE_COLUMN_INDEX); - // Set the icon - switch (type) { - case Calls.INCOMING_TYPE: - views.iconView.setImageDrawable(mDrawableIncoming); - break; - - case Calls.OUTGOING_TYPE: - views.iconView.setImageDrawable(mDrawableOutgoing); - break; - - case Calls.MISSED_TYPE: - views.iconView.setImageDrawable(mDrawableMissed); - break; - } - } - - // Listen for the first draw - if (mPreDrawListener == null) { - mFirst = true; - mPreDrawListener = this; - view.getViewTreeObserver().addOnPreDrawListener(this); - } - } - } - - private static final class QueryHandler extends AsyncQueryHandler { - private final WeakReference mActivity; - - /** - * Simple handler that wraps background calls to catch - * {@link SQLiteException}, such as when the disk is full. - */ - protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler { - public CatchingWorkerHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - try { - // Perform same query while catching any exceptions - super.handleMessage(msg); - } catch (SQLiteDiskIOException e) { - Log.w(TAG, "Exception on background worker thread", e); - } catch (SQLiteFullException e) { - Log.w(TAG, "Exception on background worker thread", e); - } catch (SQLiteDatabaseCorruptException e) { - Log.w(TAG, "Exception on background worker thread", e); - } - } - } - - @Override - protected Handler createHandler(Looper looper) { - // Provide our special handler that catches exceptions - return new CatchingWorkerHandler(looper); - } - - public QueryHandler(Context context) { - super(context.getContentResolver()); - mActivity = new WeakReference( - (RecentCallsListActivity) context); - } - - @Override - protected void onQueryComplete(int token, Object cookie, Cursor cursor) { - final RecentCallsListActivity activity = mActivity.get(); - if (activity != null && !activity.isFinishing()) { - final RecentCallsListActivity.RecentCallsAdapter callsAdapter = activity.mAdapter; - callsAdapter.setLoading(false); - callsAdapter.changeCursor(cursor); - if (activity.mScrollToTop) { - if (activity.mList.getFirstVisiblePosition() > 5) { - activity.mList.setSelection(5); - } - activity.mList.smoothScrollToPosition(0); - activity.mScrollToTop = false; - } - } else { - cursor.close(); - } - } - } - - @Override - protected void onCreate(Bundle state) { - super.onCreate(state); - - setContentView(R.layout.recent_calls); - - // Typing here goes to the dialer - setDefaultKeyMode(DEFAULT_KEYS_DIALER); - - mAdapter = new RecentCallsAdapter(); - getListView().setOnCreateContextMenuListener(this); - setListAdapter(mAdapter); - - mVoiceMailNumber = ((TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE)) - .getVoiceMailNumber(); - mQueryHandler = new QueryHandler(this); - - mCurrentCountryIso = ContactsUtils.getCurrentCountryIso(this); - } - - @Override - protected void onStart() { - mScrollToTop = true; - super.onStart(); - } - - @Override - protected void onResume() { - // The adapter caches looked up numbers, clear it so they will get - // looked up again. - if (mAdapter != null) { - mAdapter.clearCache(); - } - - startQuery(); - resetNewCallsFlag(); - - super.onResume(); - - mAdapter.mPreDrawListener = null; // Let it restart the thread after next draw - } - - @Override - protected void onPause() { - super.onPause(); - - // Kill the requests thread - mAdapter.stopRequestProcessing(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - mAdapter.stopRequestProcessing(); - mAdapter.changeCursor(null); - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - - // Clear notifications only when window gains focus. This activity won't - // immediately receive focus if the keyguard screen is above it. - if (hasFocus) { - try { - ITelephony iTelephony = - ITelephony.Stub.asInterface(ServiceManager.getService("phone")); - if (iTelephony != null) { - iTelephony.cancelMissedCallsNotification(); - } else { - Log.w(TAG, "Telephony service is null, can't call " + - "cancelMissedCallsNotification"); - } - } catch (RemoteException e) { - Log.e(TAG, "Failed to clear missed calls notification due to remote exception"); - } - } - } - - /** - * Format the given phone number - * - * @param number the number to be formatted. - * @param normalizedNumber the normalized number of the given number. - * @param countryIso the ISO 3166-1 two letters country code, the country's - * convention will be used to format the number if the normalized - * phone is null. - * - * @return the formatted number, or the given number if it was formatted. - */ - private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) { - if (TextUtils.isEmpty(number)) { - return ""; - } - // If "number" is really a SIP address, don't try to do any formatting at all. - if (PhoneNumberUtils.isUriNumber(number)) { - return number; - } - if (TextUtils.isEmpty(countryIso)) { - countryIso = mCurrentCountryIso; - } - return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso); - } - - private void resetNewCallsFlag() { - // Mark all "new" missed calls as not new anymore - StringBuilder where = new StringBuilder("type="); - where.append(Calls.MISSED_TYPE); - where.append(" AND new=1"); - - ContentValues values = new ContentValues(1); - values.put(Calls.NEW, "0"); - mQueryHandler.startUpdate(UPDATE_TOKEN, null, Calls.CONTENT_URI, - values, where.toString(), null); - } - - private void startQuery() { - mAdapter.setLoading(true); - - // Cancel any pending queries - mQueryHandler.cancelOperation(QUERY_TOKEN); - mQueryHandler.startQuery(QUERY_TOKEN, null, Calls.CONTENT_URI, - CALL_LOG_PROJECTION, null, null, Calls.DEFAULT_SORT_ORDER); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - menu.add(0, MENU_ITEM_DELETE_ALL, 0, R.string.recentCalls_deleteAll) - .setIcon(android.R.drawable.ic_menu_close_clear_cancel); - return true; - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) { - AdapterView.AdapterContextMenuInfo menuInfo; - try { - menuInfo = (AdapterView.AdapterContextMenuInfo) menuInfoIn; - } catch (ClassCastException e) { - Log.e(TAG, "bad menuInfoIn", e); - return; - } - - Cursor cursor = (Cursor) mAdapter.getItem(menuInfo.position); - - String number = cursor.getString(NUMBER_COLUMN_INDEX); - Uri numberUri = null; - boolean isVoicemail = false; - boolean isSipNumber = false; - if (number.equals(CallerInfo.UNKNOWN_NUMBER)) { - number = getString(R.string.unknown); - } else if (number.equals(CallerInfo.PRIVATE_NUMBER)) { - number = getString(R.string.private_num); - } else if (number.equals(CallerInfo.PAYPHONE_NUMBER)) { - number = getString(R.string.payphone); - } else if (PhoneNumberUtils.extractNetworkPortion(number).equals(mVoiceMailNumber)) { - number = getString(R.string.voicemail); - numberUri = Uri.parse("voicemail:x"); - isVoicemail = true; - } else if (PhoneNumberUtils.isUriNumber(number)) { - numberUri = Uri.fromParts("sip", number, null); - isSipNumber = true; - } else { - numberUri = Uri.fromParts("tel", number, null); - } - - ContactInfo info = mAdapter.getContactInfo(number); - boolean contactInfoPresent = (info != null && info != ContactInfo.EMPTY); - if (contactInfoPresent) { - menu.setHeaderTitle(info.name); - } else { - menu.setHeaderTitle(number); - } - - if (numberUri != null) { - Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, numberUri); - menu.add(0, 0, 0, getResources().getString(R.string.recentCalls_callNumber, number)) - .setIntent(intent); - } - - if (contactInfoPresent) { - menu.add(0, 0, 0, R.string.menu_viewContact) - .setIntent(new Intent(Intent.ACTION_VIEW, - ContentUris.withAppendedId(Contacts.CONTENT_URI, info.personId))); - } - - if (numberUri != null && !isVoicemail && !isSipNumber) { - menu.add(0, 0, 0, R.string.recentCalls_editNumberBeforeCall) - .setIntent(new Intent(Intent.ACTION_DIAL, numberUri)); - menu.add(0, 0, 0, R.string.menu_sendTextMessage) - .setIntent(new Intent(Intent.ACTION_SENDTO, - Uri.fromParts("sms", number, null))); - } - - // "Add to contacts" item, if this entry isn't already associated with a contact - if (!contactInfoPresent && numberUri != null && !isVoicemail && !isSipNumber) { - // TODO: This item is currently disabled for SIP addresses, because - // the Insert.PHONE extra only works correctly for PSTN numbers. - // - // To fix this for SIP addresses, we need to: - // - define ContactsContract.Intents.Insert.SIP_ADDRESS, and use it here if - // the current number is a SIP address - // - update the contacts UI code to handle Insert.SIP_ADDRESS by - // updating the SipAddress field - // and then we can remove the "!isSipNumber" check above. - - Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); - intent.setType(Contacts.CONTENT_ITEM_TYPE); - intent.putExtra(Insert.PHONE, number); - menu.add(0, 0, 0, R.string.recentCalls_addToContact) - .setIntent(intent); - } - menu.add(0, MENU_ITEM_DELETE, 0, R.string.recentCalls_removeFromRecentList); - } - - @Override - protected Dialog onCreateDialog(int id, Bundle args) { - switch (id) { - case DIALOG_CONFIRM_DELETE_ALL: - final ContentResolver resolver = getContentResolver(); - final OnClickListener okListener = new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - final ProgressDialog progressDialog = ProgressDialog.show( - RecentCallsListActivity.this, - getString(R.string.clearCallLogProgress_title), - "", true, false); - final AsyncTask task = new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - resolver.delete(Calls.CONTENT_URI, null, null); - return null; - } - @Override - protected void onPostExecute(Void result) { - progressDialog.dismiss(); - // TODO The change notification should do this automatically, but - // it isn't working right now. Remove this when the change - // notification is working properly. - startQuery(); - } - }; - // TODO: Once we have the API, we should configure this ProgressDialog - // to only show up after a certain time (e.g. 150ms) - progressDialog.show(); - task.execute(); - } - }; - return new AlertDialog.Builder(this) - .setTitle(R.string.clearCallLogConfirmation_title) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setMessage(R.string.clearCallLogConfirmation) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, okListener) - .setCancelable(true) - .create(); - } - return null; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case MENU_ITEM_DELETE_ALL: { - showDialog(DIALOG_CONFIRM_DELETE_ALL); - return true; - } - - case MENU_ITEM_VIEW_CONTACTS: { - Intent intent = new Intent(Intent.ACTION_VIEW, Contacts.CONTENT_URI); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - return true; - } - } - return super.onOptionsItemSelected(item); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - // Convert the menu info to the proper type - AdapterView.AdapterContextMenuInfo menuInfo; - try { - menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); - } catch (ClassCastException e) { - Log.e(TAG, "bad menuInfoIn", e); - return false; - } - - switch (item.getItemId()) { - case MENU_ITEM_DELETE: { - Cursor cursor = (Cursor)mAdapter.getItem(menuInfo.position); - int groupSize = 1; - if (mAdapter.isGroupHeader(menuInfo.position)) { - groupSize = mAdapter.getGroupSize(menuInfo.position); - } - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < groupSize; i++) { - if (i != 0) { - sb.append(","); - cursor.moveToNext(); - } - long id = cursor.getLong(ID_COLUMN_INDEX); - sb.append(id); - } - - getContentResolver().delete(Calls.CONTENT_URI, Calls._ID + " IN (" + sb + ")", - null); - } - } - return super.onContextItemSelected(item); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_CALL: { - long callPressDiff = SystemClock.uptimeMillis() - event.getDownTime(); - if (callPressDiff >= ViewConfiguration.getLongPressTimeout()) { - // Launch voice dialer - Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { - startActivity(intent); - } catch (ActivityNotFoundException e) { - } - return true; - } - } - } - return super.onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_CALL: - try { - ITelephony phone = ITelephony.Stub.asInterface( - ServiceManager.checkService("phone")); - if (phone != null && !phone.isIdle()) { - // Let the super class handle it - break; - } - } catch (RemoteException re) { - // Fall through and try to call the contact - } - - callEntry(getListView().getSelectedItemPosition()); - return true; - } - return super.onKeyUp(keyCode, event); - } - - /* - * Get the number from the Contacts, if available, since sometimes - * the number provided by caller id may not be formatted properly - * depending on the carrier (roaming) in use at the time of the - * incoming call. - * Logic : If the caller-id number starts with a "+", use it - * Else if the number in the contacts starts with a "+", use that one - * Else if the number in the contacts is longer, use that one - */ - private String getBetterNumberFromContacts(String number) { - String matchingNumber = null; - // Look in the cache first. If it's not found then query the Phones db - ContactInfo ci = mAdapter.mContactInfo.get(number); - if (ci != null && ci != ContactInfo.EMPTY) { - matchingNumber = ci.number; - } else { - try { - Cursor phonesCursor = - RecentCallsListActivity.this.getContentResolver().query( - Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, - number), - PHONES_PROJECTION, null, null, null); - if (phonesCursor != null) { - if (phonesCursor.moveToFirst()) { - matchingNumber = phonesCursor.getString(MATCHED_NUMBER_COLUMN_INDEX); - } - phonesCursor.close(); - } - } catch (Exception e) { - // Use the number from the call log - } - } - if (!TextUtils.isEmpty(matchingNumber) && - (matchingNumber.startsWith("+") - || matchingNumber.length() > number.length())) { - number = matchingNumber; - } - return number; - } - - private void callEntry(int position) { - if (position < 0) { - // In touch mode you may often not have something selected, so - // just call the first entry to make sure that [send] [send] calls the - // most recent entry. - position = 0; - } - final Cursor cursor = (Cursor)mAdapter.getItem(position); - if (cursor != null) { - String number = cursor.getString(NUMBER_COLUMN_INDEX); - if (TextUtils.isEmpty(number) - || number.equals(CallerInfo.UNKNOWN_NUMBER) - || number.equals(CallerInfo.PRIVATE_NUMBER) - || number.equals(CallerInfo.PAYPHONE_NUMBER)) { - // This number can't be called, do nothing - return; - } - Intent intent; - // If "number" is really a SIP address, construct a sip: URI. - if (PhoneNumberUtils.isUriNumber(number)) { - intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, - Uri.fromParts("sip", number, null)); - } else { - // We're calling a regular PSTN phone number. - // Construct a tel: URI, but do some other possible cleanup first. - int callType = cursor.getInt(CALL_TYPE_COLUMN_INDEX); - if (!number.startsWith("+") && - (callType == Calls.INCOMING_TYPE - || callType == Calls.MISSED_TYPE)) { - // If the caller-id matches a contact with a better qualified number, use it - number = getBetterNumberFromContacts(number); - } - intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, - Uri.fromParts("tel", number, null)); - } - intent.setFlags( - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - startActivity(intent); - } - } - - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - if (mAdapter.isGroupHeader(position)) { - mAdapter.toggleGroup(position); - } else { - Intent intent = new Intent(this, CallDetailActivity.class); - intent.setData(ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI, id)); - startActivity(intent); - } - } - - @Override - public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, - boolean globalSearch) { - if (globalSearch) { - super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); - } else { - ContactsSearchManager.startSearch(this, initialQuery); - } - } -} diff --git a/src/com/android/contacts/SpecialCharSequenceMgr.java b/src/com/android/contacts/SpecialCharSequenceMgr.java index a047a68ae..9f9f40d46 100644 --- a/src/com/android/contacts/SpecialCharSequenceMgr.java +++ b/src/com/android/contacts/SpecialCharSequenceMgr.java @@ -54,7 +54,7 @@ public class SpecialCharSequenceMgr { private SpecialCharSequenceMgr() { } - static boolean handleChars(Context context, String input, EditText textField) { + public static boolean handleChars(Context context, String input, EditText textField) { return handleChars(context, input, false, textField); } diff --git a/src/com/android/contacts/TwelveKeyDialer.java b/src/com/android/contacts/TwelveKeyDialer.java deleted file mode 100644 index b93375c4a..000000000 --- a/src/com/android/contacts/TwelveKeyDialer.java +++ /dev/null @@ -1,1280 +0,0 @@ -/* - * Copyright (C) 2007 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; - -import com.android.internal.telephony.ITelephony; -import com.android.phone.CallLogAsync; -import com.android.phone.HapticFeedback; - -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.Drawable; -import android.media.AudioManager; -import android.media.ToneGenerator; -import android.net.Uri; -import android.os.Bundle; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.provider.Settings; -import android.provider.Contacts.People; -import android.provider.Contacts.Phones; -import android.provider.Contacts.PhonesColumns; -import android.provider.Contacts.Intents.Insert; -import android.telephony.PhoneNumberFormattingTextWatcher; -import android.telephony.PhoneNumberUtils; -import android.telephony.PhoneStateListener; -import android.telephony.TelephonyManager; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.text.method.DialerKeyListener; -import android.util.Log; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.Window; -import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.TextView; - -/** - * Dialer activity that displays the typical twelve key interface. - */ -@SuppressWarnings("deprecation") -public class TwelveKeyDialer extends Activity implements View.OnClickListener, - View.OnLongClickListener, View.OnKeyListener, - AdapterView.OnItemClickListener, TextWatcher { - private static final String EMPTY_NUMBER = ""; - private static final String TAG = "TwelveKeyDialer"; - - /** The length of DTMF tones in milliseconds */ - private static final int TONE_LENGTH_MS = 150; - - /** The DTMF tone volume relative to other sounds in the stream */ - private static final int TONE_RELATIVE_VOLUME = 80; - - /** Stream type used to play the DTMF tones off call, and mapped to the volume control keys */ - private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_MUSIC; - - private EditText mDigits; - private View mDelete; - private MenuItem mAddToContactMenuItem; - private ToneGenerator mToneGenerator; - private Object mToneGeneratorLock = new Object(); - private Drawable mDigitsBackground; - private Drawable mDigitsEmptyBackground; - private View mDialpad; - private View mVoicemailDialAndDeleteRow; - private View mVoicemailButton; - private View mDialButton; - private ListView mDialpadChooser; - private DialpadChooserAdapter mDialpadChooserAdapter; - //Member variables for dialpad options - private MenuItem m2SecPauseMenuItem; - private MenuItem mWaitMenuItem; - private static final int MENU_ADD_CONTACTS = 1; - private static final int MENU_2S_PAUSE = 2; - private static final int MENU_WAIT = 3; - - // Last number dialed, retrieved asynchronously from the call DB - // in onCreate. This number is displayed when the user hits the - // send key and cleared in onPause. - CallLogAsync mCallLog = new CallLogAsync(); - private String mLastNumberDialed = EMPTY_NUMBER; - - // determines if we want to playback local DTMF tones. - private boolean mDTMFToneEnabled; - - // Vibration (haptic feedback) for dialer key presses. - private HapticFeedback mHaptic = new HapticFeedback(); - - /** Identifier for the "Add Call" intent extra. */ - static final String ADD_CALL_MODE_KEY = "add_call_mode"; - - /** - * Identifier for intent extra for sending an empty Flash message for - * CDMA networks. This message is used by the network to simulate a - * press/depress of the "hookswitch" of a landline phone. Aka "empty flash". - * - * TODO: Using an intent extra to tell the phone to send this flash is a - * temporary measure. To be replaced with an ITelephony call in the future. - * TODO: Keep in sync with the string defined in OutgoingCallBroadcaster.java - * in Phone app until this is replaced with the ITelephony API. - */ - static final String EXTRA_SEND_EMPTY_FLASH - = "com.android.phone.extra.SEND_EMPTY_FLASH"; - - /** Indicates if we are opening this dialer to add a call from the InCallScreen. */ - private boolean mIsAddCallMode; - - private String mCurrentCountryIso; - - PhoneStateListener mPhoneStateListener = new PhoneStateListener() { - /** - * Listen for phone state changes so that we can take down the - * "dialpad chooser" if the phone becomes idle while the - * chooser UI is visible. - */ - @Override - public void onCallStateChanged(int state, String incomingNumber) { - // Log.i(TAG, "PhoneStateListener.onCallStateChanged: " - // + state + ", '" + incomingNumber + "'"); - if ((state == TelephonyManager.CALL_STATE_IDLE) && dialpadChooserVisible()) { - // Log.i(TAG, "Call ended with dialpad chooser visible! Taking it down..."); - // Note there's a race condition in the UI here: the - // dialpad chooser could conceivably disappear (on its - // own) at the exact moment the user was trying to select - // one of the choices, which would be confusing. (But at - // least that's better than leaving the dialpad chooser - // onscreen, but useless...) - showDialpadChooser(false); - } - } - }; - - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Do nothing - } - - public void onTextChanged(CharSequence input, int start, int before, int changeCount) { - // Do nothing - // DTMF Tones do not need to be played here any longer - - // the DTMF dialer handles that functionality now. - } - - public void afterTextChanged(Editable input) { - if (SpecialCharSequenceMgr.handleChars(this, input.toString(), mDigits)) { - // A special sequence was entered, clear the digits - mDigits.getText().clear(); - } - - if (!isDigitsEmpty()) { - mDigits.setBackgroundDrawable(mDigitsBackground); - } else { - mDigits.setCursorVisible(false); - mDigits.setBackgroundDrawable(mDigitsEmptyBackground); - } - - updateDialAndDeleteButtonEnabledState(); - } - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - mCurrentCountryIso = ContactsUtils.getCurrentCountryIso(this); - Resources r = getResources(); - // Do not show title in the case the device is in carmode. - if ((r.getConfiguration().uiMode & Configuration.UI_MODE_TYPE_MASK) == - Configuration.UI_MODE_TYPE_CAR) { - requestWindowFeature(Window.FEATURE_NO_TITLE); - } - // Set the content view - setContentView(getContentViewResource()); - - // Load up the resources for the text field. - mDigitsBackground = r.getDrawable(R.drawable.btn_dial_textfield_active); - mDigitsEmptyBackground = r.getDrawable(R.drawable.btn_dial_textfield); - - mDigits = (EditText) findViewById(R.id.digits); - mDigits.setKeyListener(DialerKeyListener.getInstance()); - mDigits.setOnClickListener(this); - mDigits.setOnKeyListener(this); - - maybeAddNumberFormatting(); - - // Check for the presence of the keypad - View view = findViewById(R.id.one); - if (view != null) { - setupKeypad(); - } - - mVoicemailDialAndDeleteRow = findViewById(R.id.voicemailAndDialAndDelete); - - initVoicemailButton(); - - // Check whether we should show the onscreen "Dial" button. - mDialButton = mVoicemailDialAndDeleteRow.findViewById(R.id.dialButton); - - if (r.getBoolean(R.bool.config_show_onscreen_dial_button)) { - mDialButton.setOnClickListener(this); - } else { - mDialButton.setVisibility(View.GONE); // It's VISIBLE by default - mDialButton = null; - } - - view = mVoicemailDialAndDeleteRow.findViewById(R.id.deleteButton); - view.setOnClickListener(this); - view.setOnLongClickListener(this); - mDelete = view; - - mDialpad = findViewById(R.id.dialpad); // This is null in landscape mode. - - // In landscape we put the keyboard in phone mode. - // In portrait we prevent the soft keyboard to show since the - // dialpad acts as one already. - if (null == mDialpad) { - mDigits.setInputType(android.text.InputType.TYPE_CLASS_PHONE); - } else { - mDigits.setInputType(android.text.InputType.TYPE_NULL); - } - - // Set up the "dialpad chooser" UI; see showDialpadChooser(). - mDialpadChooser = (ListView) findViewById(R.id.dialpadChooser); - mDialpadChooser.setOnItemClickListener(this); - - if (!resolveIntent() && icicle != null) { - super.onRestoreInstanceState(icicle); - } - - try { - mHaptic.init(this, r.getBoolean(R.bool.config_enable_dialer_key_vibration)); - } catch (Resources.NotFoundException nfe) { - Log.e(TAG, "Vibrate control bool missing.", nfe); - } - - } - - @Override - protected void onRestoreInstanceState(Bundle icicle) { - // Do nothing, state is restored in onCreate() if needed - } - - protected void maybeAddNumberFormatting() { - mDigits.addTextChangedListener(new PhoneNumberFormattingTextWatcher(mCurrentCountryIso)); - } - - /** - * Overridden by subclasses to control the resource used by the content view. - */ - protected int getContentViewResource() { - return R.layout.twelve_key_dialer; - } - - private boolean resolveIntent() { - boolean ignoreState = false; - - // Find the proper intent - final Intent intent; - if (isChild()) { - intent = getParent().getIntent(); - ignoreState = intent.getBooleanExtra(DialtactsActivity.EXTRA_IGNORE_STATE, false); - } else { - intent = getIntent(); - } - // Log.i(TAG, "==> resolveIntent(): intent: " + intent); - - // by default we are not adding a call. - mIsAddCallMode = false; - - // By default we don't show the "dialpad chooser" UI. - boolean needToShowDialpadChooser = false; - - // Resolve the intent - final String action = intent.getAction(); - if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) { - // see if we are "adding a call" from the InCallScreen; false by default. - mIsAddCallMode = intent.getBooleanExtra(ADD_CALL_MODE_KEY, false); - - Uri uri = intent.getData(); - if (uri != null) { - if ("tel".equals(uri.getScheme())) { - // Put the requested number into the input area - String data = uri.getSchemeSpecificPart(); - setFormattedDigits(data, null); - } else { - String type = intent.getType(); - if (People.CONTENT_ITEM_TYPE.equals(type) - || Phones.CONTENT_ITEM_TYPE.equals(type)) { - // Query the phone number - Cursor c = getContentResolver().query(intent.getData(), - new String[] {PhonesColumns.NUMBER, PhonesColumns.NUMBER_KEY}, - null, null, null); - if (c != null) { - if (c.moveToFirst()) { - // Put the number into the input area - setFormattedDigits(c.getString(0), c.getString(1)); - } - c.close(); - } - } - } - } else { - // ACTION_DIAL or ACTION_VIEW with no data. - // This behaves basically like ACTION_MAIN: If there's - // already an active call, bring up an intermediate UI to - // make the user confirm what they really want to do. - // Be sure *not* to show the dialpad chooser if this is an - // explicit "Add call" action, though. - if (!mIsAddCallMode && phoneIsInUse()) { - needToShowDialpadChooser = true; - } - } - } else if (Intent.ACTION_MAIN.equals(action)) { - // The MAIN action means we're bringing up a blank dialer - // (e.g. by selecting the Home shortcut, or tabbing over from - // Contacts or Call log.) - // - // At this point, IF there's already an active call, there's a - // good chance that the user got here accidentally (but really - // wanted the in-call dialpad instead). So we bring up an - // intermediate UI to make the user confirm what they really - // want to do. - if (phoneIsInUse()) { - // Log.i(TAG, "resolveIntent(): phone is in use; showing dialpad chooser!"); - needToShowDialpadChooser = true; - } - } - - // Bring up the "dialpad chooser" IFF we need to make the user - // confirm which dialpad they really want. - showDialpadChooser(needToShowDialpadChooser); - - return ignoreState; - } - - protected void setFormattedDigits(String data, String normalizedNumber) { - // strip the non-dialable numbers out of the data string. - String dialString = PhoneNumberUtils.extractNetworkPortion(data); - dialString = - PhoneNumberUtils.formatNumber(dialString, normalizedNumber, mCurrentCountryIso); - if (!TextUtils.isEmpty(dialString)) { - Editable digits = mDigits.getText(); - digits.replace(0, digits.length(), dialString); - // for some reason this isn't getting called in the digits.replace call above.. - // but in any case, this will make sure the background drawable looks right - afterTextChanged(digits); - } - } - - @Override - protected void onNewIntent(Intent newIntent) { - setIntent(newIntent); - resolveIntent(); - } - - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - - // This can't be done in onCreate(), since the auto-restoring of the digits - // will play DTMF tones for all the old digits if it is when onRestoreSavedInstanceState() - // is called. This method will be called every time the activity is created, and - // will always happen after onRestoreSavedInstanceState(). - mDigits.addTextChangedListener(this); - } - - private void setupKeypad() { - // Setup the listeners for the buttons - View view = findViewById(R.id.one); - view.setOnClickListener(this); - view.setOnLongClickListener(this); - - findViewById(R.id.two).setOnClickListener(this); - findViewById(R.id.three).setOnClickListener(this); - findViewById(R.id.four).setOnClickListener(this); - findViewById(R.id.five).setOnClickListener(this); - findViewById(R.id.six).setOnClickListener(this); - findViewById(R.id.seven).setOnClickListener(this); - findViewById(R.id.eight).setOnClickListener(this); - findViewById(R.id.nine).setOnClickListener(this); - findViewById(R.id.star).setOnClickListener(this); - - view = findViewById(R.id.zero); - view.setOnClickListener(this); - view.setOnLongClickListener(this); - - findViewById(R.id.pound).setOnClickListener(this); - } - - @Override - protected void onResume() { - super.onResume(); - - // Query the last dialed number. Do it first because hitting - // the DB is 'slow'. This call is asynchronous. - queryLastOutgoingCall(); - - // retrieve the DTMF tone play back setting. - mDTMFToneEnabled = Settings.System.getInt(getContentResolver(), - Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1; - - // Retrieve the haptic feedback setting. - mHaptic.checkSystemSetting(); - - // if the mToneGenerator creation fails, just continue without it. It is - // a local audio signal, and is not as important as the dtmf tone itself. - synchronized(mToneGeneratorLock) { - if (mToneGenerator == null) { - try { - // we want the user to be able to control the volume of the dial tones - // outside of a call, so we use the stream type that is also mapped to the - // volume control keys for this activity - mToneGenerator = new ToneGenerator(DIAL_TONE_STREAM_TYPE, TONE_RELATIVE_VOLUME); - setVolumeControlStream(DIAL_TONE_STREAM_TYPE); - } catch (RuntimeException e) { - Log.w(TAG, "Exception caught while creating local tone generator: " + e); - mToneGenerator = null; - } - } - } - - Activity parent = getParent(); - // See if we were invoked with a DIAL intent. If we were, fill in the appropriate - // digits in the dialer field. - if (parent != null && parent instanceof DialtactsActivity) { - Uri dialUri = ((DialtactsActivity) parent).getAndClearDialUri(); - if (dialUri != null) { - resolveIntent(); - } - } - - // While we're in the foreground, listen for phone state changes, - // purely so that we can take down the "dialpad chooser" if the - // phone becomes idle while the chooser UI is visible. - TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); - telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); - - // Potentially show hint text in the mDigits field when the user - // hasn't typed any digits yet. (If there's already an active call, - // this hint text will remind the user that he's about to add a new - // call.) - // - // TODO: consider adding better UI for the case where *both* lines - // are currently in use. (Right now we let the user try to add - // another call, but that call is guaranteed to fail. Perhaps the - // entire dialer UI should be disabled instead.) - if (phoneIsInUse()) { - mDigits.setHint(R.string.dialerDialpadHintText); - } else { - // Common case; no hint necessary. - mDigits.setHint(null); - - // Also, a sanity-check: the "dialpad chooser" UI should NEVER - // be visible if the phone is idle! - showDialpadChooser(false); - } - - updateDialAndDeleteButtonEnabledState(); - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - if (hasFocus) { - // Hide soft keyboard, if visible (it's fugly over button dialer). - // The only known case where this will be true is when launching the dialer with - // ACTION_DIAL via a soft keyboard. we dismiss it here because we don't - // have a window token yet in onCreate / onNewIntent - InputMethodManager inputMethodManager = (InputMethodManager) - getSystemService(Context.INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow(mDigits.getWindowToken(), 0); - } - } - - @Override - protected void onPause() { - super.onPause(); - - // Stop listening for phone state changes. - TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); - telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); - - synchronized(mToneGeneratorLock) { - if (mToneGenerator != null) { - mToneGenerator.release(); - mToneGenerator = null; - } - } - // TODO: I wonder if we should not check if the AsyncTask that - // lookup the last dialed number has completed. - mLastNumberDialed = EMPTY_NUMBER; // Since we are going to query again, free stale number. - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - mAddToContactMenuItem = menu.add(0, MENU_ADD_CONTACTS, 0, R.string.recentCalls_addToContact) - .setIcon(android.R.drawable.ic_menu_add); - m2SecPauseMenuItem = menu.add(0, MENU_2S_PAUSE, 0, R.string.add_2sec_pause) - .setIcon(R.drawable.ic_menu_2sec_pause); - mWaitMenuItem = menu.add(0, MENU_WAIT, 0, R.string.add_wait) - .setIcon(R.drawable.ic_menu_wait); - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - // We never show a menu if the "choose dialpad" UI is up. - if (dialpadChooserVisible()) { - return false; - } - - if (isDigitsEmpty()) { - mAddToContactMenuItem.setVisible(false); - m2SecPauseMenuItem.setVisible(false); - mWaitMenuItem.setVisible(false); - } else { - CharSequence digits = mDigits.getText(); - - // Put the current digits string into an intent - Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); - intent.putExtra(Insert.PHONE, digits); - intent.setType(People.CONTENT_ITEM_TYPE); - mAddToContactMenuItem.setIntent(intent); - mAddToContactMenuItem.setVisible(true); - - // Check out whether to show Pause & Wait option menu items - int selectionStart; - int selectionEnd; - String strDigits = digits.toString(); - - selectionStart = mDigits.getSelectionStart(); - selectionEnd = mDigits.getSelectionEnd(); - - if (selectionStart != -1) { - if (selectionStart > selectionEnd) { - // swap it as we want start to be less then end - int tmp = selectionStart; - selectionStart = selectionEnd; - selectionEnd = tmp; - } - - if (selectionStart != 0) { - // Pause can be visible if cursor is not in the begining - m2SecPauseMenuItem.setVisible(true); - - // For Wait to be visible set of condition to meet - mWaitMenuItem.setVisible(showWait(selectionStart, - selectionEnd, strDigits)); - } else { - // cursor in the beginning both pause and wait to be invisible - m2SecPauseMenuItem.setVisible(false); - mWaitMenuItem.setVisible(false); - } - } else { - // cursor is not selected so assume new digit is added to the end - int strLength = strDigits.length(); - mWaitMenuItem.setVisible(showWait(strLength, - strLength, strDigits)); - } - } - return true; - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_CALL: { - long callPressDiff = SystemClock.uptimeMillis() - event.getDownTime(); - if (callPressDiff >= ViewConfiguration.getLongPressTimeout()) { - // Launch voice dialer - Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { - startActivity(intent); - } catch (ActivityNotFoundException e) { - } - } - return true; - } - case KeyEvent.KEYCODE_1: { - long timeDiff = SystemClock.uptimeMillis() - event.getDownTime(); - if (timeDiff >= ViewConfiguration.getLongPressTimeout()) { - // Long press detected, call voice mail - callVoicemail(); - } - return true; - } - } - return super.onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_CALL: { - dialButtonPressed(); - return true; - } - } - return super.onKeyUp(keyCode, event); - } - - private void keyPressed(int keyCode) { - mHaptic.vibrate(); - KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); - mDigits.onKeyDown(keyCode, event); - } - - public boolean onKey(View view, int keyCode, KeyEvent event) { - switch (view.getId()) { - case R.id.digits: - if (keyCode == KeyEvent.KEYCODE_ENTER) { - dialButtonPressed(); - return true; - } - break; - } - return false; - } - - public void onClick(View view) { - switch (view.getId()) { - case R.id.one: { - playTone(ToneGenerator.TONE_DTMF_1); - keyPressed(KeyEvent.KEYCODE_1); - return; - } - case R.id.two: { - playTone(ToneGenerator.TONE_DTMF_2); - keyPressed(KeyEvent.KEYCODE_2); - return; - } - case R.id.three: { - playTone(ToneGenerator.TONE_DTMF_3); - keyPressed(KeyEvent.KEYCODE_3); - return; - } - case R.id.four: { - playTone(ToneGenerator.TONE_DTMF_4); - keyPressed(KeyEvent.KEYCODE_4); - return; - } - case R.id.five: { - playTone(ToneGenerator.TONE_DTMF_5); - keyPressed(KeyEvent.KEYCODE_5); - return; - } - case R.id.six: { - playTone(ToneGenerator.TONE_DTMF_6); - keyPressed(KeyEvent.KEYCODE_6); - return; - } - case R.id.seven: { - playTone(ToneGenerator.TONE_DTMF_7); - keyPressed(KeyEvent.KEYCODE_7); - return; - } - case R.id.eight: { - playTone(ToneGenerator.TONE_DTMF_8); - keyPressed(KeyEvent.KEYCODE_8); - return; - } - case R.id.nine: { - playTone(ToneGenerator.TONE_DTMF_9); - keyPressed(KeyEvent.KEYCODE_9); - return; - } - case R.id.zero: { - playTone(ToneGenerator.TONE_DTMF_0); - keyPressed(KeyEvent.KEYCODE_0); - return; - } - case R.id.pound: { - playTone(ToneGenerator.TONE_DTMF_P); - keyPressed(KeyEvent.KEYCODE_POUND); - return; - } - case R.id.star: { - playTone(ToneGenerator.TONE_DTMF_S); - keyPressed(KeyEvent.KEYCODE_STAR); - return; - } - case R.id.deleteButton: { - keyPressed(KeyEvent.KEYCODE_DEL); - return; - } - case R.id.dialButton: { - mHaptic.vibrate(); // Vibrate here too, just like we do for the regular keys - dialButtonPressed(); - return; - } - case R.id.voicemailButton: { - callVoicemail(); - mHaptic.vibrate(); - return; - } - case R.id.digits: { - if (!isDigitsEmpty()) { - mDigits.setCursorVisible(true); - } - return; - } - } - } - - public boolean onLongClick(View view) { - final Editable digits = mDigits.getText(); - int id = view.getId(); - switch (id) { - case R.id.deleteButton: { - digits.clear(); - // TODO: The framework forgets to clear the pressed - // status of disabled button. Until this is fixed, - // clear manually the pressed status. b/2133127 - mDelete.setPressed(false); - return true; - } - case R.id.one: { - if (isDigitsEmpty()) { - callVoicemail(); - return true; - } - return false; - } - case R.id.zero: { - keyPressed(KeyEvent.KEYCODE_PLUS); - return true; - } - } - return false; - } - - void callVoicemail() { - startActivity(newVoicemailIntent()); - mDigits.getText().clear(); // TODO: Fix bug 1745781 - finish(); - } - - /** - * In most cases, when the dial button is pressed, there is a - * number in digits area. Pack it in the intent, start the - * outgoing call broadcast as a separate task and finish this - * activity. - * - * When there is no digit and the phone is CDMA and off hook, - * we're sending a blank flash for CDMA. CDMA networks use Flash - * messages when special processing needs to be done, mainly for - * 3-way or call waiting scenarios. Presumably, here we're in a - * special 3-way scenario where the network needs a blank flash - * before being able to add the new participant. (This is not the - * case with all 3-way calls, just certain CDMA infrastructures.) - * - * Otherwise, there is no digit, display the last dialed - * number. Don't finish since the user may want to edit it. The - * user needs to press the dial button again, to dial it (general - * case described above). - */ - void dialButtonPressed() { - if (isDigitsEmpty()) { // No number entered. - if (phoneIsCdma() && phoneIsOffhook()) { - // This is really CDMA specific. On GSM is it possible - // to be off hook and wanted to add a 3rd party using - // the redial feature. - startActivity(newFlashIntent()); - } else { - if (!TextUtils.isEmpty(mLastNumberDialed)) { - mDigits.setText(mLastNumberDialed); - } else { - // There's no "last number dialed" or the - // background query is still running. There's - // nothing useful for the Dial button to do in - // this case. Note: with a soft dial button, this - // can never happens since the dial button is - // disabled under these conditons. - playTone(ToneGenerator.TONE_PROP_NACK); - } - } - } else { - final String number = mDigits.getText().toString(); - - startActivity(newDialNumberIntent(number)); - mDigits.getText().clear(); // TODO: Fix bug 1745781 - finish(); - } - } - - - /** - * Plays the specified tone for TONE_LENGTH_MS milliseconds. - * - * The tone is played locally, using the audio stream for phone calls. - * Tones are played only if the "Audible touch tones" user preference - * is checked, and are NOT played if the device is in silent mode. - * - * @param tone a tone code from {@link ToneGenerator} - */ - void playTone(int tone) { - // if local tone playback is disabled, just return. - if (!mDTMFToneEnabled) { - return; - } - - // Also do nothing if the phone is in silent mode. - // We need to re-check the ringer mode for *every* playTone() - // call, rather than keeping a local flag that's updated in - // onResume(), since it's possible to toggle silent mode without - // leaving the current activity (via the ENDCALL-longpress menu.) - AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - int ringerMode = audioManager.getRingerMode(); - if ((ringerMode == AudioManager.RINGER_MODE_SILENT) - || (ringerMode == AudioManager.RINGER_MODE_VIBRATE)) { - return; - } - - synchronized(mToneGeneratorLock) { - if (mToneGenerator == null) { - Log.w(TAG, "playTone: mToneGenerator == null, tone: "+tone); - return; - } - - // Start the new tone (will stop any playing tone) - mToneGenerator.startTone(tone, TONE_LENGTH_MS); - } - } - - /** - * Brings up the "dialpad chooser" UI in place of the usual Dialer - * elements (the textfield/button and the dialpad underneath). - * - * We show this UI if the user brings up the Dialer while a call is - * already in progress, since there's a good chance we got here - * accidentally (and the user really wanted the in-call dialpad instead). - * So in this situation we display an intermediate UI that lets the user - * explicitly choose between the in-call dialpad ("Use touch tone - * keypad") and the regular Dialer ("Add call"). (Or, the option "Return - * to call in progress" just goes back to the in-call UI with no dialpad - * at all.) - * - * @param enabled If true, show the "dialpad chooser" instead - * of the regular Dialer UI - */ - private void showDialpadChooser(boolean enabled) { - if (enabled) { - // Log.i(TAG, "Showing dialpad chooser!"); - mDigits.setVisibility(View.GONE); - if (mDialpad != null) mDialpad.setVisibility(View.GONE); - mVoicemailDialAndDeleteRow.setVisibility(View.GONE); - mDialpadChooser.setVisibility(View.VISIBLE); - - // Instantiate the DialpadChooserAdapter and hook it up to the - // ListView. We do this only once. - if (mDialpadChooserAdapter == null) { - mDialpadChooserAdapter = new DialpadChooserAdapter(this); - mDialpadChooser.setAdapter(mDialpadChooserAdapter); - } - } else { - // Log.i(TAG, "Displaying normal Dialer UI."); - mDigits.setVisibility(View.VISIBLE); - if (mDialpad != null) mDialpad.setVisibility(View.VISIBLE); - mVoicemailDialAndDeleteRow.setVisibility(View.VISIBLE); - mDialpadChooser.setVisibility(View.GONE); - } - } - - /** - * @return true if we're currently showing the "dialpad chooser" UI. - */ - private boolean dialpadChooserVisible() { - return mDialpadChooser.getVisibility() == View.VISIBLE; - } - - /** - * Simple list adapter, binding to an icon + text label - * for each item in the "dialpad chooser" list. - */ - private static class DialpadChooserAdapter extends BaseAdapter { - private LayoutInflater mInflater; - - // Simple struct for a single "choice" item. - static class ChoiceItem { - String text; - Bitmap icon; - int id; - - public ChoiceItem(String s, Bitmap b, int i) { - text = s; - icon = b; - id = i; - } - } - - // IDs for the possible "choices": - static final int DIALPAD_CHOICE_USE_DTMF_DIALPAD = 101; - static final int DIALPAD_CHOICE_RETURN_TO_CALL = 102; - static final int DIALPAD_CHOICE_ADD_NEW_CALL = 103; - - private static final int NUM_ITEMS = 3; - private ChoiceItem mChoiceItems[] = new ChoiceItem[NUM_ITEMS]; - - public DialpadChooserAdapter(Context context) { - // Cache the LayoutInflate to avoid asking for a new one each time. - mInflater = LayoutInflater.from(context); - - // Initialize the possible choices. - // TODO: could this be specified entirely in XML? - - // - "Use touch tone keypad" - mChoiceItems[0] = new ChoiceItem( - context.getString(R.string.dialer_useDtmfDialpad), - BitmapFactory.decodeResource(context.getResources(), - R.drawable.ic_dialer_fork_tt_keypad), - DIALPAD_CHOICE_USE_DTMF_DIALPAD); - - // - "Return to call in progress" - mChoiceItems[1] = new ChoiceItem( - context.getString(R.string.dialer_returnToInCallScreen), - BitmapFactory.decodeResource(context.getResources(), - R.drawable.ic_dialer_fork_current_call), - DIALPAD_CHOICE_RETURN_TO_CALL); - - // - "Add call" - mChoiceItems[2] = new ChoiceItem( - context.getString(R.string.dialer_addAnotherCall), - BitmapFactory.decodeResource(context.getResources(), - R.drawable.ic_dialer_fork_add_call), - DIALPAD_CHOICE_ADD_NEW_CALL); - } - - public int getCount() { - return NUM_ITEMS; - } - - /** - * Return the ChoiceItem for a given position. - */ - public Object getItem(int position) { - return mChoiceItems[position]; - } - - /** - * Return a unique ID for each possible choice. - */ - public long getItemId(int position) { - return position; - } - - /** - * Make a view for each row. - */ - public View getView(int position, View convertView, ViewGroup parent) { - // When convertView is non-null, we can reuse it (there's no need - // to reinflate it.) - if (convertView == null) { - convertView = mInflater.inflate(R.layout.dialpad_chooser_list_item, null); - } - - TextView text = (TextView) convertView.findViewById(R.id.text); - text.setText(mChoiceItems[position].text); - - ImageView icon = (ImageView) convertView.findViewById(R.id.icon); - icon.setImageBitmap(mChoiceItems[position].icon); - - return convertView; - } - } - - /** - * Handle clicks from the dialpad chooser. - */ - public void onItemClick(AdapterView parent, View v, int position, long id) { - DialpadChooserAdapter.ChoiceItem item = - (DialpadChooserAdapter.ChoiceItem) parent.getItemAtPosition(position); - int itemId = item.id; - switch (itemId) { - case DialpadChooserAdapter.DIALPAD_CHOICE_USE_DTMF_DIALPAD: - // Log.i(TAG, "DIALPAD_CHOICE_USE_DTMF_DIALPAD"); - // Fire off an intent to go back to the in-call UI - // with the dialpad visible. - returnToInCallScreen(true); - break; - - case DialpadChooserAdapter.DIALPAD_CHOICE_RETURN_TO_CALL: - // Log.i(TAG, "DIALPAD_CHOICE_RETURN_TO_CALL"); - // Fire off an intent to go back to the in-call UI - // (with the dialpad hidden). - returnToInCallScreen(false); - break; - - case DialpadChooserAdapter.DIALPAD_CHOICE_ADD_NEW_CALL: - // Log.i(TAG, "DIALPAD_CHOICE_ADD_NEW_CALL"); - // Ok, guess the user really did want to be here (in the - // regular Dialer) after all. Bring back the normal Dialer UI. - showDialpadChooser(false); - break; - - default: - Log.w(TAG, "onItemClick: unexpected itemId: " + itemId); - break; - } - } - - /** - * Returns to the in-call UI (where there's presumably a call in - * progress) in response to the user selecting "use touch tone keypad" - * or "return to call" from the dialpad chooser. - */ - private void returnToInCallScreen(boolean showDialpad) { - try { - ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); - if (phone != null) phone.showCallScreenWithDialpad(showDialpad); - } catch (RemoteException e) { - Log.w(TAG, "phone.showCallScreenWithDialpad() failed", e); - } - - // Finally, finish() ourselves so that we don't stay on the - // activity stack. - // Note that we do this whether or not the showCallScreenWithDialpad() - // call above had any effect or not! (That call is a no-op if the - // phone is idle, which can happen if the current call ends while - // the dialpad chooser is up. In this case we can't show the - // InCallScreen, and there's no point staying here in the Dialer, - // so we just take the user back where he came from...) - finish(); - } - - /** - * @return true if the phone is "in use", meaning that at least one line - * is active (ie. off hook or ringing or dialing). - */ - private boolean phoneIsInUse() { - boolean phoneInUse = false; - try { - ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); - if (phone != null) phoneInUse = !phone.isIdle(); - } catch (RemoteException e) { - Log.w(TAG, "phone.isIdle() failed", e); - } - return phoneInUse; - } - - /** - * @return true if the phone is a CDMA phone type - */ - private boolean phoneIsCdma() { - boolean isCdma = false; - try { - ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); - if (phone != null) { - isCdma = (phone.getActivePhoneType() == TelephonyManager.PHONE_TYPE_CDMA); - } - } catch (RemoteException e) { - Log.w(TAG, "phone.getActivePhoneType() failed", e); - } - return isCdma; - } - - /** - * @return true if the phone state is OFFHOOK - */ - private boolean phoneIsOffhook() { - boolean phoneOffhook = false; - try { - ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); - if (phone != null) phoneOffhook = phone.isOffhook(); - } catch (RemoteException e) { - Log.w(TAG, "phone.isOffhook() failed", e); - } - return phoneOffhook; - } - - - /** - * Returns true whenever any one of the options from the menu is selected. - * Code changes to support dialpad options - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case MENU_2S_PAUSE: - updateDialString(","); - return true; - case MENU_WAIT: - updateDialString(";"); - return true; - } - return false; - } - - /** - * Updates the dial string (mDigits) after inserting a Pause character (,) - * or Wait character (;). - */ - private void updateDialString(String newDigits) { - int selectionStart; - int selectionEnd; - - // SpannableStringBuilder editable_text = new SpannableStringBuilder(mDigits.getText()); - int anchor = mDigits.getSelectionStart(); - int point = mDigits.getSelectionEnd(); - - selectionStart = Math.min(anchor, point); - selectionEnd = Math.max(anchor, point); - - Editable digits = mDigits.getText(); - if (selectionStart != -1 ) { - if (selectionStart == selectionEnd) { - // then there is no selection. So insert the pause at this - // position and update the mDigits. - digits.replace(selectionStart, selectionStart, newDigits); - } else { - digits.replace(selectionStart, selectionEnd, newDigits); - // Unselect: back to a regular cursor, just pass the character inserted. - mDigits.setSelection(selectionStart + 1); - } - } else { - int len = mDigits.length(); - digits.replace(len, len, newDigits); - } - } - - /** - * Update the enabledness of the "Dial" and "Backspace" buttons if applicable. - */ - private void updateDialAndDeleteButtonEnabledState() { - final boolean digitsNotEmpty = !isDigitsEmpty(); - - if (mDialButton != null) { - // On CDMA phones, if we're already on a call, we *always* - // enable the Dial button (since you can press it without - // entering any digits to send an empty flash.) - if (phoneIsCdma() && phoneIsOffhook()) { - mDialButton.setEnabled(true); - } else { - // Common case: GSM, or CDMA but not on a call. - // Enable the Dial button if some digits have - // been entered, or if there is a last dialed number - // that could be redialed. - mDialButton.setEnabled(digitsNotEmpty || - !TextUtils.isEmpty(mLastNumberDialed)); - } - } - mDelete.setEnabled(digitsNotEmpty); - } - - - /** - * Check if voicemail is enabled/accessible. - */ - private void initVoicemailButton() { - boolean hasVoicemail = false; - try { - hasVoicemail = TelephonyManager.getDefault().getVoiceMailNumber() != null; - } catch (SecurityException se) { - // Possibly no READ_PHONE_STATE privilege. - } - - mVoicemailButton = mVoicemailDialAndDeleteRow.findViewById(R.id.voicemailButton); - if (hasVoicemail) { - mVoicemailButton.setOnClickListener(this); - } else { - mVoicemailButton.setEnabled(false); - } - } - - /** - * This function return true if Wait menu item can be shown - * otherwise returns false. Assumes the passed string is non-empty - * and the 0th index check is not required. - */ - private boolean showWait(int start, int end, String digits) { - if (start == end) { - // visible false in this case - if (start > digits.length()) return false; - - // preceding char is ';', so visible should be false - if (digits.charAt(start-1) == ';') return false; - - // next char is ';', so visible should be false - if ((digits.length() > start) && (digits.charAt(start) == ';')) return false; - } else { - // visible false in this case - if (start > digits.length() || end > digits.length()) return false; - - // In this case we need to just check for ';' preceding to start - // or next to end - if (digits.charAt(start-1) == ';') return false; - } - return true; - } - - /** - * @return true if the widget with the phone number digits is empty. - */ - private boolean isDigitsEmpty() { - return mDigits.length() == 0; - } - - /** - * Starts the asyn query to get the last dialed/outgoing - * number. When the background query finishes, mLastNumberDialed - * is set to the last dialed number or an empty string if none - * exists yet. - */ - private void queryLastOutgoingCall() { - mLastNumberDialed = EMPTY_NUMBER; - CallLogAsync.GetLastOutgoingCallArgs lastCallArgs = - new CallLogAsync.GetLastOutgoingCallArgs( - this, - new CallLogAsync.OnLastOutgoingCallComplete() { - public void lastOutgoingCall(String number) { - // TODO: Filter out emergency numbers if - // the carrier does not want redial for - // these. - mLastNumberDialed = number; - updateDialAndDeleteButtonEnabledState(); - } - }); - mCallLog.getLastOutgoingCall(lastCallArgs); - } - - @Override - public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, - boolean globalSearch) { - if (globalSearch) { - super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); - } else { - ContactsSearchManager.startSearch(this, initialQuery); - } - } - - // Helpers for the call intents. - private Intent newVoicemailIntent() { - final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, - Uri.fromParts("voicemail", EMPTY_NUMBER, null)); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - return intent; - } - - private Intent newFlashIntent() { - final Intent intent = newDialNumberIntent(EMPTY_NUMBER); - intent.putExtra(EXTRA_SEND_EMPTY_FLASH, true); - return intent; - } - - private Intent newDialNumberIntent(String number) { - final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, - Uri.fromParts("tel", number, null)); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - return intent; - } -} diff --git a/src/com/android/contacts/activities/CallLogActivity.java b/src/com/android/contacts/activities/CallLogActivity.java new file mode 100644 index 000000000..5672af9b1 --- /dev/null +++ b/src/com/android/contacts/activities/CallLogActivity.java @@ -0,0 +1,1287 @@ +/* + * Copyright (C) 2007 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.activities; + +import com.android.common.widget.GroupingListAdapter; +import com.android.contacts.CallDetailActivity; +import com.android.contacts.ContactsSearchManager; +import com.android.contacts.ContactsUtils; +import com.android.contacts.R; +import com.android.internal.telephony.CallerInfo; +import com.android.internal.telephony.ITelephony; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ListActivity; +import android.app.ProgressDialog; +import android.content.ActivityNotFoundException; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.DialogInterface.OnClickListener; +import android.database.CharArrayBuffer; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabaseCorruptException; +import android.database.sqlite.SQLiteDiskIOException; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteFullException; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.provider.CallLog; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.Intents.Insert; +import android.provider.ContactsContract.PhoneLookup; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.ContextMenu.ContextMenuInfo; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.LinkedList; + +/** + * Displays a list of call log entries. + */ +public class CallLogActivity extends ListActivity + implements View.OnCreateContextMenuListener { + private static final String TAG = "CallLogActivity"; + + /** The projection to use when querying the call log table */ + static final String[] CALL_LOG_PROJECTION = new String[] { + Calls._ID, + Calls.NUMBER, + Calls.DATE, + Calls.DURATION, + Calls.TYPE, + Calls.CACHED_NAME, + Calls.CACHED_NUMBER_TYPE, + Calls.CACHED_NUMBER_LABEL, + Calls.COUNTRY_ISO}; + + static final int ID_COLUMN_INDEX = 0; + static final int NUMBER_COLUMN_INDEX = 1; + static final int DATE_COLUMN_INDEX = 2; + static final int DURATION_COLUMN_INDEX = 3; + static final int CALL_TYPE_COLUMN_INDEX = 4; + static final int CALLER_NAME_COLUMN_INDEX = 5; + static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 6; + static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 7; + static final int COUNTRY_ISO_COLUMN_INDEX = 8; + + /** The projection to use when querying the phones table */ + static final String[] PHONES_PROJECTION = new String[] { + PhoneLookup._ID, + PhoneLookup.DISPLAY_NAME, + PhoneLookup.TYPE, + PhoneLookup.LABEL, + PhoneLookup.NUMBER, + PhoneLookup.NORMALIZED_NUMBER}; + + static final int PERSON_ID_COLUMN_INDEX = 0; + static final int NAME_COLUMN_INDEX = 1; + static final int PHONE_TYPE_COLUMN_INDEX = 2; + static final int LABEL_COLUMN_INDEX = 3; + static final int MATCHED_NUMBER_COLUMN_INDEX = 4; + static final int NORMALIZED_NUMBER_COLUMN_INDEX = 5; + + private static final int MENU_ITEM_DELETE = 1; + private static final int MENU_ITEM_DELETE_ALL = 2; + private static final int MENU_ITEM_VIEW_CONTACTS = 3; + + private static final int QUERY_TOKEN = 53; + private static final int UPDATE_TOKEN = 54; + + private static final int DIALOG_CONFIRM_DELETE_ALL = 1; + + CallLogAdapter mAdapter; + private QueryHandler mQueryHandler; + String mVoiceMailNumber; + private String mCurrentCountryIso; + + private boolean mScrollToTop; + + static final class ContactInfo { + public long personId; + public String name; + public int type; + public String label; + public String number; + public String formattedNumber; + public String normalizedNumber; + + public static ContactInfo EMPTY = new ContactInfo(); + } + + public static final class CallLogListItemViews { + TextView line1View; + TextView labelView; + TextView numberView; + TextView dateView; + ImageView iconView; + View callView; + ImageView groupIndicator; + TextView groupSize; + } + + static final class CallerInfoQuery { + String number; + int position; + String name; + int numberType; + String numberLabel; + } + + /** Adapter class to fill in data for the Call Log */ + final class CallLogAdapter extends GroupingListAdapter + implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener { + HashMap mContactInfo; + private final LinkedList mRequests; + private volatile boolean mDone; + private boolean mLoading = true; + ViewTreeObserver.OnPreDrawListener mPreDrawListener; + private static final int REDRAW = 1; + private static final int START_THREAD = 2; + private boolean mFirst; + private Thread mCallerIdThread; + + private CharSequence[] mLabelArray; + + private Drawable mDrawableIncoming; + private Drawable mDrawableOutgoing; + private Drawable mDrawableMissed; + + /** + * Reusable char array buffers. + */ + private CharArrayBuffer mBuffer1 = new CharArrayBuffer(128); + private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128); + + public void onClick(View view) { + String number = (String) view.getTag(); + if (!TextUtils.isEmpty(number)) { + // Here, "number" can either be a PSTN phone number or a + // SIP address. So turn it into either a tel: URI or a + // sip: URI, as appropriate. + Uri callUri; + if (PhoneNumberUtils.isUriNumber(number)) { + callUri = Uri.fromParts("sip", number, null); + } else { + callUri = Uri.fromParts("tel", number, null); + } + startActivity(new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri)); + } + } + + public boolean onPreDraw() { + if (mFirst) { + mHandler.sendEmptyMessageDelayed(START_THREAD, 1000); + mFirst = false; + } + return true; + } + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case REDRAW: + notifyDataSetChanged(); + break; + case START_THREAD: + startRequestProcessing(); + break; + } + } + }; + + public CallLogAdapter() { + super(CallLogActivity.this); + + mContactInfo = new HashMap(); + mRequests = new LinkedList(); + mPreDrawListener = null; + + mDrawableIncoming = getResources().getDrawable( + R.drawable.ic_call_log_list_incoming_call); + mDrawableOutgoing = getResources().getDrawable( + R.drawable.ic_call_log_list_outgoing_call); + mDrawableMissed = getResources().getDrawable( + R.drawable.ic_call_log_list_missed_call); + mLabelArray = getResources().getTextArray(com.android.internal.R.array.phoneTypes); + } + + /** + * Requery on background thread when {@link Cursor} changes. + */ + @Override + protected void onContentChanged() { + // Start async requery + startQuery(); + } + + void setLoading(boolean loading) { + mLoading = loading; + } + + @Override + public boolean isEmpty() { + if (mLoading) { + // We don't want the empty state to show when loading. + return false; + } else { + return super.isEmpty(); + } + } + + public ContactInfo getContactInfo(String number) { + return mContactInfo.get(number); + } + + public void startRequestProcessing() { + mDone = false; + mCallerIdThread = new Thread(this); + mCallerIdThread.setPriority(Thread.MIN_PRIORITY); + mCallerIdThread.start(); + } + + public void stopRequestProcessing() { + mDone = true; + if (mCallerIdThread != null) mCallerIdThread.interrupt(); + } + + public void clearCache() { + synchronized (mContactInfo) { + mContactInfo.clear(); + } + } + + private void updateCallLog(CallerInfoQuery ciq, ContactInfo ci) { + // Check if they are different. If not, don't update. + if (TextUtils.equals(ciq.name, ci.name) + && TextUtils.equals(ciq.numberLabel, ci.label) + && ciq.numberType == ci.type) { + return; + } + ContentValues values = new ContentValues(3); + values.put(Calls.CACHED_NAME, ci.name); + values.put(Calls.CACHED_NUMBER_TYPE, ci.type); + values.put(Calls.CACHED_NUMBER_LABEL, ci.label); + + try { + CallLogActivity.this.getContentResolver().update(Calls.CONTENT_URI, values, + Calls.NUMBER + "='" + ciq.number + "'", null); + } catch (SQLiteDiskIOException e) { + Log.w(TAG, "Exception while updating call info", e); + } catch (SQLiteFullException e) { + Log.w(TAG, "Exception while updating call info", e); + } catch (SQLiteDatabaseCorruptException e) { + Log.w(TAG, "Exception while updating call info", e); + } + } + + private void enqueueRequest(String number, int position, + String name, int numberType, String numberLabel) { + CallerInfoQuery ciq = new CallerInfoQuery(); + ciq.number = number; + ciq.position = position; + ciq.name = name; + ciq.numberType = numberType; + ciq.numberLabel = numberLabel; + synchronized (mRequests) { + mRequests.add(ciq); + mRequests.notifyAll(); + } + } + + private boolean queryContactInfo(CallerInfoQuery ciq) { + // First check if there was a prior request for the same number + // that was already satisfied + ContactInfo info = mContactInfo.get(ciq.number); + boolean needNotify = false; + if (info != null && info != ContactInfo.EMPTY) { + return true; + } else { + // Ok, do a fresh Contacts lookup for ciq.number. + boolean infoUpdated = false; + + if (PhoneNumberUtils.isUriNumber(ciq.number)) { + // This "number" is really a SIP address. + + // TODO: This code is duplicated from the + // CallerInfoAsyncQuery class. To avoid that, could the + // code here just use CallerInfoAsyncQuery, rather than + // manually running ContentResolver.query() itself? + + // We look up SIP addresses directly in the Data table: + Uri contactRef = Data.CONTENT_URI; + + // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent. + // + // Also note we use "upper(data1)" in the WHERE clause, and + // uppercase the incoming SIP address, in order to do a + // case-insensitive match. + // + // TODO: May also need to normalize by adding "sip:" as a + // prefix, if we start storing SIP addresses that way in the + // database. + String selection = "upper(" + Data.DATA1 + ")=?" + + " AND " + + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'"; + String[] selectionArgs = new String[] { ciq.number.toUpperCase() }; + + Cursor dataTableCursor = + CallLogActivity.this.getContentResolver().query( + contactRef, + null, // projection + selection, // selection + selectionArgs, // selectionArgs + null); // sortOrder + + if (dataTableCursor != null) { + if (dataTableCursor.moveToFirst()) { + info = new ContactInfo(); + + // TODO: we could slightly speed this up using an + // explicit projection (and thus not have to do + // those getColumnIndex() calls) but the benefit is + // very minimal. + + // Note the Data.CONTACT_ID column here is + // equivalent to the PERSON_ID_COLUMN_INDEX column + // we use with "phonesCursor" below. + info.personId = dataTableCursor.getLong( + dataTableCursor.getColumnIndex(Data.CONTACT_ID)); + info.name = dataTableCursor.getString( + dataTableCursor.getColumnIndex(Data.DISPLAY_NAME)); + // "type" and "label" are currently unused for SIP addresses + info.type = SipAddress.TYPE_OTHER; + info.label = null; + + // And "number" is the SIP address. + // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent. + info.number = dataTableCursor.getString( + dataTableCursor.getColumnIndex(Data.DATA1)); + info.normalizedNumber = null; // meaningless for SIP addresses + + infoUpdated = true; + } + dataTableCursor.close(); + } + } else { + // "number" is a regular phone number, so use the + // PhoneLookup table: + Cursor phonesCursor = + CallLogActivity.this.getContentResolver().query( + Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, + Uri.encode(ciq.number)), + PHONES_PROJECTION, null, null, null); + if (phonesCursor != null) { + if (phonesCursor.moveToFirst()) { + info = new ContactInfo(); + info.personId = phonesCursor.getLong(PERSON_ID_COLUMN_INDEX); + info.name = phonesCursor.getString(NAME_COLUMN_INDEX); + info.type = phonesCursor.getInt(PHONE_TYPE_COLUMN_INDEX); + info.label = phonesCursor.getString(LABEL_COLUMN_INDEX); + info.number = phonesCursor.getString(MATCHED_NUMBER_COLUMN_INDEX); + info.normalizedNumber = + phonesCursor.getString(NORMALIZED_NUMBER_COLUMN_INDEX); + + infoUpdated = true; + } + phonesCursor.close(); + } + } + + if (infoUpdated) { + // New incoming phone number invalidates our formatted + // cache. Any cache fills happen only on the GUI thread. + info.formattedNumber = null; + + mContactInfo.put(ciq.number, info); + + // Inform list to update this item, if in view + needNotify = true; + } + } + if (info != null) { + updateCallLog(ciq, info); + } + return needNotify; + } + + /* + * Handles requests for contact name and number type + * @see java.lang.Runnable#run() + */ + public void run() { + boolean needNotify = false; + while (!mDone) { + CallerInfoQuery ciq = null; + synchronized (mRequests) { + if (!mRequests.isEmpty()) { + ciq = mRequests.removeFirst(); + } else { + if (needNotify) { + needNotify = false; + mHandler.sendEmptyMessage(REDRAW); + } + try { + mRequests.wait(1000); + } catch (InterruptedException ie) { + // Ignore and continue processing requests + } + } + } + if (ciq != null && queryContactInfo(ciq)) { + needNotify = true; + } + } + } + + @Override + protected void addGroups(Cursor cursor) { + + int count = cursor.getCount(); + if (count == 0) { + return; + } + + int groupItemCount = 1; + + CharArrayBuffer currentValue = mBuffer1; + CharArrayBuffer value = mBuffer2; + cursor.moveToFirst(); + cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, currentValue); + int currentCallType = cursor.getInt(CALL_TYPE_COLUMN_INDEX); + for (int i = 1; i < count; i++) { + cursor.moveToNext(); + cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, value); + boolean sameNumber = equalPhoneNumbers(value, currentValue); + + // Group adjacent calls with the same number. Make an exception + // for the latest item if it was a missed call. We don't want + // a missed call to be hidden inside a group. + if (sameNumber && currentCallType != Calls.MISSED_TYPE) { + groupItemCount++; + } else { + if (groupItemCount > 1) { + addGroup(i - groupItemCount, groupItemCount, false); + } + + groupItemCount = 1; + + // Swap buffers + CharArrayBuffer temp = currentValue; + currentValue = value; + value = temp; + + // If we have just examined a row following a missed call, make + // sure that it is grouped with subsequent calls from the same number + // even if it was also missed. + if (sameNumber && currentCallType == Calls.MISSED_TYPE) { + currentCallType = 0; // "not a missed call" + } else { + currentCallType = cursor.getInt(CALL_TYPE_COLUMN_INDEX); + } + } + } + if (groupItemCount > 1) { + addGroup(count - groupItemCount, groupItemCount, false); + } + } + + protected boolean equalPhoneNumbers(CharArrayBuffer buffer1, CharArrayBuffer buffer2) { + + // TODO add PhoneNumberUtils.compare(CharSequence, CharSequence) to avoid + // string allocation + return PhoneNumberUtils.compare(new String(buffer1.data, 0, buffer1.sizeCopied), + new String(buffer2.data, 0, buffer2.sizeCopied)); + } + + + @Override + protected View newStandAloneView(Context context, ViewGroup parent) { + LayoutInflater inflater = + (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.call_log_list_item, parent, false); + findAndCacheViews(view); + return view; + } + + @Override + protected void bindStandAloneView(View view, Context context, Cursor cursor) { + bindView(context, view, cursor); + } + + @Override + protected View newChildView(Context context, ViewGroup parent) { + LayoutInflater inflater = + (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.call_log_list_child_item, parent, false); + findAndCacheViews(view); + return view; + } + + @Override + protected void bindChildView(View view, Context context, Cursor cursor) { + bindView(context, view, cursor); + } + + @Override + protected View newGroupView(Context context, ViewGroup parent) { + LayoutInflater inflater = + (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.call_log_list_group_item, parent, false); + findAndCacheViews(view); + return view; + } + + @Override + protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize, + boolean expanded) { + final CallLogListItemViews views = (CallLogListItemViews) view.getTag(); + int groupIndicator = expanded + ? com.android.internal.R.drawable.expander_ic_maximized + : com.android.internal.R.drawable.expander_ic_minimized; + views.groupIndicator.setImageResource(groupIndicator); + views.groupSize.setText("(" + groupSize + ")"); + bindView(context, view, cursor); + } + + private void findAndCacheViews(View view) { + + // Get the views to bind to + CallLogListItemViews views = new CallLogListItemViews(); + views.line1View = (TextView) view.findViewById(R.id.line1); + views.labelView = (TextView) view.findViewById(R.id.label); + views.numberView = (TextView) view.findViewById(R.id.number); + views.dateView = (TextView) view.findViewById(R.id.date); + views.iconView = (ImageView) view.findViewById(R.id.call_type_icon); + views.callView = view.findViewById(R.id.call_icon); + views.callView.setOnClickListener(this); + views.groupIndicator = (ImageView) view.findViewById(R.id.groupIndicator); + views.groupSize = (TextView) view.findViewById(R.id.groupSize); + view.setTag(views); + } + + public void bindView(Context context, View view, Cursor c) { + final CallLogListItemViews views = (CallLogListItemViews) view.getTag(); + + String number = c.getString(NUMBER_COLUMN_INDEX); + String formattedNumber = null; + String callerName = c.getString(CALLER_NAME_COLUMN_INDEX); + int callerNumberType = c.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); + String callerNumberLabel = c.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); + String countryIso = c.getString(COUNTRY_ISO_COLUMN_INDEX); + // Store away the number so we can call it directly if you click on the call icon + views.callView.setTag(number); + + // Lookup contacts with this number + ContactInfo info = mContactInfo.get(number); + if (info == null) { + // Mark it as empty and queue up a request to find the name + // The db request should happen on a non-UI thread + info = ContactInfo.EMPTY; + mContactInfo.put(number, info); + enqueueRequest(number, c.getPosition(), + callerName, callerNumberType, callerNumberLabel); + } else if (info != ContactInfo.EMPTY) { // Has been queried + // Check if any data is different from the data cached in the + // calls db. If so, queue the request so that we can update + // the calls db. + if (!TextUtils.equals(info.name, callerName) + || info.type != callerNumberType + || !TextUtils.equals(info.label, callerNumberLabel)) { + // Something is amiss, so sync up. + enqueueRequest(number, c.getPosition(), + callerName, callerNumberType, callerNumberLabel); + } + + // Format and cache phone number for found contact + if (info.formattedNumber == null) { + info.formattedNumber = + formatPhoneNumber(info.number, info.normalizedNumber, countryIso); + } + formattedNumber = info.formattedNumber; + } + + String name = info.name; + int ntype = info.type; + String label = info.label; + // If there's no name cached in our hashmap, but there's one in the + // calls db, use the one in the calls db. Otherwise the name in our + // hashmap is more recent, so it has precedence. + if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(callerName)) { + name = callerName; + ntype = callerNumberType; + label = callerNumberLabel; + + // Format the cached call_log phone number + formattedNumber = formatPhoneNumber(number, null, countryIso); + } + // Set the text lines and call icon. + // Assumes the call back feature is on most of the + // time. For private and unknown numbers: hide it. + views.callView.setVisibility(View.VISIBLE); + + if (!TextUtils.isEmpty(name)) { + views.line1View.setText(name); + views.labelView.setVisibility(View.VISIBLE); + + // "type" and "label" are currently unused for SIP addresses. + CharSequence numberLabel = null; + if (!PhoneNumberUtils.isUriNumber(number)) { + numberLabel = Phone.getDisplayLabel(context, ntype, label, + mLabelArray); + } + views.numberView.setVisibility(View.VISIBLE); + views.numberView.setText(formattedNumber); + if (!TextUtils.isEmpty(numberLabel)) { + views.labelView.setText(numberLabel); + views.labelView.setVisibility(View.VISIBLE); + + // Zero out the numberView's left margin (see below) + ViewGroup.MarginLayoutParams numberLP = + (ViewGroup.MarginLayoutParams) views.numberView.getLayoutParams(); + numberLP.leftMargin = 0; + views.numberView.setLayoutParams(numberLP); + } else { + // There's nothing to display in views.labelView, so hide it. + // We can't set it to View.GONE, since it's the anchor for + // numberView in the RelativeLayout, so make it INVISIBLE. + // Also, we need to manually *subtract* some left margin from + // numberView to compensate for the right margin built in to + // labelView (otherwise the number will be indented by a very + // slight amount). + // TODO: a cleaner fix would be to contain both the label and + // number inside a LinearLayout, and then set labelView *and* + // its padding to GONE when there's no label to display. + views.labelView.setText(null); + views.labelView.setVisibility(View.INVISIBLE); + + ViewGroup.MarginLayoutParams labelLP = + (ViewGroup.MarginLayoutParams) views.labelView.getLayoutParams(); + ViewGroup.MarginLayoutParams numberLP = + (ViewGroup.MarginLayoutParams) views.numberView.getLayoutParams(); + // Equivalent to setting android:layout_marginLeft in XML + numberLP.leftMargin = -labelLP.rightMargin; + views.numberView.setLayoutParams(numberLP); + } + } else { + if (number.equals(CallerInfo.UNKNOWN_NUMBER)) { + number = getString(R.string.unknown); + views.callView.setVisibility(View.INVISIBLE); + } else if (number.equals(CallerInfo.PRIVATE_NUMBER)) { + number = getString(R.string.private_num); + views.callView.setVisibility(View.INVISIBLE); + } else if (number.equals(CallerInfo.PAYPHONE_NUMBER)) { + number = getString(R.string.payphone); + } else if (PhoneNumberUtils.extractNetworkPortion(number) + .equals(mVoiceMailNumber)) { + number = getString(R.string.voicemail); + } else { + // Just a raw number, and no cache, so format it nicely + number = formatPhoneNumber(number, null, countryIso); + } + + views.line1View.setText(number); + views.numberView.setVisibility(View.GONE); + views.labelView.setVisibility(View.GONE); + } + + long date = c.getLong(DATE_COLUMN_INDEX); + + // Set the date/time field by mixing relative and absolute times. + int flags = DateUtils.FORMAT_ABBREV_RELATIVE; + + views.dateView.setText(DateUtils.getRelativeTimeSpanString(date, + System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, flags)); + + if (views.iconView != null) { + int type = c.getInt(CALL_TYPE_COLUMN_INDEX); + // Set the icon + switch (type) { + case Calls.INCOMING_TYPE: + views.iconView.setImageDrawable(mDrawableIncoming); + break; + + case Calls.OUTGOING_TYPE: + views.iconView.setImageDrawable(mDrawableOutgoing); + break; + + case Calls.MISSED_TYPE: + views.iconView.setImageDrawable(mDrawableMissed); + break; + } + } + + // Listen for the first draw + if (mPreDrawListener == null) { + mFirst = true; + mPreDrawListener = this; + view.getViewTreeObserver().addOnPreDrawListener(this); + } + } + } + + private static final class QueryHandler extends AsyncQueryHandler { + private final WeakReference mActivity; + + /** + * Simple handler that wraps background calls to catch + * {@link SQLiteException}, such as when the disk is full. + */ + protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler { + public CatchingWorkerHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + try { + // Perform same query while catching any exceptions + super.handleMessage(msg); + } catch (SQLiteDiskIOException e) { + Log.w(TAG, "Exception on background worker thread", e); + } catch (SQLiteFullException e) { + Log.w(TAG, "Exception on background worker thread", e); + } catch (SQLiteDatabaseCorruptException e) { + Log.w(TAG, "Exception on background worker thread", e); + } + } + } + + @Override + protected Handler createHandler(Looper looper) { + // Provide our special handler that catches exceptions + return new CatchingWorkerHandler(looper); + } + + public QueryHandler(Context context) { + super(context.getContentResolver()); + mActivity = new WeakReference( + (CallLogActivity) context); + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + final CallLogActivity activity = mActivity.get(); + if (activity != null && !activity.isFinishing()) { + final CallLogActivity.CallLogAdapter callsAdapter = activity.mAdapter; + callsAdapter.setLoading(false); + callsAdapter.changeCursor(cursor); + if (activity.mScrollToTop) { + if (activity.mList.getFirstVisiblePosition() > 5) { + activity.mList.setSelection(5); + } + activity.mList.smoothScrollToPosition(0); + activity.mScrollToTop = false; + } + } else { + cursor.close(); + } + } + } + + @Override + protected void onCreate(Bundle state) { + super.onCreate(state); + + setContentView(R.layout.call_log_activity); + + // Typing here goes to the dialer + setDefaultKeyMode(DEFAULT_KEYS_DIALER); + + mAdapter = new CallLogAdapter(); + getListView().setOnCreateContextMenuListener(this); + setListAdapter(mAdapter); + + mVoiceMailNumber = ((TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE)) + .getVoiceMailNumber(); + mQueryHandler = new QueryHandler(this); + + mCurrentCountryIso = ContactsUtils.getCurrentCountryIso(this); + } + + @Override + protected void onStart() { + mScrollToTop = true; + super.onStart(); + } + + @Override + protected void onResume() { + // The adapter caches looked up numbers, clear it so they will get + // looked up again. + if (mAdapter != null) { + mAdapter.clearCache(); + } + + startQuery(); + resetNewCallsFlag(); + + super.onResume(); + + mAdapter.mPreDrawListener = null; // Let it restart the thread after next draw + } + + @Override + protected void onPause() { + super.onPause(); + + // Kill the requests thread + mAdapter.stopRequestProcessing(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mAdapter.stopRequestProcessing(); + mAdapter.changeCursor(null); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + + // Clear notifications only when window gains focus. This activity won't + // immediately receive focus if the keyguard screen is above it. + if (hasFocus) { + try { + ITelephony iTelephony = + ITelephony.Stub.asInterface(ServiceManager.getService("phone")); + if (iTelephony != null) { + iTelephony.cancelMissedCallsNotification(); + } else { + Log.w(TAG, "Telephony service is null, can't call " + + "cancelMissedCallsNotification"); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to clear missed calls notification due to remote exception"); + } + } + } + + /** + * Format the given phone number + * + * @param number the number to be formatted. + * @param normalizedNumber the normalized number of the given number. + * @param countryIso the ISO 3166-1 two letters country code, the country's + * convention will be used to format the number if the normalized + * phone is null. + * + * @return the formatted number, or the given number if it was formatted. + */ + private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) { + if (TextUtils.isEmpty(number)) { + return ""; + } + // If "number" is really a SIP address, don't try to do any formatting at all. + if (PhoneNumberUtils.isUriNumber(number)) { + return number; + } + if (TextUtils.isEmpty(countryIso)) { + countryIso = mCurrentCountryIso; + } + return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso); + } + + private void resetNewCallsFlag() { + // Mark all "new" missed calls as not new anymore + StringBuilder where = new StringBuilder("type="); + where.append(Calls.MISSED_TYPE); + where.append(" AND new=1"); + + ContentValues values = new ContentValues(1); + values.put(Calls.NEW, "0"); + mQueryHandler.startUpdate(UPDATE_TOKEN, null, Calls.CONTENT_URI, + values, where.toString(), null); + } + + private void startQuery() { + mAdapter.setLoading(true); + + // Cancel any pending queries + mQueryHandler.cancelOperation(QUERY_TOKEN); + mQueryHandler.startQuery(QUERY_TOKEN, null, Calls.CONTENT_URI, + CALL_LOG_PROJECTION, null, null, Calls.DEFAULT_SORT_ORDER); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(0, MENU_ITEM_DELETE_ALL, 0, R.string.recentCalls_deleteAll) + .setIcon(android.R.drawable.ic_menu_close_clear_cancel); + return true; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) { + AdapterView.AdapterContextMenuInfo menuInfo; + try { + menuInfo = (AdapterView.AdapterContextMenuInfo) menuInfoIn; + } catch (ClassCastException e) { + Log.e(TAG, "bad menuInfoIn", e); + return; + } + + Cursor cursor = (Cursor) mAdapter.getItem(menuInfo.position); + + String number = cursor.getString(NUMBER_COLUMN_INDEX); + Uri numberUri = null; + boolean isVoicemail = false; + boolean isSipNumber = false; + if (number.equals(CallerInfo.UNKNOWN_NUMBER)) { + number = getString(R.string.unknown); + } else if (number.equals(CallerInfo.PRIVATE_NUMBER)) { + number = getString(R.string.private_num); + } else if (number.equals(CallerInfo.PAYPHONE_NUMBER)) { + number = getString(R.string.payphone); + } else if (PhoneNumberUtils.extractNetworkPortion(number).equals(mVoiceMailNumber)) { + number = getString(R.string.voicemail); + numberUri = Uri.parse("voicemail:x"); + isVoicemail = true; + } else if (PhoneNumberUtils.isUriNumber(number)) { + numberUri = Uri.fromParts("sip", number, null); + isSipNumber = true; + } else { + numberUri = Uri.fromParts("tel", number, null); + } + + ContactInfo info = mAdapter.getContactInfo(number); + boolean contactInfoPresent = (info != null && info != ContactInfo.EMPTY); + if (contactInfoPresent) { + menu.setHeaderTitle(info.name); + } else { + menu.setHeaderTitle(number); + } + + if (numberUri != null) { + Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, numberUri); + menu.add(0, 0, 0, getResources().getString(R.string.recentCalls_callNumber, number)) + .setIntent(intent); + } + + if (contactInfoPresent) { + menu.add(0, 0, 0, R.string.menu_viewContact) + .setIntent(new Intent(Intent.ACTION_VIEW, + ContentUris.withAppendedId(Contacts.CONTENT_URI, info.personId))); + } + + if (numberUri != null && !isVoicemail && !isSipNumber) { + menu.add(0, 0, 0, R.string.recentCalls_editNumberBeforeCall) + .setIntent(new Intent(Intent.ACTION_DIAL, numberUri)); + menu.add(0, 0, 0, R.string.menu_sendTextMessage) + .setIntent(new Intent(Intent.ACTION_SENDTO, + Uri.fromParts("sms", number, null))); + } + + // "Add to contacts" item, if this entry isn't already associated with a contact + if (!contactInfoPresent && numberUri != null && !isVoicemail && !isSipNumber) { + // TODO: This item is currently disabled for SIP addresses, because + // the Insert.PHONE extra only works correctly for PSTN numbers. + // + // To fix this for SIP addresses, we need to: + // - define ContactsContract.Intents.Insert.SIP_ADDRESS, and use it here if + // the current number is a SIP address + // - update the contacts UI code to handle Insert.SIP_ADDRESS by + // updating the SipAddress field + // and then we can remove the "!isSipNumber" check above. + + Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + intent.setType(Contacts.CONTENT_ITEM_TYPE); + intent.putExtra(Insert.PHONE, number); + menu.add(0, 0, 0, R.string.recentCalls_addToContact) + .setIntent(intent); + } + menu.add(0, MENU_ITEM_DELETE, 0, R.string.recentCalls_removeFromRecentList); + } + + @Override + protected Dialog onCreateDialog(int id, Bundle args) { + switch (id) { + case DIALOG_CONFIRM_DELETE_ALL: + final ContentResolver resolver = getContentResolver(); + final OnClickListener okListener = new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + final ProgressDialog progressDialog = ProgressDialog.show( + CallLogActivity.this, + getString(R.string.clearCallLogProgress_title), + "", true, false); + final AsyncTask task = new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + resolver.delete(Calls.CONTENT_URI, null, null); + return null; + } + @Override + protected void onPostExecute(Void result) { + progressDialog.dismiss(); + // TODO The change notification should do this automatically, but + // it isn't working right now. Remove this when the change + // notification is working properly. + startQuery(); + } + }; + // TODO: Once we have the API, we should configure this ProgressDialog + // to only show up after a certain time (e.g. 150ms) + progressDialog.show(); + task.execute(); + } + }; + return new AlertDialog.Builder(this) + .setTitle(R.string.clearCallLogConfirmation_title) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setMessage(R.string.clearCallLogConfirmation) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, okListener) + .setCancelable(true) + .create(); + } + return null; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case MENU_ITEM_DELETE_ALL: { + showDialog(DIALOG_CONFIRM_DELETE_ALL); + return true; + } + + case MENU_ITEM_VIEW_CONTACTS: { + Intent intent = new Intent(Intent.ACTION_VIEW, Contacts.CONTENT_URI); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + // Convert the menu info to the proper type + AdapterView.AdapterContextMenuInfo menuInfo; + try { + menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); + } catch (ClassCastException e) { + Log.e(TAG, "bad menuInfoIn", e); + return false; + } + + switch (item.getItemId()) { + case MENU_ITEM_DELETE: { + Cursor cursor = (Cursor)mAdapter.getItem(menuInfo.position); + int groupSize = 1; + if (mAdapter.isGroupHeader(menuInfo.position)) { + groupSize = mAdapter.getGroupSize(menuInfo.position); + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < groupSize; i++) { + if (i != 0) { + sb.append(","); + cursor.moveToNext(); + } + long id = cursor.getLong(ID_COLUMN_INDEX); + sb.append(id); + } + + getContentResolver().delete(Calls.CONTENT_URI, Calls._ID + " IN (" + sb + ")", + null); + } + } + return super.onContextItemSelected(item); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_CALL: { + long callPressDiff = SystemClock.uptimeMillis() - event.getDownTime(); + if (callPressDiff >= ViewConfiguration.getLongPressTimeout()) { + // Launch voice dialer + Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + } + return true; + } + } + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_CALL: + try { + ITelephony phone = ITelephony.Stub.asInterface( + ServiceManager.checkService("phone")); + if (phone != null && !phone.isIdle()) { + // Let the super class handle it + break; + } + } catch (RemoteException re) { + // Fall through and try to call the contact + } + + callEntry(getListView().getSelectedItemPosition()); + return true; + } + return super.onKeyUp(keyCode, event); + } + + /* + * Get the number from the Contacts, if available, since sometimes + * the number provided by caller id may not be formatted properly + * depending on the carrier (roaming) in use at the time of the + * incoming call. + * Logic : If the caller-id number starts with a "+", use it + * Else if the number in the contacts starts with a "+", use that one + * Else if the number in the contacts is longer, use that one + */ + private String getBetterNumberFromContacts(String number) { + String matchingNumber = null; + // Look in the cache first. If it's not found then query the Phones db + ContactInfo ci = mAdapter.mContactInfo.get(number); + if (ci != null && ci != ContactInfo.EMPTY) { + matchingNumber = ci.number; + } else { + try { + Cursor phonesCursor = + CallLogActivity.this.getContentResolver().query( + Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, + number), + PHONES_PROJECTION, null, null, null); + if (phonesCursor != null) { + if (phonesCursor.moveToFirst()) { + matchingNumber = phonesCursor.getString(MATCHED_NUMBER_COLUMN_INDEX); + } + phonesCursor.close(); + } + } catch (Exception e) { + // Use the number from the call log + } + } + if (!TextUtils.isEmpty(matchingNumber) && + (matchingNumber.startsWith("+") + || matchingNumber.length() > number.length())) { + number = matchingNumber; + } + return number; + } + + private void callEntry(int position) { + if (position < 0) { + // In touch mode you may often not have something selected, so + // just call the first entry to make sure that [send] [send] calls the + // most recent entry. + position = 0; + } + final Cursor cursor = (Cursor)mAdapter.getItem(position); + if (cursor != null) { + String number = cursor.getString(NUMBER_COLUMN_INDEX); + if (TextUtils.isEmpty(number) + || number.equals(CallerInfo.UNKNOWN_NUMBER) + || number.equals(CallerInfo.PRIVATE_NUMBER) + || number.equals(CallerInfo.PAYPHONE_NUMBER)) { + // This number can't be called, do nothing + return; + } + Intent intent; + // If "number" is really a SIP address, construct a sip: URI. + if (PhoneNumberUtils.isUriNumber(number)) { + intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, + Uri.fromParts("sip", number, null)); + } else { + // We're calling a regular PSTN phone number. + // Construct a tel: URI, but do some other possible cleanup first. + int callType = cursor.getInt(CALL_TYPE_COLUMN_INDEX); + if (!number.startsWith("+") && + (callType == Calls.INCOMING_TYPE + || callType == Calls.MISSED_TYPE)) { + // If the caller-id matches a contact with a better qualified number, use it + number = getBetterNumberFromContacts(number); + } + intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, + Uri.fromParts("tel", number, null)); + } + intent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + startActivity(intent); + } + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + if (mAdapter.isGroupHeader(position)) { + mAdapter.toggleGroup(position); + } else { + Intent intent = new Intent(this, CallDetailActivity.class); + intent.setData(ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI, id)); + startActivity(intent); + } + } + + @Override + public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, + boolean globalSearch) { + if (globalSearch) { + super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); + } else { + ContactsSearchManager.startSearch(this, initialQuery); + } + } +} diff --git a/src/com/android/contacts/activities/ContactsFrontDoor.java b/src/com/android/contacts/activities/ContactsFrontDoor.java index 1f8a491fb..d34f2c4b5 100644 --- a/src/com/android/contacts/activities/ContactsFrontDoor.java +++ b/src/com/android/contacts/activities/ContactsFrontDoor.java @@ -17,7 +17,6 @@ package com.android.contacts.activities; import com.android.contacts.ContactsActivity; -import com.android.contacts.DialtactsActivity; import com.android.contacts.util.PhoneCapabilityTester; import android.content.Intent; diff --git a/src/com/android/contacts/activities/DialpadActivity.java b/src/com/android/contacts/activities/DialpadActivity.java new file mode 100644 index 000000000..cfbdff27b --- /dev/null +++ b/src/com/android/contacts/activities/DialpadActivity.java @@ -0,0 +1,1284 @@ +/* + * Copyright (C) 2007 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.activities; + +import com.android.contacts.ContactsSearchManager; +import com.android.contacts.ContactsUtils; +import com.android.contacts.R; +import com.android.contacts.SpecialCharSequenceMgr; +import com.android.internal.telephony.ITelephony; +import com.android.phone.CallLogAsync; +import com.android.phone.HapticFeedback; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Drawable; +import android.media.AudioManager; +import android.media.ToneGenerator; +import android.net.Uri; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.provider.Settings; +import android.provider.Contacts.People; +import android.provider.Contacts.Phones; +import android.provider.Contacts.PhonesColumns; +import android.provider.Contacts.Intents.Insert; +import android.telephony.PhoneNumberFormattingTextWatcher; +import android.telephony.PhoneNumberUtils; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.method.DialerKeyListener; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.Window; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +/** + * Dialer activity that displays the typical twelve key interface. + */ +@SuppressWarnings("deprecation") +public class DialpadActivity extends Activity implements View.OnClickListener, + View.OnLongClickListener, View.OnKeyListener, + AdapterView.OnItemClickListener, TextWatcher { + private static final String EMPTY_NUMBER = ""; + private static final String TAG = "DialpadActivity"; + + /** The length of DTMF tones in milliseconds */ + private static final int TONE_LENGTH_MS = 150; + + /** The DTMF tone volume relative to other sounds in the stream */ + private static final int TONE_RELATIVE_VOLUME = 80; + + /** Stream type used to play the DTMF tones off call, and mapped to the volume control keys */ + private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_MUSIC; + + private EditText mDigits; + private View mDelete; + private MenuItem mAddToContactMenuItem; + private ToneGenerator mToneGenerator; + private Object mToneGeneratorLock = new Object(); + private Drawable mDigitsBackground; + private Drawable mDigitsEmptyBackground; + private View mDialpad; + private View mVoicemailDialAndDeleteRow; + private View mVoicemailButton; + private View mDialButton; + private ListView mDialpadChooser; + private DialpadChooserAdapter mDialpadChooserAdapter; + //Member variables for dialpad options + private MenuItem m2SecPauseMenuItem; + private MenuItem mWaitMenuItem; + private static final int MENU_ADD_CONTACTS = 1; + private static final int MENU_2S_PAUSE = 2; + private static final int MENU_WAIT = 3; + + // Last number dialed, retrieved asynchronously from the call DB + // in onCreate. This number is displayed when the user hits the + // send key and cleared in onPause. + CallLogAsync mCallLog = new CallLogAsync(); + private String mLastNumberDialed = EMPTY_NUMBER; + + // determines if we want to playback local DTMF tones. + private boolean mDTMFToneEnabled; + + // Vibration (haptic feedback) for dialer key presses. + private HapticFeedback mHaptic = new HapticFeedback(); + + /** Identifier for the "Add Call" intent extra. */ + static final String ADD_CALL_MODE_KEY = "add_call_mode"; + + /** + * Identifier for intent extra for sending an empty Flash message for + * CDMA networks. This message is used by the network to simulate a + * press/depress of the "hookswitch" of a landline phone. Aka "empty flash". + * + * TODO: Using an intent extra to tell the phone to send this flash is a + * temporary measure. To be replaced with an ITelephony call in the future. + * TODO: Keep in sync with the string defined in OutgoingCallBroadcaster.java + * in Phone app until this is replaced with the ITelephony API. + */ + static final String EXTRA_SEND_EMPTY_FLASH + = "com.android.phone.extra.SEND_EMPTY_FLASH"; + + /** Indicates if we are opening this dialer to add a call from the InCallScreen. */ + private boolean mIsAddCallMode; + + private String mCurrentCountryIso; + + PhoneStateListener mPhoneStateListener = new PhoneStateListener() { + /** + * Listen for phone state changes so that we can take down the + * "dialpad chooser" if the phone becomes idle while the + * chooser UI is visible. + */ + @Override + public void onCallStateChanged(int state, String incomingNumber) { + // Log.i(TAG, "PhoneStateListener.onCallStateChanged: " + // + state + ", '" + incomingNumber + "'"); + if ((state == TelephonyManager.CALL_STATE_IDLE) && dialpadChooserVisible()) { + // Log.i(TAG, "Call ended with dialpad chooser visible! Taking it down..."); + // Note there's a race condition in the UI here: the + // dialpad chooser could conceivably disappear (on its + // own) at the exact moment the user was trying to select + // one of the choices, which would be confusing. (But at + // least that's better than leaving the dialpad chooser + // onscreen, but useless...) + showDialpadChooser(false); + } + } + }; + + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing + } + + public void onTextChanged(CharSequence input, int start, int before, int changeCount) { + // Do nothing + // DTMF Tones do not need to be played here any longer - + // the DTMF dialer handles that functionality now. + } + + public void afterTextChanged(Editable input) { + if (SpecialCharSequenceMgr.handleChars(this, input.toString(), mDigits)) { + // A special sequence was entered, clear the digits + mDigits.getText().clear(); + } + + if (!isDigitsEmpty()) { + mDigits.setBackgroundDrawable(mDigitsBackground); + } else { + mDigits.setCursorVisible(false); + mDigits.setBackgroundDrawable(mDigitsEmptyBackground); + } + + updateDialAndDeleteButtonEnabledState(); + } + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + mCurrentCountryIso = ContactsUtils.getCurrentCountryIso(this); + Resources r = getResources(); + // Do not show title in the case the device is in carmode. + if ((r.getConfiguration().uiMode & Configuration.UI_MODE_TYPE_MASK) == + Configuration.UI_MODE_TYPE_CAR) { + requestWindowFeature(Window.FEATURE_NO_TITLE); + } + // Set the content view + setContentView(getContentViewResource()); + + // Load up the resources for the text field. + mDigitsBackground = r.getDrawable(R.drawable.btn_dial_textfield_active); + mDigitsEmptyBackground = r.getDrawable(R.drawable.btn_dial_textfield); + + mDigits = (EditText) findViewById(R.id.digits); + mDigits.setKeyListener(DialerKeyListener.getInstance()); + mDigits.setOnClickListener(this); + mDigits.setOnKeyListener(this); + + maybeAddNumberFormatting(); + + // Check for the presence of the keypad + View view = findViewById(R.id.one); + if (view != null) { + setupKeypad(); + } + + mVoicemailDialAndDeleteRow = findViewById(R.id.voicemailAndDialAndDelete); + + initVoicemailButton(); + + // Check whether we should show the onscreen "Dial" button. + mDialButton = mVoicemailDialAndDeleteRow.findViewById(R.id.dialButton); + + if (r.getBoolean(R.bool.config_show_onscreen_dial_button)) { + mDialButton.setOnClickListener(this); + } else { + mDialButton.setVisibility(View.GONE); // It's VISIBLE by default + mDialButton = null; + } + + view = mVoicemailDialAndDeleteRow.findViewById(R.id.deleteButton); + view.setOnClickListener(this); + view.setOnLongClickListener(this); + mDelete = view; + + mDialpad = findViewById(R.id.dialpad); // This is null in landscape mode. + + // In landscape we put the keyboard in phone mode. + // In portrait we prevent the soft keyboard to show since the + // dialpad acts as one already. + if (null == mDialpad) { + mDigits.setInputType(android.text.InputType.TYPE_CLASS_PHONE); + } else { + mDigits.setInputType(android.text.InputType.TYPE_NULL); + } + + // Set up the "dialpad chooser" UI; see showDialpadChooser(). + mDialpadChooser = (ListView) findViewById(R.id.dialpadChooser); + mDialpadChooser.setOnItemClickListener(this); + + if (!resolveIntent() && icicle != null) { + super.onRestoreInstanceState(icicle); + } + + try { + mHaptic.init(this, r.getBoolean(R.bool.config_enable_dialer_key_vibration)); + } catch (Resources.NotFoundException nfe) { + Log.e(TAG, "Vibrate control bool missing.", nfe); + } + + } + + @Override + protected void onRestoreInstanceState(Bundle icicle) { + // Do nothing, state is restored in onCreate() if needed + } + + protected void maybeAddNumberFormatting() { + mDigits.addTextChangedListener(new PhoneNumberFormattingTextWatcher(mCurrentCountryIso)); + } + + /** + * Overridden by subclasses to control the resource used by the content view. + */ + protected int getContentViewResource() { + return R.layout.dialpad_activity; + } + + private boolean resolveIntent() { + boolean ignoreState = false; + + // Find the proper intent + final Intent intent; + if (isChild()) { + intent = getParent().getIntent(); + ignoreState = intent.getBooleanExtra(DialtactsActivity.EXTRA_IGNORE_STATE, false); + } else { + intent = getIntent(); + } + // Log.i(TAG, "==> resolveIntent(): intent: " + intent); + + // by default we are not adding a call. + mIsAddCallMode = false; + + // By default we don't show the "dialpad chooser" UI. + boolean needToShowDialpadChooser = false; + + // Resolve the intent + final String action = intent.getAction(); + if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) { + // see if we are "adding a call" from the InCallScreen; false by default. + mIsAddCallMode = intent.getBooleanExtra(ADD_CALL_MODE_KEY, false); + + Uri uri = intent.getData(); + if (uri != null) { + if ("tel".equals(uri.getScheme())) { + // Put the requested number into the input area + String data = uri.getSchemeSpecificPart(); + setFormattedDigits(data, null); + } else { + String type = intent.getType(); + if (People.CONTENT_ITEM_TYPE.equals(type) + || Phones.CONTENT_ITEM_TYPE.equals(type)) { + // Query the phone number + Cursor c = getContentResolver().query(intent.getData(), + new String[] {PhonesColumns.NUMBER, PhonesColumns.NUMBER_KEY}, + null, null, null); + if (c != null) { + if (c.moveToFirst()) { + // Put the number into the input area + setFormattedDigits(c.getString(0), c.getString(1)); + } + c.close(); + } + } + } + } else { + // ACTION_DIAL or ACTION_VIEW with no data. + // This behaves basically like ACTION_MAIN: If there's + // already an active call, bring up an intermediate UI to + // make the user confirm what they really want to do. + // Be sure *not* to show the dialpad chooser if this is an + // explicit "Add call" action, though. + if (!mIsAddCallMode && phoneIsInUse()) { + needToShowDialpadChooser = true; + } + } + } else if (Intent.ACTION_MAIN.equals(action)) { + // The MAIN action means we're bringing up a blank dialer + // (e.g. by selecting the Home shortcut, or tabbing over from + // Contacts or Call log.) + // + // At this point, IF there's already an active call, there's a + // good chance that the user got here accidentally (but really + // wanted the in-call dialpad instead). So we bring up an + // intermediate UI to make the user confirm what they really + // want to do. + if (phoneIsInUse()) { + // Log.i(TAG, "resolveIntent(): phone is in use; showing dialpad chooser!"); + needToShowDialpadChooser = true; + } + } + + // Bring up the "dialpad chooser" IFF we need to make the user + // confirm which dialpad they really want. + showDialpadChooser(needToShowDialpadChooser); + + return ignoreState; + } + + protected void setFormattedDigits(String data, String normalizedNumber) { + // strip the non-dialable numbers out of the data string. + String dialString = PhoneNumberUtils.extractNetworkPortion(data); + dialString = + PhoneNumberUtils.formatNumber(dialString, normalizedNumber, mCurrentCountryIso); + if (!TextUtils.isEmpty(dialString)) { + Editable digits = mDigits.getText(); + digits.replace(0, digits.length(), dialString); + // for some reason this isn't getting called in the digits.replace call above.. + // but in any case, this will make sure the background drawable looks right + afterTextChanged(digits); + } + } + + @Override + protected void onNewIntent(Intent newIntent) { + setIntent(newIntent); + resolveIntent(); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + + // This can't be done in onCreate(), since the auto-restoring of the digits + // will play DTMF tones for all the old digits if it is when onRestoreSavedInstanceState() + // is called. This method will be called every time the activity is created, and + // will always happen after onRestoreSavedInstanceState(). + mDigits.addTextChangedListener(this); + } + + private void setupKeypad() { + // Setup the listeners for the buttons + View view = findViewById(R.id.one); + view.setOnClickListener(this); + view.setOnLongClickListener(this); + + findViewById(R.id.two).setOnClickListener(this); + findViewById(R.id.three).setOnClickListener(this); + findViewById(R.id.four).setOnClickListener(this); + findViewById(R.id.five).setOnClickListener(this); + findViewById(R.id.six).setOnClickListener(this); + findViewById(R.id.seven).setOnClickListener(this); + findViewById(R.id.eight).setOnClickListener(this); + findViewById(R.id.nine).setOnClickListener(this); + findViewById(R.id.star).setOnClickListener(this); + + view = findViewById(R.id.zero); + view.setOnClickListener(this); + view.setOnLongClickListener(this); + + findViewById(R.id.pound).setOnClickListener(this); + } + + @Override + protected void onResume() { + super.onResume(); + + // Query the last dialed number. Do it first because hitting + // the DB is 'slow'. This call is asynchronous. + queryLastOutgoingCall(); + + // retrieve the DTMF tone play back setting. + mDTMFToneEnabled = Settings.System.getInt(getContentResolver(), + Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1; + + // Retrieve the haptic feedback setting. + mHaptic.checkSystemSetting(); + + // if the mToneGenerator creation fails, just continue without it. It is + // a local audio signal, and is not as important as the dtmf tone itself. + synchronized(mToneGeneratorLock) { + if (mToneGenerator == null) { + try { + // we want the user to be able to control the volume of the dial tones + // outside of a call, so we use the stream type that is also mapped to the + // volume control keys for this activity + mToneGenerator = new ToneGenerator(DIAL_TONE_STREAM_TYPE, TONE_RELATIVE_VOLUME); + setVolumeControlStream(DIAL_TONE_STREAM_TYPE); + } catch (RuntimeException e) { + Log.w(TAG, "Exception caught while creating local tone generator: " + e); + mToneGenerator = null; + } + } + } + + Activity parent = getParent(); + // See if we were invoked with a DIAL intent. If we were, fill in the appropriate + // digits in the dialer field. + if (parent != null && parent instanceof DialtactsActivity) { + Uri dialUri = ((DialtactsActivity) parent).getAndClearDialUri(); + if (dialUri != null) { + resolveIntent(); + } + } + + // While we're in the foreground, listen for phone state changes, + // purely so that we can take down the "dialpad chooser" if the + // phone becomes idle while the chooser UI is visible. + TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); + telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); + + // Potentially show hint text in the mDigits field when the user + // hasn't typed any digits yet. (If there's already an active call, + // this hint text will remind the user that he's about to add a new + // call.) + // + // TODO: consider adding better UI for the case where *both* lines + // are currently in use. (Right now we let the user try to add + // another call, but that call is guaranteed to fail. Perhaps the + // entire dialer UI should be disabled instead.) + if (phoneIsInUse()) { + mDigits.setHint(R.string.dialerDialpadHintText); + } else { + // Common case; no hint necessary. + mDigits.setHint(null); + + // Also, a sanity-check: the "dialpad chooser" UI should NEVER + // be visible if the phone is idle! + showDialpadChooser(false); + } + + updateDialAndDeleteButtonEnabledState(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + if (hasFocus) { + // Hide soft keyboard, if visible (it's fugly over button dialer). + // The only known case where this will be true is when launching the dialer with + // ACTION_DIAL via a soft keyboard. we dismiss it here because we don't + // have a window token yet in onCreate / onNewIntent + InputMethodManager inputMethodManager = (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(mDigits.getWindowToken(), 0); + } + } + + @Override + protected void onPause() { + super.onPause(); + + // Stop listening for phone state changes. + TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); + telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); + + synchronized(mToneGeneratorLock) { + if (mToneGenerator != null) { + mToneGenerator.release(); + mToneGenerator = null; + } + } + // TODO: I wonder if we should not check if the AsyncTask that + // lookup the last dialed number has completed. + mLastNumberDialed = EMPTY_NUMBER; // Since we are going to query again, free stale number. + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + mAddToContactMenuItem = menu.add(0, MENU_ADD_CONTACTS, 0, R.string.recentCalls_addToContact) + .setIcon(android.R.drawable.ic_menu_add); + m2SecPauseMenuItem = menu.add(0, MENU_2S_PAUSE, 0, R.string.add_2sec_pause) + .setIcon(R.drawable.ic_menu_2sec_pause); + mWaitMenuItem = menu.add(0, MENU_WAIT, 0, R.string.add_wait) + .setIcon(R.drawable.ic_menu_wait); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + // We never show a menu if the "choose dialpad" UI is up. + if (dialpadChooserVisible()) { + return false; + } + + if (isDigitsEmpty()) { + mAddToContactMenuItem.setVisible(false); + m2SecPauseMenuItem.setVisible(false); + mWaitMenuItem.setVisible(false); + } else { + CharSequence digits = mDigits.getText(); + + // Put the current digits string into an intent + Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Insert.PHONE, digits); + intent.setType(People.CONTENT_ITEM_TYPE); + mAddToContactMenuItem.setIntent(intent); + mAddToContactMenuItem.setVisible(true); + + // Check out whether to show Pause & Wait option menu items + int selectionStart; + int selectionEnd; + String strDigits = digits.toString(); + + selectionStart = mDigits.getSelectionStart(); + selectionEnd = mDigits.getSelectionEnd(); + + if (selectionStart != -1) { + if (selectionStart > selectionEnd) { + // swap it as we want start to be less then end + int tmp = selectionStart; + selectionStart = selectionEnd; + selectionEnd = tmp; + } + + if (selectionStart != 0) { + // Pause can be visible if cursor is not in the begining + m2SecPauseMenuItem.setVisible(true); + + // For Wait to be visible set of condition to meet + mWaitMenuItem.setVisible(showWait(selectionStart, + selectionEnd, strDigits)); + } else { + // cursor in the beginning both pause and wait to be invisible + m2SecPauseMenuItem.setVisible(false); + mWaitMenuItem.setVisible(false); + } + } else { + // cursor is not selected so assume new digit is added to the end + int strLength = strDigits.length(); + mWaitMenuItem.setVisible(showWait(strLength, + strLength, strDigits)); + } + } + return true; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_CALL: { + long callPressDiff = SystemClock.uptimeMillis() - event.getDownTime(); + if (callPressDiff >= ViewConfiguration.getLongPressTimeout()) { + // Launch voice dialer + Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + } + } + return true; + } + case KeyEvent.KEYCODE_1: { + long timeDiff = SystemClock.uptimeMillis() - event.getDownTime(); + if (timeDiff >= ViewConfiguration.getLongPressTimeout()) { + // Long press detected, call voice mail + callVoicemail(); + } + return true; + } + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_CALL: { + dialButtonPressed(); + return true; + } + } + return super.onKeyUp(keyCode, event); + } + + private void keyPressed(int keyCode) { + mHaptic.vibrate(); + KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); + mDigits.onKeyDown(keyCode, event); + } + + public boolean onKey(View view, int keyCode, KeyEvent event) { + switch (view.getId()) { + case R.id.digits: + if (keyCode == KeyEvent.KEYCODE_ENTER) { + dialButtonPressed(); + return true; + } + break; + } + return false; + } + + public void onClick(View view) { + switch (view.getId()) { + case R.id.one: { + playTone(ToneGenerator.TONE_DTMF_1); + keyPressed(KeyEvent.KEYCODE_1); + return; + } + case R.id.two: { + playTone(ToneGenerator.TONE_DTMF_2); + keyPressed(KeyEvent.KEYCODE_2); + return; + } + case R.id.three: { + playTone(ToneGenerator.TONE_DTMF_3); + keyPressed(KeyEvent.KEYCODE_3); + return; + } + case R.id.four: { + playTone(ToneGenerator.TONE_DTMF_4); + keyPressed(KeyEvent.KEYCODE_4); + return; + } + case R.id.five: { + playTone(ToneGenerator.TONE_DTMF_5); + keyPressed(KeyEvent.KEYCODE_5); + return; + } + case R.id.six: { + playTone(ToneGenerator.TONE_DTMF_6); + keyPressed(KeyEvent.KEYCODE_6); + return; + } + case R.id.seven: { + playTone(ToneGenerator.TONE_DTMF_7); + keyPressed(KeyEvent.KEYCODE_7); + return; + } + case R.id.eight: { + playTone(ToneGenerator.TONE_DTMF_8); + keyPressed(KeyEvent.KEYCODE_8); + return; + } + case R.id.nine: { + playTone(ToneGenerator.TONE_DTMF_9); + keyPressed(KeyEvent.KEYCODE_9); + return; + } + case R.id.zero: { + playTone(ToneGenerator.TONE_DTMF_0); + keyPressed(KeyEvent.KEYCODE_0); + return; + } + case R.id.pound: { + playTone(ToneGenerator.TONE_DTMF_P); + keyPressed(KeyEvent.KEYCODE_POUND); + return; + } + case R.id.star: { + playTone(ToneGenerator.TONE_DTMF_S); + keyPressed(KeyEvent.KEYCODE_STAR); + return; + } + case R.id.deleteButton: { + keyPressed(KeyEvent.KEYCODE_DEL); + return; + } + case R.id.dialButton: { + mHaptic.vibrate(); // Vibrate here too, just like we do for the regular keys + dialButtonPressed(); + return; + } + case R.id.voicemailButton: { + callVoicemail(); + mHaptic.vibrate(); + return; + } + case R.id.digits: { + if (!isDigitsEmpty()) { + mDigits.setCursorVisible(true); + } + return; + } + } + } + + public boolean onLongClick(View view) { + final Editable digits = mDigits.getText(); + int id = view.getId(); + switch (id) { + case R.id.deleteButton: { + digits.clear(); + // TODO: The framework forgets to clear the pressed + // status of disabled button. Until this is fixed, + // clear manually the pressed status. b/2133127 + mDelete.setPressed(false); + return true; + } + case R.id.one: { + if (isDigitsEmpty()) { + callVoicemail(); + return true; + } + return false; + } + case R.id.zero: { + keyPressed(KeyEvent.KEYCODE_PLUS); + return true; + } + } + return false; + } + + void callVoicemail() { + startActivity(newVoicemailIntent()); + mDigits.getText().clear(); // TODO: Fix bug 1745781 + finish(); + } + + /** + * In most cases, when the dial button is pressed, there is a + * number in digits area. Pack it in the intent, start the + * outgoing call broadcast as a separate task and finish this + * activity. + * + * When there is no digit and the phone is CDMA and off hook, + * we're sending a blank flash for CDMA. CDMA networks use Flash + * messages when special processing needs to be done, mainly for + * 3-way or call waiting scenarios. Presumably, here we're in a + * special 3-way scenario where the network needs a blank flash + * before being able to add the new participant. (This is not the + * case with all 3-way calls, just certain CDMA infrastructures.) + * + * Otherwise, there is no digit, display the last dialed + * number. Don't finish since the user may want to edit it. The + * user needs to press the dial button again, to dial it (general + * case described above). + */ + void dialButtonPressed() { + if (isDigitsEmpty()) { // No number entered. + if (phoneIsCdma() && phoneIsOffhook()) { + // This is really CDMA specific. On GSM is it possible + // to be off hook and wanted to add a 3rd party using + // the redial feature. + startActivity(newFlashIntent()); + } else { + if (!TextUtils.isEmpty(mLastNumberDialed)) { + mDigits.setText(mLastNumberDialed); + } else { + // There's no "last number dialed" or the + // background query is still running. There's + // nothing useful for the Dial button to do in + // this case. Note: with a soft dial button, this + // can never happens since the dial button is + // disabled under these conditons. + playTone(ToneGenerator.TONE_PROP_NACK); + } + } + } else { + final String number = mDigits.getText().toString(); + + startActivity(newDialNumberIntent(number)); + mDigits.getText().clear(); // TODO: Fix bug 1745781 + finish(); + } + } + + + /** + * Plays the specified tone for TONE_LENGTH_MS milliseconds. + * + * The tone is played locally, using the audio stream for phone calls. + * Tones are played only if the "Audible touch tones" user preference + * is checked, and are NOT played if the device is in silent mode. + * + * @param tone a tone code from {@link ToneGenerator} + */ + void playTone(int tone) { + // if local tone playback is disabled, just return. + if (!mDTMFToneEnabled) { + return; + } + + // Also do nothing if the phone is in silent mode. + // We need to re-check the ringer mode for *every* playTone() + // call, rather than keeping a local flag that's updated in + // onResume(), since it's possible to toggle silent mode without + // leaving the current activity (via the ENDCALL-longpress menu.) + AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + int ringerMode = audioManager.getRingerMode(); + if ((ringerMode == AudioManager.RINGER_MODE_SILENT) + || (ringerMode == AudioManager.RINGER_MODE_VIBRATE)) { + return; + } + + synchronized(mToneGeneratorLock) { + if (mToneGenerator == null) { + Log.w(TAG, "playTone: mToneGenerator == null, tone: "+tone); + return; + } + + // Start the new tone (will stop any playing tone) + mToneGenerator.startTone(tone, TONE_LENGTH_MS); + } + } + + /** + * Brings up the "dialpad chooser" UI in place of the usual Dialer + * elements (the textfield/button and the dialpad underneath). + * + * We show this UI if the user brings up the Dialer while a call is + * already in progress, since there's a good chance we got here + * accidentally (and the user really wanted the in-call dialpad instead). + * So in this situation we display an intermediate UI that lets the user + * explicitly choose between the in-call dialpad ("Use touch tone + * keypad") and the regular Dialer ("Add call"). (Or, the option "Return + * to call in progress" just goes back to the in-call UI with no dialpad + * at all.) + * + * @param enabled If true, show the "dialpad chooser" instead + * of the regular Dialer UI + */ + private void showDialpadChooser(boolean enabled) { + if (enabled) { + // Log.i(TAG, "Showing dialpad chooser!"); + mDigits.setVisibility(View.GONE); + if (mDialpad != null) mDialpad.setVisibility(View.GONE); + mVoicemailDialAndDeleteRow.setVisibility(View.GONE); + mDialpadChooser.setVisibility(View.VISIBLE); + + // Instantiate the DialpadChooserAdapter and hook it up to the + // ListView. We do this only once. + if (mDialpadChooserAdapter == null) { + mDialpadChooserAdapter = new DialpadChooserAdapter(this); + mDialpadChooser.setAdapter(mDialpadChooserAdapter); + } + } else { + // Log.i(TAG, "Displaying normal Dialer UI."); + mDigits.setVisibility(View.VISIBLE); + if (mDialpad != null) mDialpad.setVisibility(View.VISIBLE); + mVoicemailDialAndDeleteRow.setVisibility(View.VISIBLE); + mDialpadChooser.setVisibility(View.GONE); + } + } + + /** + * @return true if we're currently showing the "dialpad chooser" UI. + */ + private boolean dialpadChooserVisible() { + return mDialpadChooser.getVisibility() == View.VISIBLE; + } + + /** + * Simple list adapter, binding to an icon + text label + * for each item in the "dialpad chooser" list. + */ + private static class DialpadChooserAdapter extends BaseAdapter { + private LayoutInflater mInflater; + + // Simple struct for a single "choice" item. + static class ChoiceItem { + String text; + Bitmap icon; + int id; + + public ChoiceItem(String s, Bitmap b, int i) { + text = s; + icon = b; + id = i; + } + } + + // IDs for the possible "choices": + static final int DIALPAD_CHOICE_USE_DTMF_DIALPAD = 101; + static final int DIALPAD_CHOICE_RETURN_TO_CALL = 102; + static final int DIALPAD_CHOICE_ADD_NEW_CALL = 103; + + private static final int NUM_ITEMS = 3; + private ChoiceItem mChoiceItems[] = new ChoiceItem[NUM_ITEMS]; + + public DialpadChooserAdapter(Context context) { + // Cache the LayoutInflate to avoid asking for a new one each time. + mInflater = LayoutInflater.from(context); + + // Initialize the possible choices. + // TODO: could this be specified entirely in XML? + + // - "Use touch tone keypad" + mChoiceItems[0] = new ChoiceItem( + context.getString(R.string.dialer_useDtmfDialpad), + BitmapFactory.decodeResource(context.getResources(), + R.drawable.ic_dialer_fork_tt_keypad), + DIALPAD_CHOICE_USE_DTMF_DIALPAD); + + // - "Return to call in progress" + mChoiceItems[1] = new ChoiceItem( + context.getString(R.string.dialer_returnToInCallScreen), + BitmapFactory.decodeResource(context.getResources(), + R.drawable.ic_dialer_fork_current_call), + DIALPAD_CHOICE_RETURN_TO_CALL); + + // - "Add call" + mChoiceItems[2] = new ChoiceItem( + context.getString(R.string.dialer_addAnotherCall), + BitmapFactory.decodeResource(context.getResources(), + R.drawable.ic_dialer_fork_add_call), + DIALPAD_CHOICE_ADD_NEW_CALL); + } + + public int getCount() { + return NUM_ITEMS; + } + + /** + * Return the ChoiceItem for a given position. + */ + public Object getItem(int position) { + return mChoiceItems[position]; + } + + /** + * Return a unique ID for each possible choice. + */ + public long getItemId(int position) { + return position; + } + + /** + * Make a view for each row. + */ + public View getView(int position, View convertView, ViewGroup parent) { + // When convertView is non-null, we can reuse it (there's no need + // to reinflate it.) + if (convertView == null) { + convertView = mInflater.inflate(R.layout.dialpad_chooser_list_item, null); + } + + TextView text = (TextView) convertView.findViewById(R.id.text); + text.setText(mChoiceItems[position].text); + + ImageView icon = (ImageView) convertView.findViewById(R.id.icon); + icon.setImageBitmap(mChoiceItems[position].icon); + + return convertView; + } + } + + /** + * Handle clicks from the dialpad chooser. + */ + public void onItemClick(AdapterView parent, View v, int position, long id) { + DialpadChooserAdapter.ChoiceItem item = + (DialpadChooserAdapter.ChoiceItem) parent.getItemAtPosition(position); + int itemId = item.id; + switch (itemId) { + case DialpadChooserAdapter.DIALPAD_CHOICE_USE_DTMF_DIALPAD: + // Log.i(TAG, "DIALPAD_CHOICE_USE_DTMF_DIALPAD"); + // Fire off an intent to go back to the in-call UI + // with the dialpad visible. + returnToInCallScreen(true); + break; + + case DialpadChooserAdapter.DIALPAD_CHOICE_RETURN_TO_CALL: + // Log.i(TAG, "DIALPAD_CHOICE_RETURN_TO_CALL"); + // Fire off an intent to go back to the in-call UI + // (with the dialpad hidden). + returnToInCallScreen(false); + break; + + case DialpadChooserAdapter.DIALPAD_CHOICE_ADD_NEW_CALL: + // Log.i(TAG, "DIALPAD_CHOICE_ADD_NEW_CALL"); + // Ok, guess the user really did want to be here (in the + // regular Dialer) after all. Bring back the normal Dialer UI. + showDialpadChooser(false); + break; + + default: + Log.w(TAG, "onItemClick: unexpected itemId: " + itemId); + break; + } + } + + /** + * Returns to the in-call UI (where there's presumably a call in + * progress) in response to the user selecting "use touch tone keypad" + * or "return to call" from the dialpad chooser. + */ + private void returnToInCallScreen(boolean showDialpad) { + try { + ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); + if (phone != null) phone.showCallScreenWithDialpad(showDialpad); + } catch (RemoteException e) { + Log.w(TAG, "phone.showCallScreenWithDialpad() failed", e); + } + + // Finally, finish() ourselves so that we don't stay on the + // activity stack. + // Note that we do this whether or not the showCallScreenWithDialpad() + // call above had any effect or not! (That call is a no-op if the + // phone is idle, which can happen if the current call ends while + // the dialpad chooser is up. In this case we can't show the + // InCallScreen, and there's no point staying here in the Dialer, + // so we just take the user back where he came from...) + finish(); + } + + /** + * @return true if the phone is "in use", meaning that at least one line + * is active (ie. off hook or ringing or dialing). + */ + private boolean phoneIsInUse() { + boolean phoneInUse = false; + try { + ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); + if (phone != null) phoneInUse = !phone.isIdle(); + } catch (RemoteException e) { + Log.w(TAG, "phone.isIdle() failed", e); + } + return phoneInUse; + } + + /** + * @return true if the phone is a CDMA phone type + */ + private boolean phoneIsCdma() { + boolean isCdma = false; + try { + ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); + if (phone != null) { + isCdma = (phone.getActivePhoneType() == TelephonyManager.PHONE_TYPE_CDMA); + } + } catch (RemoteException e) { + Log.w(TAG, "phone.getActivePhoneType() failed", e); + } + return isCdma; + } + + /** + * @return true if the phone state is OFFHOOK + */ + private boolean phoneIsOffhook() { + boolean phoneOffhook = false; + try { + ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); + if (phone != null) phoneOffhook = phone.isOffhook(); + } catch (RemoteException e) { + Log.w(TAG, "phone.isOffhook() failed", e); + } + return phoneOffhook; + } + + + /** + * Returns true whenever any one of the options from the menu is selected. + * Code changes to support dialpad options + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case MENU_2S_PAUSE: + updateDialString(","); + return true; + case MENU_WAIT: + updateDialString(";"); + return true; + } + return false; + } + + /** + * Updates the dial string (mDigits) after inserting a Pause character (,) + * or Wait character (;). + */ + private void updateDialString(String newDigits) { + int selectionStart; + int selectionEnd; + + // SpannableStringBuilder editable_text = new SpannableStringBuilder(mDigits.getText()); + int anchor = mDigits.getSelectionStart(); + int point = mDigits.getSelectionEnd(); + + selectionStart = Math.min(anchor, point); + selectionEnd = Math.max(anchor, point); + + Editable digits = mDigits.getText(); + if (selectionStart != -1 ) { + if (selectionStart == selectionEnd) { + // then there is no selection. So insert the pause at this + // position and update the mDigits. + digits.replace(selectionStart, selectionStart, newDigits); + } else { + digits.replace(selectionStart, selectionEnd, newDigits); + // Unselect: back to a regular cursor, just pass the character inserted. + mDigits.setSelection(selectionStart + 1); + } + } else { + int len = mDigits.length(); + digits.replace(len, len, newDigits); + } + } + + /** + * Update the enabledness of the "Dial" and "Backspace" buttons if applicable. + */ + private void updateDialAndDeleteButtonEnabledState() { + final boolean digitsNotEmpty = !isDigitsEmpty(); + + if (mDialButton != null) { + // On CDMA phones, if we're already on a call, we *always* + // enable the Dial button (since you can press it without + // entering any digits to send an empty flash.) + if (phoneIsCdma() && phoneIsOffhook()) { + mDialButton.setEnabled(true); + } else { + // Common case: GSM, or CDMA but not on a call. + // Enable the Dial button if some digits have + // been entered, or if there is a last dialed number + // that could be redialed. + mDialButton.setEnabled(digitsNotEmpty || + !TextUtils.isEmpty(mLastNumberDialed)); + } + } + mDelete.setEnabled(digitsNotEmpty); + } + + + /** + * Check if voicemail is enabled/accessible. + */ + private void initVoicemailButton() { + boolean hasVoicemail = false; + try { + hasVoicemail = TelephonyManager.getDefault().getVoiceMailNumber() != null; + } catch (SecurityException se) { + // Possibly no READ_PHONE_STATE privilege. + } + + mVoicemailButton = mVoicemailDialAndDeleteRow.findViewById(R.id.voicemailButton); + if (hasVoicemail) { + mVoicemailButton.setOnClickListener(this); + } else { + mVoicemailButton.setEnabled(false); + } + } + + /** + * This function return true if Wait menu item can be shown + * otherwise returns false. Assumes the passed string is non-empty + * and the 0th index check is not required. + */ + private boolean showWait(int start, int end, String digits) { + if (start == end) { + // visible false in this case + if (start > digits.length()) return false; + + // preceding char is ';', so visible should be false + if (digits.charAt(start-1) == ';') return false; + + // next char is ';', so visible should be false + if ((digits.length() > start) && (digits.charAt(start) == ';')) return false; + } else { + // visible false in this case + if (start > digits.length() || end > digits.length()) return false; + + // In this case we need to just check for ';' preceding to start + // or next to end + if (digits.charAt(start-1) == ';') return false; + } + return true; + } + + /** + * @return true if the widget with the phone number digits is empty. + */ + private boolean isDigitsEmpty() { + return mDigits.length() == 0; + } + + /** + * Starts the asyn query to get the last dialed/outgoing + * number. When the background query finishes, mLastNumberDialed + * is set to the last dialed number or an empty string if none + * exists yet. + */ + private void queryLastOutgoingCall() { + mLastNumberDialed = EMPTY_NUMBER; + CallLogAsync.GetLastOutgoingCallArgs lastCallArgs = + new CallLogAsync.GetLastOutgoingCallArgs( + this, + new CallLogAsync.OnLastOutgoingCallComplete() { + public void lastOutgoingCall(String number) { + // TODO: Filter out emergency numbers if + // the carrier does not want redial for + // these. + mLastNumberDialed = number; + updateDialAndDeleteButtonEnabledState(); + } + }); + mCallLog.getLastOutgoingCall(lastCallArgs); + } + + @Override + public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, + boolean globalSearch) { + if (globalSearch) { + super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); + } else { + ContactsSearchManager.startSearch(this, initialQuery); + } + } + + // Helpers for the call intents. + private Intent newVoicemailIntent() { + final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, + Uri.fromParts("voicemail", EMPTY_NUMBER, null)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return intent; + } + + private Intent newFlashIntent() { + final Intent intent = newDialNumberIntent(EMPTY_NUMBER); + intent.putExtra(EXTRA_SEND_EMPTY_FLASH, true); + return intent; + } + + private Intent newDialNumberIntent(String number) { + final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, + Uri.fromParts("tel", number, null)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return intent; + } +} diff --git a/src/com/android/contacts/activities/DialtactsActivity.java b/src/com/android/contacts/activities/DialtactsActivity.java new file mode 100644 index 000000000..505430956 --- /dev/null +++ b/src/com/android/contacts/activities/DialtactsActivity.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2008 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.activities; + +import com.android.contacts.R; +import com.android.contacts.activities.ContactsFrontDoor; +import com.android.contacts.activities.ContactBrowserActivity; +import com.android.contacts.activities.DialpadActivity; +import com.android.internal.telephony.ITelephony; + +import android.app.Activity; +import android.app.TabActivity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract.Intents.UI; +import android.util.Log; +import android.view.Window; +import android.widget.TabHost; + +/** + * The dialer activity that has one tab with the virtual 12key + * dialer, a tab with recent calls in it, a tab with the contacts and + * a tab with the favorite. This is the container and the tabs are + * embedded using intents. + * The dialer tab's title is 'phone', a more common name (see strings.xml). + */ +public class DialtactsActivity extends TabActivity implements TabHost.OnTabChangeListener { + private static final String TAG = "DialtactsActivity"; + + private static final int TAB_INDEX_DIALER = 0; + private static final int TAB_INDEX_CALL_LOG = 1; + private static final int TAB_INDEX_CONTACTS = 2; + private static final int TAB_INDEX_FAVORITES = 3; + + static final String EXTRA_IGNORE_STATE = "ignore-state"; + + /** Name of the dialtacts shared preferences */ + static final String PREFS_DIALTACTS = "dialtacts"; + /** If true, when handling the contacts intent the favorites tab will be shown instead */ + static final String PREF_FAVORITES_AS_CONTACTS = "favorites_as_contacts"; + static final boolean PREF_FAVORITES_AS_CONTACTS_DEFAULT = false; + + /** Last manually selected tab index */ + private static final String PREF_LAST_MANUALLY_SELECTED_TAB = "last_manually_selected_tab"; + private static final int PREF_LAST_MANUALLY_SELECTED_TAB_DEFAULT = TAB_INDEX_DIALER; + + private TabHost mTabHost; + private String mFilterText; + private Uri mDialUri; + + /** + * The index of the tab that has last been manually selected (the user clicked on a tab). + * This value does not keep track of programmatically set Tabs (e.g. Call Log after a Call) + */ + private int mLastManuallySelectedTab; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + final Intent intent = getIntent(); + fixIntent(intent); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.dialtacts_activity); + + mTabHost = getTabHost(); + mTabHost.setOnTabChangedListener(this); + + // Setup the tabs + setupDialerTab(); + setupCallLogTab(); + setupContactsTab(); + setupFavoritesTab(); + + // Load the last manually loaded tab + final SharedPreferences prefs = getSharedPreferences(PREFS_DIALTACTS, MODE_PRIVATE); + mLastManuallySelectedTab = prefs.getInt(PREF_LAST_MANUALLY_SELECTED_TAB, + PREF_LAST_MANUALLY_SELECTED_TAB_DEFAULT); + + setCurrentTab(intent); + + if (UI.FILTER_CONTACTS_ACTION.equals(intent.getAction()) + && icicle == null) { + setupFilterText(intent); + } + } + + @Override + protected void onPause() { + super.onPause(); + + final int currentTabIndex = mTabHost.getCurrentTab(); + final SharedPreferences.Editor editor = + getSharedPreferences(PREFS_DIALTACTS, MODE_PRIVATE).edit(); + if (currentTabIndex == TAB_INDEX_CONTACTS || currentTabIndex == TAB_INDEX_FAVORITES) { + editor.putBoolean(PREF_FAVORITES_AS_CONTACTS, currentTabIndex == TAB_INDEX_FAVORITES); + } + editor.putInt(PREF_LAST_MANUALLY_SELECTED_TAB, mLastManuallySelectedTab); + + editor.apply(); + } + + private void fixIntent(Intent intent) { + // This should be cleaned up: the call key used to send an Intent + // that just said to go to the recent calls list. It now sends this + // abstract action, but this class hasn't been rewritten to deal with it. + if (Intent.ACTION_CALL_BUTTON.equals(intent.getAction())) { + intent.setDataAndType(Calls.CONTENT_URI, Calls.CONTENT_TYPE); + intent.putExtra("call_key", true); + setIntent(intent); + } + } + + private void setupCallLogTab() { + // Force the class since overriding tab entries doesn't work + Intent intent = new Intent("com.android.phone.action.RECENT_CALLS"); + intent.setClass(this, CallLogActivity.class); + + mTabHost.addTab(mTabHost.newTabSpec("call_log") + .setIndicator(getString(R.string.recentCallsIconLabel), + getResources().getDrawable(R.drawable.ic_tab_recent)) + .setContent(intent)); + } + + private void setupDialerTab() { + Intent intent = new Intent("com.android.phone.action.TOUCH_DIALER"); + intent.setClass(this, DialpadActivity.class); + + mTabHost.addTab(mTabHost.newTabSpec("dialer") + .setIndicator(getString(R.string.dialerIconLabel), + getResources().getDrawable(R.drawable.ic_tab_dialer)) + .setContent(intent)); + } + + private void setupContactsTab() { + Intent intent = new Intent(UI.LIST_ALL_CONTACTS_ACTION); + intent.setClass(this, ContactBrowserActivity.class); + + mTabHost.addTab(mTabHost.newTabSpec("contacts") + .setIndicator(getText(R.string.contactsIconLabel), + getResources().getDrawable(R.drawable.ic_tab_contacts)) + .setContent(intent)); + } + + private void setupFavoritesTab() { + Intent intent = new Intent(UI.LIST_STREQUENT_ACTION); + intent.setClass(this, ContactBrowserActivity.class); + + mTabHost.addTab(mTabHost.newTabSpec("favorites") + .setIndicator(getString(R.string.contactsFavoritesLabel), + getResources().getDrawable(R.drawable.ic_tab_starred)) + .setContent(intent)); + } + + /** + * Returns true if the intent is due to hitting the green send key while in a call. + * + * @param intent the intent that launched this activity + * @param recentCallsRequest true if the intent is requesting to view recent calls + * @return true if the intent is due to hitting the green send key while in a call + */ + private boolean isSendKeyWhileInCall(final Intent intent, final boolean recentCallsRequest) { + // If there is a call in progress go to the call screen + if (recentCallsRequest) { + final boolean callKey = intent.getBooleanExtra("call_key", false); + + try { + ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); + if (callKey && phone != null && phone.showCallScreen()) { + return true; + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to handle send while in call", e); + } + } + + return false; + } + + /** + * Sets the current tab based on the intent's request type + * + * @param intent Intent that contains information about which tab should be selected + */ + private void setCurrentTab(Intent intent) { + // If we got here by hitting send and we're in call forward along to the in-call activity + final boolean recentCallsRequest = Calls.CONTENT_TYPE.equals(intent.getType()); + if (isSendKeyWhileInCall(intent, recentCallsRequest)) { + finish(); + return; + } + + // Dismiss menu provided by any children activities + Activity activity = getLocalActivityManager(). + getActivity(mTabHost.getCurrentTabTag()); + if (activity != null) { + activity.closeOptionsMenu(); + } + + // Tell the children activities that they should ignore any possible saved + // state and instead reload their state from the parent's intent + intent.putExtra(EXTRA_IGNORE_STATE, true); + + // Remember the old manually selected tab index so that it can be restored if it is + // overwritten by one of the programmatic tab selections + final int savedTabIndex = mLastManuallySelectedTab; + + // Choose the tab based on the inbound intent + if (intent.getBooleanExtra(ContactsFrontDoor.EXTRA_FRONT_DOOR, false)) { + // Launched through the contacts front door, set the proper contacts tab (sticky + // between favorites and contacts) + SharedPreferences prefs = getSharedPreferences(PREFS_DIALTACTS, MODE_PRIVATE); + boolean favoritesAsContacts = prefs.getBoolean(PREF_FAVORITES_AS_CONTACTS, + PREF_FAVORITES_AS_CONTACTS_DEFAULT); + if (favoritesAsContacts) { + mTabHost.setCurrentTab(TAB_INDEX_FAVORITES); + } else { + mTabHost.setCurrentTab(TAB_INDEX_CONTACTS); + } + } else { + // Not launched through the front door, look at the component to determine the tab + String componentName = intent.getComponent().getClassName(); + if (getClass().getName().equals(componentName)) { + if (recentCallsRequest) { + mTabHost.setCurrentTab(TAB_INDEX_CALL_LOG); + } else { + mTabHost.setCurrentTab(TAB_INDEX_DIALER); + } + } else { + mTabHost.setCurrentTab(mLastManuallySelectedTab); + } + } + + // Restore to the previous manual selection + mLastManuallySelectedTab = savedTabIndex; + + // Tell the children activities that they should honor their saved states + // instead of the state from the parent's intent + intent.putExtra(EXTRA_IGNORE_STATE, false); + } + + @Override + public void onNewIntent(Intent newIntent) { + setIntent(newIntent); + fixIntent(newIntent); + setCurrentTab(newIntent); + final String action = newIntent.getAction(); + if (UI.FILTER_CONTACTS_ACTION.equals(action)) { + setupFilterText(newIntent); + } else if (isDialIntent(newIntent)) { + setupDialUri(newIntent); + } + } + + /** Returns true if the given intent contains a phone number to populate the dialer with */ + private boolean isDialIntent(Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_DIAL.equals(action)) { + return true; + } + if (Intent.ACTION_VIEW.equals(action)) { + final Uri data = intent.getData(); + if (data != null && "tel".equals(data.getScheme())) { + return true; + } + } + return false; + } + + /** + * Retrieves the filter text stored in {@link #setupFilterText(Intent)}. + * This text originally came from a FILTER_CONTACTS_ACTION intent received + * by this activity. The stored text will then be cleared after after this + * method returns. + * + * @return The stored filter text + */ + public String getAndClearFilterText() { + String filterText = mFilterText; + mFilterText = null; + return filterText; + } + + /** + * Stores the filter text associated with a FILTER_CONTACTS_ACTION intent. + * This is so child activities can check if they are supposed to display a filter. + * + * @param intent The intent received in {@link #onNewIntent(Intent)} + */ + private void setupFilterText(Intent intent) { + // If the intent was relaunched from history, don't apply the filter text. + if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { + return; + } + String filter = intent.getStringExtra(UI.FILTER_TEXT_EXTRA_KEY); + if (filter != null && filter.length() > 0) { + mFilterText = filter; + } + } + + /** + * Retrieves the uri stored in {@link #setupDialUri(Intent)}. This uri + * originally came from a dial intent received by this activity. The stored + * uri will then be cleared after after this method returns. + * + * @return The stored uri + */ + public Uri getAndClearDialUri() { + Uri dialUri = mDialUri; + mDialUri = null; + return dialUri; + } + + /** + * Stores the uri associated with a dial intent. This is so child activities can + * check if they are supposed to display new dial info. + * + * @param intent The intent received in {@link #onNewIntent(Intent)} + */ + private void setupDialUri(Intent intent) { + // If the intent was relaunched from history, don't reapply the intent. + if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { + return; + } + mDialUri = intent.getData(); + } + + @Override + public void onBackPressed() { + if (isTaskRoot()) { + // Instead of stopping, simply push this to the back of the stack. + // This is only done when running at the top of the stack; + // otherwise, we have been launched by someone else so need to + // allow the user to go back to the caller. + moveTaskToBack(false); + } else { + super.onBackPressed(); + } + } + + /** {@inheritDoc} */ + public void onTabChanged(String tabId) { + // Because we're using Activities as our tab children, we trigger + // onWindowFocusChanged() to let them know when they're active. This may + // seem to duplicate the purpose of onResume(), but it's needed because + // onResume() can't reliably check if a keyguard is active. + Activity activity = getLocalActivityManager().getActivity(tabId); + if (activity != null) { + activity.onWindowFocusChanged(true); + } + + // Remember this tab index. This function is also called, if the tab is set automatically + // in which case the setter (setCurrentTab) has to set this to its old value afterwards + mLastManuallySelectedTab = mTabHost.getCurrentTab(); + } +} diff --git a/tests/src/com/android/contacts/DialerLaunchPerformance.java b/tests/src/com/android/contacts/DialerLaunchPerformance.java index ae780828d..0803c6b1a 100644 --- a/tests/src/com/android/contacts/DialerLaunchPerformance.java +++ b/tests/src/com/android/contacts/DialerLaunchPerformance.java @@ -32,7 +32,7 @@ public class DialerLaunchPerformance extends LaunchPerformanceBase { mIntent.setAction(Intent.ACTION_MAIN); mIntent.addCategory(Intent.CATEGORY_LAUNCHER); mIntent.setComponent(new ComponentName("com.android.contacts", - "com.android.contacts.DialtactsActivity")); + "com.android.contacts.activities.DialtactsActivity")); start(); } diff --git a/tests/src/com/android/contacts/RecentCallsListActivityTests.java b/tests/src/com/android/contacts/RecentCallsListActivityTests.java deleted file mode 100644 index 3c2a9fe42..000000000 --- a/tests/src/com/android/contacts/RecentCallsListActivityTests.java +++ /dev/null @@ -1,358 +0,0 @@ -/* - * Copyright (C) 2009 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; - -import android.content.res.Resources; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.provider.CallLog.Calls; -import android.test.ActivityInstrumentationTestCase2; -import android.test.suitebuilder.annotation.MediumTest; -import android.util.Log; -import android.view.View; -import android.widget.FrameLayout; -import com.android.contacts.RecentCallsListActivity; -import com.android.internal.telephony.CallerInfo; -import java.util.Date; -import java.util.Formatter; -import java.util.HashMap; -import java.util.Random; - -/** - * Tests for the contact call list activity. - * - * Running all tests: - * - * runtest contacts - * or - * adb shell am instrument \ - * -w com.android.contacts.tests/android.test.InstrumentationTestRunner - */ - -public class RecentCallsListActivityTests - extends ActivityInstrumentationTestCase2 { - static private final String TAG = "RecentCallsListActivityTests"; - static private final String[] CALL_LOG_PROJECTION = new String[] { - Calls._ID, - Calls.NUMBER, - Calls.DATE, - Calls.DURATION, - Calls.TYPE, - Calls.CACHED_NAME, - Calls.CACHED_NUMBER_TYPE, - Calls.CACHED_NUMBER_LABEL, - Calls.COUNTRY_ISO, - }; - static private final int RAND_DURATION = -1; - static private final long NOW = -1L; - - // We get the call list activity and assign is a frame to build - // its list. mAdapter is an inner class of - // RecentCallsListActivity to build the rows (view) in the call - // list. We reuse it with our own in-mem DB. - private RecentCallsListActivity mActivity; - private FrameLayout mParentView; - private RecentCallsListActivity.RecentCallsAdapter mAdapter; - private String mVoicemail; - - // In memory array to hold the rows corresponding to the 'calls' table. - private MatrixCursor mCursor; - private int mIndex; // Of the next row. - - private Random mRnd; - - // References to the icons bitmaps used to build the list are stored in a - // map mIcons. The keys to retrieve the icons are: - // Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE and Calls.MISSED_TYPE. - private HashMap mCallTypeIcons; - - // An item in the call list. All the methods performing checks use it. - private RecentCallsListActivity.RecentCallsListItemViews mItem; - // The list of views representing the data in the DB. View are in - // reverse order compare to the DB. - private View[] mList; - - public RecentCallsListActivityTests() { - super("com.android.contacts", RecentCallsListActivity.class); - mIndex = 1; - mRnd = new Random(); - } - - @Override - public void setUp() { - mActivity = getActivity(); - mVoicemail = mActivity.mVoiceMailNumber; - mAdapter = mActivity.mAdapter; - mParentView = new FrameLayout(mActivity); - mCursor = new MatrixCursor(CALL_LOG_PROJECTION); - buildIconMap(); - } - - /** - * Checks that the call icon is not visible for private and - * unknown numbers. - * Use 2 passes, one where new views are created and one where - * half of the total views are updated and the other half created. - */ - @MediumTest - public void testCallViewIsNotVisibleForPrivateAndUnknownNumbers() { - final int SIZE = 100; - mList = new View[SIZE]; - - // Insert the first batch of entries. - mCursor.moveToFirst(); - insertRandomEntries(SIZE / 2); - int startOfSecondBatch = mCursor.getPosition(); - - buildViewListFromDb(); - checkCallStatus(); - - // Append the rest of the entries. We keep the first set of - // views around so they get updated and not built from - // scratch, this exposes some bugs that are not there when the - // call log is launched for the 1st time but show up when the - // call log gets updated afterwards. - mCursor.move(startOfSecondBatch); - insertRandomEntries(SIZE / 2); - - buildViewListFromDb(); - checkCallStatus(); - } - - // - // HELPERS to check conditions on the DB/views - // - /** - * Check the date of the current list item. - * @param date That should be present in the call log list - * item. Only NOW is supported. - */ - private void checkDate(long date) { - if (NOW == date) { - assertEquals("0 mins ago", mItem.dateView.getText()); - } - throw new UnsupportedOperationException(); - } - - /** - * Checks the right icon is used to represent the call type - * (missed, incoming, outgoing.) in the current item. - */ - private void checkCallType(int type) { - Bitmap icon = ((BitmapDrawable) mItem.iconView.getDrawable()).getBitmap(); - assertEquals(mCallTypeIcons.get(type), icon); - } - - /** - * Go over all the views in the list and check that the Call - * icon's visibility matches the nature of the number. - */ - private void checkCallStatus() { - for (int i = 0; i < mList.length; i++) { - if (null == mList[i]) { - break; - } - mItem = (RecentCallsListActivity.RecentCallsListItemViews) mList[i].getTag(); - - // callView tag is the phone number. - String number = (String) mItem.callView.getTag(); - - if (CallerInfo.PRIVATE_NUMBER.equals(number) || - CallerInfo.UNKNOWN_NUMBER.equals(number)) { - assertFalse(View.VISIBLE == mItem.callView.getVisibility()); - } else { - assertEquals(View.VISIBLE, mItem.callView.getVisibility()); - } - } - } - - - // - // HELPERS to setup the tests. - // - - /** - * Get the Bitmap from the icons in the contacts package. - */ - private Bitmap getBitmap(String resName) { - Resources r = mActivity.getResources(); - int resid = r.getIdentifier(resName, "drawable", "com.android.contacts"); - BitmapDrawable d = (BitmapDrawable) r.getDrawable(resid); - assertNotNull(d); - return d.getBitmap(); - } - - /** - * Fetch all the icons we need in tests from the contacts app and store them in a map. - */ - private void buildIconMap() { - mCallTypeIcons = new HashMap(3); - - mCallTypeIcons.put(Calls.INCOMING_TYPE, getBitmap("ic_call_log_list_incoming_call")); - mCallTypeIcons.put(Calls.MISSED_TYPE, getBitmap("ic_call_log_list_missed_call")); - mCallTypeIcons.put(Calls.OUTGOING_TYPE, getBitmap("ic_call_log_list_outgoing_call")); - } - - // - // HELPERS to build/update the call entries (views) from the DB. - // - - /** - * Read the DB and foreach call either update the existing view if - * one exists already otherwise create one. - * The list is build from a DESC view of the DB (last inserted entry is first). - */ - private void buildViewListFromDb() { - int i = 0; - mCursor.moveToLast(); - while(!mCursor.isBeforeFirst()) { - if (null == mList[i]) { - mList[i] = mAdapter.newStandAloneView(mActivity, mParentView); - } - mAdapter.bindStandAloneView(mList[i], mActivity, mCursor); - mCursor.moveToPrevious(); - i++; - } - } - - // - // HELPERS to insert numbers in the call log DB. - // - - /** - * Insert a certain number of random numbers in the DB. Makes sure - * there is at least one private and one unknown number in the DB. - * @param num Of entries to be inserted. - */ - private void insertRandomEntries(int num) { - if (num < 10) { - throw new IllegalArgumentException("num should be >= 10"); - } - boolean privateOrUnknownOrVm[]; - privateOrUnknownOrVm = insertRandomRange(0, num - 2); - - if (privateOrUnknownOrVm[0] && privateOrUnknownOrVm[1]) { - insertRandomRange(num - 2, num); - } else { - insertPrivate(NOW, RAND_DURATION); - insertUnknown(NOW, RAND_DURATION); - } - } - - /** - * Insert a new call entry in the test DB. - * @param number The phone number. For unknown and private numbers, - * use CallerInfo.UNKNOWN_NUMBER or CallerInfo.PRIVATE_NUMBER. - * @param date In millisec since epoch. Use NOW to use the current time. - * @param duration In seconds of the call. Use RAND_DURATION to pick a random one. - * @param type Eigher Call.OUTGOING_TYPE or Call.INCOMING_TYPE or Call.MISSED_TYPE. - */ - private void insert(String number, long date, int duration, int type) { - MatrixCursor.RowBuilder row = mCursor.newRow(); - row.add(mIndex); - mIndex ++; - row.add(number); - if (NOW == date) { - row.add(new Date().getTime()); - } - if (duration < 0) { - duration = mRnd.nextInt(10 * 60); // 0 - 10 minutes random. - } - row.add(duration); // duration - if (mVoicemail != null && mVoicemail.equals(number)) { - assertEquals(Calls.OUTGOING_TYPE, type); - } - row.add(type); // type - row.add(""); // cached name - row.add(0); // cached number type - row.add(""); // cached number label - row.add("US"); // country ISO - } - - /** - * Insert a new private call entry in the test DB. - * @param date In millisec since epoch. Use NOW to use the current time. - * @param duration In seconds of the call. Use RAND_DURATION to pick a random one. - */ - private void insertPrivate(long date, int duration) { - insert(CallerInfo.PRIVATE_NUMBER, date, duration, Calls.INCOMING_TYPE); - } - - /** - * Insert a new unknown call entry in the test DB. - * @param date In millisec since epoch. Use NOW to use the current time. - * @param duration In seconds of the call. Use RAND_DURATION to pick a random one. - */ - private void insertUnknown(long date, int duration) { - insert(CallerInfo.UNKNOWN_NUMBER, date, duration, Calls.INCOMING_TYPE); - } - - /** - * Insert a new voicemail call entry in the test DB. - * @param date In millisec since epoch. Use NOW to use the current time. - * @param duration In seconds of the call. Use RAND_DURATION to pick a random one. - */ - private void insertVoicemail(long date, int duration) { - // mVoicemail may be null - if (mVoicemail != null) { - insert(mVoicemail, date, duration, Calls.OUTGOING_TYPE); - } - } - - /** - * Insert a range [start, end) of random numbers in the DB. For - * each row, there is a 1/10 probability that the number will be - * marked as PRIVATE or UNKNOWN or VOICEMAIL. For regular numbers, a number is - * inserted, its last 4 digits will be the number of the iteration - * in the range. - * @param start Of the range. - * @param end Of the range (excluded). - * @return An array with 2 booleans [0 = private number, 1 = - * unknown number, 2 = voicemail] to indicate if at least one - * private or unknown or voicemail number has been inserted. Since - * the numbers are random some tests may want to enforce the - * insertion of such numbers. - */ - // TODO: Should insert numbers with contact entries too. - private boolean[] insertRandomRange(int start, int end) { - boolean[] privateOrUnknownOrVm = new boolean[] {false, false, false}; - - for (int i = start; i < end; i++ ) { - int type = mRnd.nextInt(10); - - if (0 == type) { - insertPrivate(NOW, RAND_DURATION); - privateOrUnknownOrVm[0] = true; - } else if (1 == type) { - insertUnknown(NOW, RAND_DURATION); - privateOrUnknownOrVm[1] = true; - } else if (2 == type) { - insertVoicemail(NOW, RAND_DURATION); - privateOrUnknownOrVm[2] = true; - } else { - int inout = mRnd.nextBoolean() ? Calls.OUTGOING_TYPE : Calls.INCOMING_TYPE; - String number = new Formatter().format("1800123%04d", i).toString(); - insert(number, NOW, RAND_DURATION, inout); - } - } - return privateOrUnknownOrVm; - } -} diff --git a/tests/src/com/android/contacts/activities/CallLogActivityTests.java b/tests/src/com/android/contacts/activities/CallLogActivityTests.java new file mode 100644 index 000000000..c6b3ea5c3 --- /dev/null +++ b/tests/src/com/android/contacts/activities/CallLogActivityTests.java @@ -0,0 +1,355 @@ +/* + * Copyright (C) 2009 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.activities; + +import com.android.internal.telephony.CallerInfo; + +import android.content.res.Resources; +import android.database.MatrixCursor; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.provider.CallLog.Calls; +import android.test.ActivityInstrumentationTestCase2; +import android.test.suitebuilder.annotation.MediumTest; +import android.view.View; +import android.widget.FrameLayout; + +import java.util.Date; +import java.util.Formatter; +import java.util.HashMap; +import java.util.Random; + +/** + * Tests for the contact call list activity. + * + * Running all tests: + * + * runtest contacts + * or + * adb shell am instrument \ + * -w com.android.contacts.tests/android.test.InstrumentationTestRunner + */ + +public class CallLogActivityTests + extends ActivityInstrumentationTestCase2 { + static private final String TAG = "CallLogActivityTests"; + static private final String[] CALL_LOG_PROJECTION = new String[] { + Calls._ID, + Calls.NUMBER, + Calls.DATE, + Calls.DURATION, + Calls.TYPE, + Calls.CACHED_NAME, + Calls.CACHED_NUMBER_TYPE, + Calls.CACHED_NUMBER_LABEL, + Calls.COUNTRY_ISO, + }; + static private final int RAND_DURATION = -1; + static private final long NOW = -1L; + + // We get the call list activity and assign is a frame to build + // its list. mAdapter is an inner class of + // CallLogActivity to build the rows (view) in the call + // list. We reuse it with our own in-mem DB. + private CallLogActivity mActivity; + private FrameLayout mParentView; + private CallLogActivity.CallLogAdapter mAdapter; + private String mVoicemail; + + // In memory array to hold the rows corresponding to the 'calls' table. + private MatrixCursor mCursor; + private int mIndex; // Of the next row. + + private Random mRnd; + + // References to the icons bitmaps used to build the list are stored in a + // map mIcons. The keys to retrieve the icons are: + // Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE and Calls.MISSED_TYPE. + private HashMap mCallTypeIcons; + + // An item in the call list. All the methods performing checks use it. + private CallLogActivity.CallLogListItemViews mItem; + // The list of views representing the data in the DB. View are in + // reverse order compare to the DB. + private View[] mList; + + public CallLogActivityTests() { + super("com.android.contacts", CallLogActivity.class); + mIndex = 1; + mRnd = new Random(); + } + + @Override + public void setUp() { + mActivity = getActivity(); + mVoicemail = mActivity.mVoiceMailNumber; + mAdapter = mActivity.mAdapter; + mParentView = new FrameLayout(mActivity); + mCursor = new MatrixCursor(CALL_LOG_PROJECTION); + buildIconMap(); + } + + /** + * Checks that the call icon is not visible for private and + * unknown numbers. + * Use 2 passes, one where new views are created and one where + * half of the total views are updated and the other half created. + */ + @MediumTest + public void testCallViewIsNotVisibleForPrivateAndUnknownNumbers() { + final int SIZE = 100; + mList = new View[SIZE]; + + // Insert the first batch of entries. + mCursor.moveToFirst(); + insertRandomEntries(SIZE / 2); + int startOfSecondBatch = mCursor.getPosition(); + + buildViewListFromDb(); + checkCallStatus(); + + // Append the rest of the entries. We keep the first set of + // views around so they get updated and not built from + // scratch, this exposes some bugs that are not there when the + // call log is launched for the 1st time but show up when the + // call log gets updated afterwards. + mCursor.move(startOfSecondBatch); + insertRandomEntries(SIZE / 2); + + buildViewListFromDb(); + checkCallStatus(); + } + + // + // HELPERS to check conditions on the DB/views + // + /** + * Check the date of the current list item. + * @param date That should be present in the call log list + * item. Only NOW is supported. + */ + private void checkDate(long date) { + if (NOW == date) { + assertEquals("0 mins ago", mItem.dateView.getText()); + } + throw new UnsupportedOperationException(); + } + + /** + * Checks the right icon is used to represent the call type + * (missed, incoming, outgoing.) in the current item. + */ + private void checkCallType(int type) { + Bitmap icon = ((BitmapDrawable) mItem.iconView.getDrawable()).getBitmap(); + assertEquals(mCallTypeIcons.get(type), icon); + } + + /** + * Go over all the views in the list and check that the Call + * icon's visibility matches the nature of the number. + */ + private void checkCallStatus() { + for (int i = 0; i < mList.length; i++) { + if (null == mList[i]) { + break; + } + mItem = (CallLogActivity.CallLogListItemViews) mList[i].getTag(); + + // callView tag is the phone number. + String number = (String) mItem.callView.getTag(); + + if (CallerInfo.PRIVATE_NUMBER.equals(number) || + CallerInfo.UNKNOWN_NUMBER.equals(number)) { + assertFalse(View.VISIBLE == mItem.callView.getVisibility()); + } else { + assertEquals(View.VISIBLE, mItem.callView.getVisibility()); + } + } + } + + + // + // HELPERS to setup the tests. + // + + /** + * Get the Bitmap from the icons in the contacts package. + */ + private Bitmap getBitmap(String resName) { + Resources r = mActivity.getResources(); + int resid = r.getIdentifier(resName, "drawable", "com.android.contacts"); + BitmapDrawable d = (BitmapDrawable) r.getDrawable(resid); + assertNotNull(d); + return d.getBitmap(); + } + + /** + * Fetch all the icons we need in tests from the contacts app and store them in a map. + */ + private void buildIconMap() { + mCallTypeIcons = new HashMap(3); + + mCallTypeIcons.put(Calls.INCOMING_TYPE, getBitmap("ic_call_log_list_incoming_call")); + mCallTypeIcons.put(Calls.MISSED_TYPE, getBitmap("ic_call_log_list_missed_call")); + mCallTypeIcons.put(Calls.OUTGOING_TYPE, getBitmap("ic_call_log_list_outgoing_call")); + } + + // + // HELPERS to build/update the call entries (views) from the DB. + // + + /** + * Read the DB and foreach call either update the existing view if + * one exists already otherwise create one. + * The list is build from a DESC view of the DB (last inserted entry is first). + */ + private void buildViewListFromDb() { + int i = 0; + mCursor.moveToLast(); + while(!mCursor.isBeforeFirst()) { + if (null == mList[i]) { + mList[i] = mAdapter.newStandAloneView(mActivity, mParentView); + } + mAdapter.bindStandAloneView(mList[i], mActivity, mCursor); + mCursor.moveToPrevious(); + i++; + } + } + + // + // HELPERS to insert numbers in the call log DB. + // + + /** + * Insert a certain number of random numbers in the DB. Makes sure + * there is at least one private and one unknown number in the DB. + * @param num Of entries to be inserted. + */ + private void insertRandomEntries(int num) { + if (num < 10) { + throw new IllegalArgumentException("num should be >= 10"); + } + boolean privateOrUnknownOrVm[]; + privateOrUnknownOrVm = insertRandomRange(0, num - 2); + + if (privateOrUnknownOrVm[0] && privateOrUnknownOrVm[1]) { + insertRandomRange(num - 2, num); + } else { + insertPrivate(NOW, RAND_DURATION); + insertUnknown(NOW, RAND_DURATION); + } + } + + /** + * Insert a new call entry in the test DB. + * @param number The phone number. For unknown and private numbers, + * use CallerInfo.UNKNOWN_NUMBER or CallerInfo.PRIVATE_NUMBER. + * @param date In millisec since epoch. Use NOW to use the current time. + * @param duration In seconds of the call. Use RAND_DURATION to pick a random one. + * @param type Eigher Call.OUTGOING_TYPE or Call.INCOMING_TYPE or Call.MISSED_TYPE. + */ + private void insert(String number, long date, int duration, int type) { + MatrixCursor.RowBuilder row = mCursor.newRow(); + row.add(mIndex); + mIndex ++; + row.add(number); + if (NOW == date) { + row.add(new Date().getTime()); + } + if (duration < 0) { + duration = mRnd.nextInt(10 * 60); // 0 - 10 minutes random. + } + row.add(duration); // duration + if (mVoicemail != null && mVoicemail.equals(number)) { + assertEquals(Calls.OUTGOING_TYPE, type); + } + row.add(type); // type + row.add(""); // cached name + row.add(0); // cached number type + row.add(""); // cached number label + row.add("US"); // country ISO + } + + /** + * Insert a new private call entry in the test DB. + * @param date In millisec since epoch. Use NOW to use the current time. + * @param duration In seconds of the call. Use RAND_DURATION to pick a random one. + */ + private void insertPrivate(long date, int duration) { + insert(CallerInfo.PRIVATE_NUMBER, date, duration, Calls.INCOMING_TYPE); + } + + /** + * Insert a new unknown call entry in the test DB. + * @param date In millisec since epoch. Use NOW to use the current time. + * @param duration In seconds of the call. Use RAND_DURATION to pick a random one. + */ + private void insertUnknown(long date, int duration) { + insert(CallerInfo.UNKNOWN_NUMBER, date, duration, Calls.INCOMING_TYPE); + } + + /** + * Insert a new voicemail call entry in the test DB. + * @param date In millisec since epoch. Use NOW to use the current time. + * @param duration In seconds of the call. Use RAND_DURATION to pick a random one. + */ + private void insertVoicemail(long date, int duration) { + // mVoicemail may be null + if (mVoicemail != null) { + insert(mVoicemail, date, duration, Calls.OUTGOING_TYPE); + } + } + + /** + * Insert a range [start, end) of random numbers in the DB. For + * each row, there is a 1/10 probability that the number will be + * marked as PRIVATE or UNKNOWN or VOICEMAIL. For regular numbers, a number is + * inserted, its last 4 digits will be the number of the iteration + * in the range. + * @param start Of the range. + * @param end Of the range (excluded). + * @return An array with 2 booleans [0 = private number, 1 = + * unknown number, 2 = voicemail] to indicate if at least one + * private or unknown or voicemail number has been inserted. Since + * the numbers are random some tests may want to enforce the + * insertion of such numbers. + */ + // TODO: Should insert numbers with contact entries too. + private boolean[] insertRandomRange(int start, int end) { + boolean[] privateOrUnknownOrVm = new boolean[] {false, false, false}; + + for (int i = start; i < end; i++ ) { + int type = mRnd.nextInt(10); + + if (0 == type) { + insertPrivate(NOW, RAND_DURATION); + privateOrUnknownOrVm[0] = true; + } else if (1 == type) { + insertUnknown(NOW, RAND_DURATION); + privateOrUnknownOrVm[1] = true; + } else if (2 == type) { + insertVoicemail(NOW, RAND_DURATION); + privateOrUnknownOrVm[2] = true; + } else { + int inout = mRnd.nextBoolean() ? Calls.OUTGOING_TYPE : Calls.INCOMING_TYPE; + String number = new Formatter().format("1800123%04d", i).toString(); + insert(number, NOW, RAND_DURATION, inout); + } + } + return privateOrUnknownOrVm; + } +} -- cgit v1.2.3