summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/com/android/dialer/BackScrollManager.java89
-rw-r--r--src/com/android/dialer/CallDetailActivity.java15
-rw-r--r--src/com/android/dialer/DialtactsActivity.java6
-rw-r--r--src/com/android/dialer/PhoneCallDetailsHelper.java2
-rw-r--r--src/com/android/dialer/ProximitySensorAware.java33
-rw-r--r--src/com/android/dialer/ProximitySensorManager.java237
-rw-r--r--src/com/android/dialer/SpecialCharSequenceMgr.java407
-rw-r--r--src/com/android/dialer/calllog/CallDetailHistoryAdapter.java2
-rw-r--r--src/com/android/dialer/calllog/CallLogAdapter.java4
-rw-r--r--src/com/android/dialer/calllog/CallLogFragment.java2
-rw-r--r--src/com/android/dialer/calllog/CallLogListItemHelper.java2
-rw-r--r--src/com/android/dialer/calllog/CallLogListItemViews.java2
-rw-r--r--src/com/android/dialer/calllog/DefaultVoicemailNotifier.java2
-rw-r--r--src/com/android/dialer/calllog/IntentProvider.java2
-rw-r--r--src/com/android/dialer/dialpad/DialpadFragment.java4
-rw-r--r--src/com/android/dialer/list/PhoneFavoriteFragment.java569
-rw-r--r--src/com/android/dialer/list/PhoneFavoriteMergedAdapter.java301
-rw-r--r--src/com/android/dialer/util/AsyncTaskExecutor.java48
-rw-r--r--src/com/android/dialer/util/AsyncTaskExecutors.java100
-rw-r--r--src/com/android/dialer/util/EmptyLoader.java60
-rw-r--r--src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java4
-rw-r--r--src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java2
-rw-r--r--tests/src/com/android/dialer/CallDetailActivityTest.java2
-rw-r--r--tests/src/com/android/dialer/util/FakeAsyncTaskExecutor.java2
24 files changed, 1868 insertions, 29 deletions
diff --git a/src/com/android/dialer/BackScrollManager.java b/src/com/android/dialer/BackScrollManager.java
new file mode 100644
index 000000000..57287022a
--- /dev/null
+++ b/src/com/android/dialer/BackScrollManager.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer;
+
+import android.view.View;
+import android.widget.AbsListView;
+import android.widget.ListView;
+
+/**
+ * Handles scrolling back of a list tied to a header.
+ * <p>
+ * This is used to implement a header that scrolls up with the content of a list to be partially
+ * obscured.
+ */
+public class BackScrollManager {
+ /** Defines the header to be scrolled. */
+ public interface ScrollableHeader {
+ /** Sets the offset by which to scroll. */
+ public void setOffset(int offset);
+ /** Gets the maximum offset that should be applied to the header. */
+ public int getMaximumScrollableHeaderOffset();
+ }
+
+ private final ScrollableHeader mHeader;
+ private final ListView mListView;
+
+ private final AbsListView.OnScrollListener mScrollListener =
+ new AbsListView.OnScrollListener() {
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+ if (firstVisibleItem != 0) {
+ // The first item is not shown, the header should be pinned at the top.
+ mHeader.setOffset(mHeader.getMaximumScrollableHeaderOffset());
+ return;
+ }
+
+ View firstVisibleItemView = view.getChildAt(firstVisibleItem);
+ if (firstVisibleItemView == null) {
+ return;
+ }
+ // We scroll the header up, but at most pin it to the top of the screen.
+ int offset = Math.min(
+ (int) -view.getChildAt(firstVisibleItem).getY(),
+ mHeader.getMaximumScrollableHeaderOffset());
+ mHeader.setOffset(offset);
+ }
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ // Nothing to do here.
+ }
+ };
+
+ /**
+ * Creates a new instance of a {@link BackScrollManager} that connected the header and the list
+ * view.
+ */
+ public static void bind(ScrollableHeader header, ListView listView) {
+ BackScrollManager backScrollManager = new BackScrollManager(header, listView);
+ backScrollManager.bind();
+ }
+
+ private BackScrollManager(ScrollableHeader header, ListView listView) {
+ mHeader = header;
+ mListView = listView;
+ }
+
+ private void bind() {
+ mListView.setOnScrollListener(mScrollListener);
+ // We disable the scroll bar because it would otherwise be incorrect because of the hidden
+ // header.
+ mListView.setVerticalScrollBarEnabled(false);
+ }
+}
diff --git a/src/com/android/dialer/CallDetailActivity.java b/src/com/android/dialer/CallDetailActivity.java
index f4ca21305..cb1437d4a 100644
--- a/src/com/android/dialer/CallDetailActivity.java
+++ b/src/com/android/dialer/CallDetailActivity.java
@@ -51,23 +51,20 @@ import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
-import com.android.contacts.BackScrollManager;
-import com.android.contacts.BackScrollManager.ScrollableHeader;
import com.android.contacts.ContactPhotoManager;
import com.android.contacts.ContactsUtils;
-import com.android.contacts.ProximitySensorAware;
-import com.android.contacts.ProximitySensorManager;
import com.android.contacts.R;
+import com.android.contacts.format.FormatUtils;
+import com.android.contacts.util.ClipboardUtils;
+import com.android.contacts.util.Constants;
+import com.android.dialer.BackScrollManager.ScrollableHeader;
import com.android.dialer.calllog.CallDetailHistoryAdapter;
import com.android.dialer.calllog.CallTypeHelper;
import com.android.dialer.calllog.ContactInfo;
import com.android.dialer.calllog.ContactInfoHelper;
import com.android.dialer.calllog.PhoneNumberHelper;
-import com.android.contacts.format.FormatUtils;
-import com.android.contacts.util.AsyncTaskExecutor;
-import com.android.contacts.util.AsyncTaskExecutors;
-import com.android.contacts.util.ClipboardUtils;
-import com.android.contacts.util.Constants;
+import com.android.dialer.util.AsyncTaskExecutor;
+import com.android.dialer.util.AsyncTaskExecutors;
import com.android.dialer.voicemail.VoicemailPlaybackFragment;
import com.android.dialer.voicemail.VoicemailStatusHelper;
import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index 380b265ce..841db1c90 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -59,17 +59,17 @@ import android.widget.SearchView.OnQueryTextListener;
import com.android.contacts.ContactsUtils;
import com.android.contacts.R;
import com.android.contacts.activities.TransactionSafeActivity;
-import com.android.dialer.calllog.CallLogFragment;
-import com.android.dialer.dialpad.DialpadFragment;
import com.android.contacts.interactions.PhoneNumberInteraction;
import com.android.contacts.list.ContactListFilterController;
import com.android.contacts.list.ContactListFilterController.ContactListFilterListener;
import com.android.contacts.list.ContactListItemView;
import com.android.contacts.list.OnPhoneNumberPickerActionListener;
-import com.android.contacts.list.PhoneFavoriteFragment;
import com.android.contacts.list.PhoneNumberPickerFragment;
import com.android.contacts.util.AccountFilterUtil;
import com.android.contacts.util.Constants;
+import com.android.dialer.calllog.CallLogFragment;
+import com.android.dialer.dialpad.DialpadFragment;
+import com.android.dialer.list.PhoneFavoriteFragment;
import com.android.internal.telephony.ITelephony;
/**
diff --git a/src/com/android/dialer/PhoneCallDetailsHelper.java b/src/com/android/dialer/PhoneCallDetailsHelper.java
index 8433ebcbb..e1420d656 100644
--- a/src/com/android/dialer/PhoneCallDetailsHelper.java
+++ b/src/com/android/dialer/PhoneCallDetailsHelper.java
@@ -30,9 +30,9 @@ import android.view.View;
import android.widget.TextView;
import com.android.contacts.R;
+import com.android.contacts.test.NeededForTesting;
import com.android.dialer.calllog.CallTypeHelper;
import com.android.dialer.calllog.PhoneNumberHelper;
-import com.android.contacts.test.NeededForTesting;
/**
* Helper class to fill in the views in {@link PhoneCallDetailsViews}.
diff --git a/src/com/android/dialer/ProximitySensorAware.java b/src/com/android/dialer/ProximitySensorAware.java
new file mode 100644
index 000000000..145b8606c
--- /dev/null
+++ b/src/com/android/dialer/ProximitySensorAware.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer;
+
+/**
+ * An object that is aware of the state of the proximity sensor.
+ */
+public interface ProximitySensorAware {
+ /** Start tracking the state of the proximity sensor. */
+ public void enableProximitySensor();
+
+ /**
+ * Stop tracking the state of the proximity sensor.
+ *
+ * @param waitForFarState if true and the sensor is currently in the near state, it will wait
+ * until it is again in the far state before stopping to track its state.
+ */
+ public void disableProximitySensor(boolean waitForFarState);
+}
diff --git a/src/com/android/dialer/ProximitySensorManager.java b/src/com/android/dialer/ProximitySensorManager.java
new file mode 100644
index 000000000..42d740fc1
--- /dev/null
+++ b/src/com/android/dialer/ProximitySensorManager.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Manages the proximity sensor and notifies a listener when enabled.
+ */
+public class ProximitySensorManager {
+ /**
+ * Listener of the state of the proximity sensor.
+ * <p>
+ * This interface abstracts two possible states for the proximity sensor, near and far.
+ * <p>
+ * The actual meaning of these states depends on the actual sensor.
+ */
+ public interface Listener {
+ /** Called when the proximity sensor transitions from the far to the near state. */
+ public void onNear();
+ /** Called when the proximity sensor transitions from the near to the far state. */
+ public void onFar();
+ }
+
+ public static enum State {
+ NEAR, FAR
+ }
+
+ private final ProximitySensorEventListener mProximitySensorListener;
+
+ /**
+ * The current state of the manager, i.e., whether it is currently tracking the state of the
+ * sensor.
+ */
+ private boolean mManagerEnabled;
+
+ /**
+ * The listener to the state of the sensor.
+ * <p>
+ * Contains most of the logic concerning tracking of the sensor.
+ * <p>
+ * After creating an instance of this object, one should call {@link #register()} and
+ * {@link #unregister()} to enable and disable the notifications.
+ * <p>
+ * Instead of calling unregister, one can call {@link #unregisterWhenFar()} to unregister the
+ * listener the next time the sensor reaches the {@link State#FAR} state if currently in the
+ * {@link State#NEAR} state.
+ */
+ private static class ProximitySensorEventListener implements SensorEventListener {
+ private static final float FAR_THRESHOLD = 5.0f;
+
+ private final SensorManager mSensorManager;
+ private final Sensor mProximitySensor;
+ private final float mMaxValue;
+ private final Listener mListener;
+
+ /**
+ * The last state of the sensor.
+ * <p>
+ * Before registering and after unregistering we are always in the {@link State#FAR} state.
+ */
+ @GuardedBy("this") private State mLastState;
+ /**
+ * If this flag is set to true, we are waiting to reach the {@link State#FAR} state and
+ * should notify the listener and unregister when that happens.
+ */
+ @GuardedBy("this") private boolean mWaitingForFarState;
+
+ public ProximitySensorEventListener(SensorManager sensorManager, Sensor proximitySensor,
+ Listener listener) {
+ mSensorManager = sensorManager;
+ mProximitySensor = proximitySensor;
+ mMaxValue = proximitySensor.getMaximumRange();
+ mListener = listener;
+ // Initialize at far state.
+ mLastState = State.FAR;
+ mWaitingForFarState = false;
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ // Make sure we have a valid value.
+ if (event.values == null) return;
+ if (event.values.length == 0) return;
+ float value = event.values[0];
+ // Convert the sensor into a NEAR/FAR state.
+ State state = getStateFromValue(value);
+ synchronized (this) {
+ // No change in state, do nothing.
+ if (state == mLastState) return;
+ // Keep track of the current state.
+ mLastState = state;
+ // If we are waiting to reach the far state and we are now in it, unregister.
+ if (mWaitingForFarState && mLastState == State.FAR) {
+ unregisterWithoutNotification();
+ }
+ }
+ // Notify the listener of the state change.
+ switch (state) {
+ case NEAR:
+ mListener.onNear();
+ break;
+
+ case FAR:
+ mListener.onFar();
+ break;
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ // Nothing to do here.
+ }
+
+ /** Returns the state of the sensor given its current value. */
+ private State getStateFromValue(float value) {
+ // Determine if the current value corresponds to the NEAR or FAR state.
+ // Take case of the case where the proximity sensor is binary: if the current value is
+ // equal to the maximum, we are always in the FAR state.
+ return (value > FAR_THRESHOLD || value == mMaxValue) ? State.FAR : State.NEAR;
+ }
+
+ /**
+ * Unregister the next time the sensor reaches the {@link State#FAR} state.
+ */
+ public synchronized void unregisterWhenFar() {
+ if (mLastState == State.FAR) {
+ // We are already in the far state, just unregister now.
+ unregisterWithoutNotification();
+ } else {
+ mWaitingForFarState = true;
+ }
+ }
+
+ /** Register the listener and call the listener as necessary. */
+ public synchronized void register() {
+ // It is okay to register multiple times.
+ mSensorManager.registerListener(this, mProximitySensor, SensorManager.SENSOR_DELAY_UI);
+ // We should no longer be waiting for the far state if we are registering again.
+ mWaitingForFarState = false;
+ }
+
+ public void unregister() {
+ State lastState;
+ synchronized (this) {
+ unregisterWithoutNotification();
+ lastState = mLastState;
+ // Always go back to the FAR state. That way, when we register again we will get a
+ // transition when the sensor gets into the NEAR state.
+ mLastState = State.FAR;
+ }
+ // Notify the listener if we changed the state to FAR while unregistering.
+ if (lastState != State.FAR) {
+ mListener.onFar();
+ }
+ }
+
+ @GuardedBy("this")
+ private void unregisterWithoutNotification() {
+ mSensorManager.unregisterListener(this);
+ mWaitingForFarState = false;
+ }
+ }
+
+ public ProximitySensorManager(Context context, Listener listener) {
+ SensorManager sensorManager =
+ (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+ Sensor proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+ if (proximitySensor == null) {
+ // If there is no sensor, we should not do anything.
+ mProximitySensorListener = null;
+ } else {
+ mProximitySensorListener =
+ new ProximitySensorEventListener(sensorManager, proximitySensor, listener);
+ }
+ }
+
+ /**
+ * Enables the proximity manager.
+ * <p>
+ * The listener will start getting notifications of events.
+ * <p>
+ * This method is idempotent.
+ */
+ public void enable() {
+ if (mProximitySensorListener != null && !mManagerEnabled) {
+ mProximitySensorListener.register();
+ mManagerEnabled = true;
+ }
+ }
+
+ /**
+ * Disables the proximity manager.
+ * <p>
+ * The listener will stop receiving notifications of events, possibly after receiving a last
+ * {@link Listener#onFar()} callback.
+ * <p>
+ * If {@code waitForFarState} is true, if the sensor is not currently in the {@link State#FAR}
+ * state, the listener will receive a {@link Listener#onFar()} callback the next time the sensor
+ * actually reaches the {@link State#FAR} state.
+ * <p>
+ * If {@code waitForFarState} is false, the listener will receive a {@link Listener#onFar()}
+ * callback immediately if the sensor is currently not in the {@link State#FAR} state.
+ * <p>
+ * This method is idempotent.
+ */
+ public void disable(boolean waitForFarState) {
+ if (mProximitySensorListener != null && mManagerEnabled) {
+ if (waitForFarState) {
+ mProximitySensorListener.unregisterWhenFar();
+ } else {
+ mProximitySensorListener.unregister();
+ }
+ mManagerEnabled = false;
+ }
+ }
+}
diff --git a/src/com/android/dialer/SpecialCharSequenceMgr.java b/src/com/android/dialer/SpecialCharSequenceMgr.java
new file mode 100644
index 000000000..5b88c8daa
--- /dev/null
+++ b/src/com/android/dialer/SpecialCharSequenceMgr.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2006 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.dialer;
+
+import android.app.AlertDialog;
+import android.app.KeyguardManager;
+import android.app.ProgressDialog;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.view.WindowManager;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import com.android.contacts.R;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.TelephonyCapabilities;
+import com.android.internal.telephony.TelephonyIntents;
+
+/**
+ * Helper class to listen for some magic character sequences
+ * that are handled specially by the dialer.
+ *
+ * Note the Phone app also handles these sequences too (in a couple of
+ * relativly obscure places in the UI), so there's a separate version of
+ * this class under apps/Phone.
+ *
+ * TODO: there's lots of duplicated code between this class and the
+ * corresponding class under apps/Phone. Let's figure out a way to
+ * unify these two classes (in the framework? in a common shared library?)
+ */
+public class SpecialCharSequenceMgr {
+ private static final String TAG = "SpecialCharSequenceMgr";
+ private static final String MMI_IMEI_DISPLAY = "*#06#";
+
+ /**
+ * Remembers the previous {@link QueryHandler} and cancel the operation when needed, to
+ * prevent possible crash.
+ *
+ * QueryHandler may call {@link ProgressDialog#dismiss()} when the screen is already gone,
+ * which will cause the app crash. This variable enables the class to prevent the crash
+ * on {@link #cleanup()}.
+ *
+ * TODO: Remove this and replace it (and {@link #cleanup()}) with better implementation.
+ * One complication is that we have SpecialCharSequencMgr in Phone package too, which has
+ * *slightly* different implementation. Note that Phone package doesn't have this problem,
+ * so the class on Phone side doesn't have this functionality.
+ * Fundamental fix would be to have one shared implementation and resolve this corner case more
+ * gracefully.
+ */
+ private static QueryHandler sPreviousAdnQueryHandler;
+
+ /** This class is never instantiated. */
+ private SpecialCharSequenceMgr() {
+ }
+
+ public static boolean handleChars(Context context, String input, EditText textField) {
+ return handleChars(context, input, false, textField);
+ }
+
+ static boolean handleChars(Context context, String input) {
+ return handleChars(context, input, false, null);
+ }
+
+ static boolean handleChars(Context context, String input, boolean useSystemWindow,
+ EditText textField) {
+
+ //get rid of the separators so that the string gets parsed correctly
+ String dialString = PhoneNumberUtils.stripSeparators(input);
+
+ if (handleIMEIDisplay(context, dialString, useSystemWindow)
+ || handlePinEntry(context, dialString)
+ || handleAdnEntry(context, dialString, textField)
+ || handleSecretCode(context, dialString)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Cleanup everything around this class. Must be run inside the main thread.
+ *
+ * This should be called when the screen becomes background.
+ */
+ public static void cleanup() {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ Log.wtf(TAG, "cleanup() is called outside the main thread");
+ return;
+ }
+
+ if (sPreviousAdnQueryHandler != null) {
+ sPreviousAdnQueryHandler.cancel();
+ sPreviousAdnQueryHandler = null;
+ }
+ }
+
+ /**
+ * Handles secret codes to launch arbitrary activities in the form of *#*#<code>#*#*.
+ * If a secret code is encountered an Intent is started with the android_secret_code://<code>
+ * URI.
+ *
+ * @param context the context to use
+ * @param input the text to check for a secret code in
+ * @return true if a secret code was encountered
+ */
+ static boolean handleSecretCode(Context context, String input) {
+ // Secret codes are in the form *#*#<code>#*#*
+ int len = input.length();
+ if (len > 8 && input.startsWith("*#*#") && input.endsWith("#*#*")) {
+ Intent intent = new Intent(TelephonyIntents.SECRET_CODE_ACTION,
+ Uri.parse("android_secret_code://" + input.substring(4, len - 4)));
+ context.sendBroadcast(intent);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handle ADN requests by filling in the SIM contact number into the requested
+ * EditText.
+ *
+ * This code works alongside the Asynchronous query handler {@link QueryHandler}
+ * and query cancel handler implemented in {@link SimContactQueryCookie}.
+ */
+ static boolean handleAdnEntry(Context context, String input, EditText textField) {
+ /* ADN entries are of the form "N(N)(N)#" */
+
+ TelephonyManager telephonyManager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (telephonyManager == null
+ || !TelephonyCapabilities.supportsAdn(telephonyManager.getCurrentPhoneType())) {
+ return false;
+ }
+
+ // if the phone is keyguard-restricted, then just ignore this
+ // input. We want to make sure that sim card contacts are NOT
+ // exposed unless the phone is unlocked, and this code can be
+ // accessed from the emergency dialer.
+ KeyguardManager keyguardManager =
+ (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
+ if (keyguardManager.inKeyguardRestrictedInputMode()) {
+ return false;
+ }
+
+ int len = input.length();
+ if ((len > 1) && (len < 5) && (input.endsWith("#"))) {
+ try {
+ // get the ordinal number of the sim contact
+ int index = Integer.parseInt(input.substring(0, len-1));
+
+ // The original code that navigated to a SIM Contacts list view did not
+ // highlight the requested contact correctly, a requirement for PTCRB
+ // certification. This behaviour is consistent with the UI paradigm
+ // for touch-enabled lists, so it does not make sense to try to work
+ // around it. Instead we fill in the the requested phone number into
+ // the dialer text field.
+
+ // create the async query handler
+ QueryHandler handler = new QueryHandler (context.getContentResolver());
+
+ // create the cookie object
+ SimContactQueryCookie sc = new SimContactQueryCookie(index - 1, handler,
+ ADN_QUERY_TOKEN);
+
+ // setup the cookie fields
+ sc.contactNum = index - 1;
+ sc.setTextField(textField);
+
+ // create the progress dialog
+ sc.progressDialog = new ProgressDialog(context);
+ sc.progressDialog.setTitle(R.string.simContacts_title);
+ sc.progressDialog.setMessage(context.getText(R.string.simContacts_emptyLoading));
+ sc.progressDialog.setIndeterminate(true);
+ sc.progressDialog.setCancelable(true);
+ sc.progressDialog.setOnCancelListener(sc);
+ sc.progressDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+
+ // display the progress dialog
+ sc.progressDialog.show();
+
+ // run the query.
+ handler.startQuery(ADN_QUERY_TOKEN, sc, Uri.parse("content://icc/adn"),
+ new String[]{ADN_PHONE_NUMBER_COLUMN_NAME}, null, null, null);
+
+ if (sPreviousAdnQueryHandler != null) {
+ // It is harmless to call cancel() even after the handler's gone.
+ sPreviousAdnQueryHandler.cancel();
+ }
+ sPreviousAdnQueryHandler = handler;
+ return true;
+ } catch (NumberFormatException ex) {
+ // Ignore
+ }
+ }
+ return false;
+ }
+
+ static boolean handlePinEntry(Context context, String input) {
+ if ((input.startsWith("**04") || input.startsWith("**05")) && input.endsWith("#")) {
+ try {
+ return ITelephony.Stub.asInterface(ServiceManager.getService("phone"))
+ .handlePinMmi(input);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to handlePinMmi due to remote exception");
+ return false;
+ }
+ }
+ return false;
+ }
+
+ static boolean handleIMEIDisplay(Context context, String input, boolean useSystemWindow) {
+ TelephonyManager telephonyManager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (telephonyManager != null && input.equals(MMI_IMEI_DISPLAY)) {
+ int phoneType = telephonyManager.getCurrentPhoneType();
+ if (phoneType == TelephonyManager.PHONE_TYPE_GSM) {
+ showIMEIPanel(context, useSystemWindow, telephonyManager);
+ return true;
+ } else if (phoneType == TelephonyManager.PHONE_TYPE_CDMA) {
+ showMEIDPanel(context, useSystemWindow, telephonyManager);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // TODO: Combine showIMEIPanel() and showMEIDPanel() into a single
+ // generic "showDeviceIdPanel()" method, like in the apps/Phone
+ // version of SpecialCharSequenceMgr.java. (This will require moving
+ // the phone app's TelephonyCapabilities.getDeviceIdLabel() method
+ // into the telephony framework, though.)
+
+ private static void showIMEIPanel(Context context, boolean useSystemWindow,
+ TelephonyManager telephonyManager) {
+ String imeiStr = telephonyManager.getDeviceId();
+
+ AlertDialog alert = new AlertDialog.Builder(context)
+ .setTitle(R.string.imei)
+ .setMessage(imeiStr)
+ .setPositiveButton(android.R.string.ok, null)
+ .setCancelable(false)
+ .show();
+ }
+
+ private static void showMEIDPanel(Context context, boolean useSystemWindow,
+ TelephonyManager telephonyManager) {
+ String meidStr = telephonyManager.getDeviceId();
+
+ AlertDialog alert = new AlertDialog.Builder(context)
+ .setTitle(R.string.meid)
+ .setMessage(meidStr)
+ .setPositiveButton(android.R.string.ok, null)
+ .setCancelable(false)
+ .show();
+ }
+
+ /*******
+ * This code is used to handle SIM Contact queries
+ *******/
+ private static final String ADN_PHONE_NUMBER_COLUMN_NAME = "number";
+ private static final String ADN_NAME_COLUMN_NAME = "name";
+ private static final int ADN_QUERY_TOKEN = -1;
+
+ /**
+ * Cookie object that contains everything we need to communicate to the
+ * handler's onQuery Complete, as well as what we need in order to cancel
+ * the query (if requested).
+ *
+ * Note, access to the textField field is going to be synchronized, because
+ * the user can request a cancel at any time through the UI.
+ */
+ private static class SimContactQueryCookie implements DialogInterface.OnCancelListener{
+ public ProgressDialog progressDialog;
+ public int contactNum;
+
+ // Used to identify the query request.
+ private int mToken;
+ private QueryHandler mHandler;
+
+ // The text field we're going to update
+ private EditText textField;
+
+ public SimContactQueryCookie(int number, QueryHandler handler, int token) {
+ contactNum = number;
+ mHandler = handler;
+ mToken = token;
+ }
+
+ /**
+ * Synchronized getter for the EditText.
+ */
+ public synchronized EditText getTextField() {
+ return textField;
+ }
+
+ /**
+ * Synchronized setter for the EditText.
+ */
+ public synchronized void setTextField(EditText text) {
+ textField = text;
+ }
+
+ /**
+ * Cancel the ADN query by stopping the operation and signaling
+ * the cookie that a cancel request is made.
+ */
+ public synchronized void onCancel(DialogInterface dialog) {
+ // close the progress dialog
+ if (progressDialog != null) {
+ progressDialog.dismiss();
+ }
+
+ // setting the textfield to null ensures that the UI does NOT get
+ // updated.
+ textField = null;
+
+ // Cancel the operation if possible.
+ mHandler.cancelOperation(mToken);
+ }
+ }
+
+ /**
+ * Asynchronous query handler that services requests to look up ADNs
+ *
+ * Queries originate from {@link handleAdnEntry}.
+ */
+ private static class QueryHandler extends AsyncQueryHandler {
+
+ private boolean mCanceled;
+
+ public QueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ /**
+ * Override basic onQueryComplete to fill in the textfield when
+ * we're handed the ADN cursor.
+ */
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor c) {
+ sPreviousAdnQueryHandler = null;
+ if (mCanceled) {
+ return;
+ }
+
+ SimContactQueryCookie sc = (SimContactQueryCookie) cookie;
+
+ // close the progress dialog.
+ sc.progressDialog.dismiss();
+
+ // get the EditText to update or see if the request was cancelled.
+ EditText text = sc.getTextField();
+
+ // if the textview is valid, and the cursor is valid and postionable
+ // on the Nth number, then we update the text field and display a
+ // toast indicating the caller name.
+ if ((c != null) && (text != null) && (c.moveToPosition(sc.contactNum))) {
+ String name = c.getString(c.getColumnIndexOrThrow(ADN_NAME_COLUMN_NAME));
+ String number = c.getString(c.getColumnIndexOrThrow(ADN_PHONE_NUMBER_COLUMN_NAME));
+
+ // fill the text in.
+ text.getText().replace(0, 0, number);
+
+ // display the name as a toast
+ Context context = sc.progressDialog.getContext();
+ name = context.getString(R.string.menu_callNumber, name);
+ Toast.makeText(context, name, Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+
+ public void cancel() {
+ mCanceled = true;
+ // Ask AsyncQueryHandler to cancel the whole request. This will fails when the
+ // query already started.
+ cancelOperation(ADN_QUERY_TOKEN);
+ }
+ }
+}
diff --git a/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java b/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java
index 38dc72722..0763f3ccb 100644
--- a/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java
+++ b/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java
@@ -25,8 +25,8 @@ import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
-import com.android.dialer.PhoneCallDetails;
import com.android.contacts.R;
+import com.android.dialer.PhoneCallDetails;
/**
* Adapter for a ListView containing history items from the details of a call.
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index 217f59765..720cc11d4 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -33,11 +33,11 @@ import android.view.ViewTreeObserver;
import com.android.common.widget.GroupingListAdapter;
import com.android.contacts.ContactPhotoManager;
+import com.android.contacts.R;
+import com.android.contacts.util.UriUtils;
import com.android.dialer.PhoneCallDetails;
import com.android.dialer.PhoneCallDetailsHelper;
-import com.android.contacts.R;
import com.android.dialer.util.ExpirableCache;
-import com.android.contacts.util.UriUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index 4b3113403..83ed830ad 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -49,7 +49,7 @@ import com.android.common.io.MoreCloseables;
import com.android.contacts.ContactsUtils;
import com.android.contacts.R;
import com.android.contacts.util.Constants;
-import com.android.contacts.util.EmptyLoader;
+import com.android.dialer.util.EmptyLoader;
import com.android.dialer.voicemail.VoicemailStatusHelper;
import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
diff --git a/src/com/android/dialer/calllog/CallLogListItemHelper.java b/src/com/android/dialer/calllog/CallLogListItemHelper.java
index 7862a5679..101ca7de3 100644
--- a/src/com/android/dialer/calllog/CallLogListItemHelper.java
+++ b/src/com/android/dialer/calllog/CallLogListItemHelper.java
@@ -21,9 +21,9 @@ import android.provider.CallLog.Calls;
import android.text.TextUtils;
import android.view.View;
+import com.android.contacts.R;
import com.android.dialer.PhoneCallDetails;
import com.android.dialer.PhoneCallDetailsHelper;
-import com.android.contacts.R;
/**
* Helper class to fill in the views of a call log entry.
diff --git a/src/com/android/dialer/calllog/CallLogListItemViews.java b/src/com/android/dialer/calllog/CallLogListItemViews.java
index 5b860efcb..ac6ad955e 100644
--- a/src/com/android/dialer/calllog/CallLogListItemViews.java
+++ b/src/com/android/dialer/calllog/CallLogListItemViews.java
@@ -22,9 +22,9 @@ import android.widget.ImageView;
import android.widget.QuickContactBadge;
import android.widget.TextView;
-import com.android.dialer.PhoneCallDetailsViews;
import com.android.contacts.R;
import com.android.contacts.test.NeededForTesting;
+import com.android.dialer.PhoneCallDetailsViews;
/**
* Simple value object containing the various views within a call log entry.
diff --git a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
index 0f6fe3b08..ff4e5eeb5 100644
--- a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
+++ b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
@@ -32,8 +32,8 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.common.io.MoreCloseables;
-import com.android.dialer.CallDetailActivity;
import com.android.contacts.R;
+import com.android.dialer.CallDetailActivity;
import com.google.common.collect.Maps;
import java.util.Map;
diff --git a/src/com/android/dialer/calllog/IntentProvider.java b/src/com/android/dialer/calllog/IntentProvider.java
index f43dc5104..859487a26 100644
--- a/src/com/android/dialer/calllog/IntentProvider.java
+++ b/src/com/android/dialer/calllog/IntentProvider.java
@@ -23,8 +23,8 @@ import android.database.Cursor;
import android.net.Uri;
import android.provider.CallLog.Calls;
-import com.android.dialer.CallDetailActivity;
import com.android.contacts.ContactsUtils;
+import com.android.dialer.CallDetailActivity;
/**
* Used to create an intent to attach to an action in the call log.
diff --git a/src/com/android/dialer/dialpad/DialpadFragment.java b/src/com/android/dialer/dialpad/DialpadFragment.java
index 65cab569a..77b7c070d 100644
--- a/src/com/android/dialer/dialpad/DialpadFragment.java
+++ b/src/com/android/dialer/dialpad/DialpadFragment.java
@@ -69,11 +69,11 @@ import android.widget.TextView;
import com.android.contacts.ContactsUtils;
import com.android.contacts.R;
-import com.android.contacts.SpecialCharSequenceMgr;
-import com.android.dialer.DialtactsActivity;
import com.android.contacts.util.Constants;
import com.android.contacts.util.PhoneNumberFormatter;
import com.android.contacts.util.StopWatch;
+import com.android.dialer.DialtactsActivity;
+import com.android.dialer.SpecialCharSequenceMgr;
import com.android.internal.telephony.ITelephony;
import com.android.phone.common.CallLogAsync;
import com.android.phone.common.HapticFeedback;
diff --git a/src/com/android/dialer/list/PhoneFavoriteFragment.java b/src/com/android/dialer/list/PhoneFavoriteFragment.java
new file mode 100644
index 000000000..157e82fb1
--- /dev/null
+++ b/src/com/android/dialer/list/PhoneFavoriteFragment.java
@@ -0,0 +1,569 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.list;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.LoaderManager;
+import android.content.CursorLoader;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Directory;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.FrameLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.contacts.ContactPhotoManager;
+import com.android.contacts.ContactTileLoaderFactory;
+import com.android.contacts.R;
+import com.android.contacts.dialog.ClearFrequentsDialog;
+import com.android.contacts.interactions.ImportExportDialogFragment;
+import com.android.contacts.list.ContactListFilter;
+import com.android.contacts.list.ContactListFilterController;
+import com.android.contacts.list.ContactListItemView;
+import com.android.contacts.list.ContactTileAdapter;
+import com.android.contacts.list.ContactTileView;
+import com.android.contacts.list.PhoneNumberListAdapter;
+import com.android.contacts.preference.ContactsPreferences;
+import com.android.contacts.util.AccountFilterUtil;
+
+/**
+ * Fragment for Phone UI's favorite screen.
+ *
+ * This fragment contains three kinds of contacts in one screen: "starred", "frequent", and "all"
+ * contacts. To show them at once, this merges results from {@link com.android.contacts.list.ContactTileAdapter} and
+ * {@link com.android.contacts.list.PhoneNumberListAdapter} into one unified list using {@link PhoneFavoriteMergedAdapter}.
+ * A contact filter header is also inserted between those adapters' results.
+ */
+public class PhoneFavoriteFragment extends Fragment implements OnItemClickListener {
+ private static final String TAG = PhoneFavoriteFragment.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ /**
+ * Used with LoaderManager.
+ */
+ private static int LOADER_ID_CONTACT_TILE = 1;
+ private static int LOADER_ID_ALL_CONTACTS = 2;
+
+ private static final String KEY_FILTER = "filter";
+
+ private static final int REQUEST_CODE_ACCOUNT_FILTER = 1;
+
+ public interface Listener {
+ public void onContactSelected(Uri contactUri);
+ public void onCallNumberDirectly(String phoneNumber);
+ }
+
+ private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
+ @Override
+ public CursorLoader onCreateLoader(int id, Bundle args) {
+ if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onCreateLoader.");
+ return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity());
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoadFinished");
+ mContactTileAdapter.setContactCursor(data);
+
+ if (mAllContactsForceReload) {
+ mAllContactsAdapter.onDataReload();
+ // Use restartLoader() to make LoaderManager to load the section again.
+ getLoaderManager().restartLoader(
+ LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener);
+ } else if (!mAllContactsLoaderStarted) {
+ // Load "all" contacts if not loaded yet.
+ getLoaderManager().initLoader(
+ LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener);
+ }
+ mAllContactsForceReload = false;
+ mAllContactsLoaderStarted = true;
+
+ // Show the filter header with "loading" state.
+ updateFilterHeaderView();
+ mAccountFilterHeader.setVisibility(View.VISIBLE);
+
+ // invalidate the options menu if needed
+ invalidateOptionsMenuIfNeeded();
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoaderReset. ");
+ }
+ }
+
+ private class AllContactsLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onCreateLoader");
+ CursorLoader loader = new CursorLoader(getActivity(), null, null, null, null, null);
+ mAllContactsAdapter.configureLoader(loader, Directory.DEFAULT);
+ return loader;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onLoadFinished");
+ mAllContactsAdapter.changeCursor(0, data);
+ updateFilterHeaderView();
+ mHandler.removeMessages(MESSAGE_SHOW_LOADING_EFFECT);
+ mLoadingView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onLoaderReset. ");
+ }
+ }
+
+ private class ContactTileAdapterListener implements ContactTileView.Listener {
+ @Override
+ public void onContactSelected(Uri contactUri, Rect targetRect) {
+ if (mListener != null) {
+ mListener.onContactSelected(contactUri);
+ }
+ }
+
+ @Override
+ public void onCallNumberDirectly(String phoneNumber) {
+ if (mListener != null) {
+ mListener.onCallNumberDirectly(phoneNumber);
+ }
+ }
+
+ @Override
+ public int getApproximateTileWidth() {
+ return getView().getWidth() / mContactTileAdapter.getColumnCount();
+ }
+ }
+
+ private class FilterHeaderClickListener implements OnClickListener {
+ @Override
+ public void onClick(View view) {
+ AccountFilterUtil.startAccountFilterActivityForResult(
+ PhoneFavoriteFragment.this,
+ REQUEST_CODE_ACCOUNT_FILTER,
+ mFilter);
+ }
+ }
+
+ private class ContactsPreferenceChangeListener
+ implements ContactsPreferences.ChangeListener {
+ @Override
+ public void onChange() {
+ if (loadContactsPreferences()) {
+ requestReloadAllContacts();
+ }
+ }
+ }
+
+ private class ScrollListener implements ListView.OnScrollListener {
+ private boolean mShouldShowFastScroller;
+ @Override
+ public void onScroll(AbsListView view,
+ int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ // FastScroller should be visible only when the user is seeing "all" contacts section.
+ final boolean shouldShow = mAdapter.shouldShowFirstScroller(firstVisibleItem);
+ if (shouldShow != mShouldShowFastScroller) {
+ mListView.setVerticalScrollBarEnabled(shouldShow);
+ mListView.setFastScrollEnabled(shouldShow);
+ mListView.setFastScrollAlwaysVisible(shouldShow);
+ mShouldShowFastScroller = shouldShow;
+ }
+ }
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ }
+ }
+
+ private static final int MESSAGE_SHOW_LOADING_EFFECT = 1;
+ private static final int LOADING_EFFECT_DELAY = 500; // ms
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_SHOW_LOADING_EFFECT:
+ mLoadingView.setVisibility(View.VISIBLE);
+ break;
+ }
+ }
+ };
+
+ private Listener mListener;
+ private PhoneFavoriteMergedAdapter mAdapter;
+ private ContactTileAdapter mContactTileAdapter;
+ private PhoneNumberListAdapter mAllContactsAdapter;
+
+ /**
+ * true when the loader for {@link PhoneNumberListAdapter} has started already.
+ */
+ private boolean mAllContactsLoaderStarted;
+ /**
+ * true when the loader for {@link PhoneNumberListAdapter} must reload "all" contacts again.
+ * It typically happens when {@link ContactsPreferences} has changed its settings
+ * (display order and sort order)
+ */
+ private boolean mAllContactsForceReload;
+
+ private ContactsPreferences mContactsPrefs;
+ private ContactListFilter mFilter;
+
+ private TextView mEmptyView;
+ private ListView mListView;
+ /**
+ * Layout containing {@link #mAccountFilterHeader}. Used to limit area being "pressed".
+ */
+ private FrameLayout mAccountFilterHeaderContainer;
+ private View mAccountFilterHeader;
+
+ /**
+ * Layout used when contacts load is slower than expected and thus "loading" view should be
+ * shown.
+ */
+ private View mLoadingView;
+
+ private final ContactTileView.Listener mContactTileAdapterListener =
+ new ContactTileAdapterListener();
+ private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener =
+ new ContactTileLoaderListener();
+ private final LoaderManager.LoaderCallbacks<Cursor> mAllContactsLoaderListener =
+ new AllContactsLoaderListener();
+ private final OnClickListener mFilterHeaderClickListener = new FilterHeaderClickListener();
+ private final ContactsPreferenceChangeListener mContactsPreferenceChangeListener =
+ new ContactsPreferenceChangeListener();
+ private final ScrollListener mScrollListener = new ScrollListener();
+
+ private boolean mOptionsMenuHasFrequents;
+
+ @Override
+ public void onAttach(Activity activity) {
+ if (DEBUG) Log.d(TAG, "onAttach()");
+ super.onAttach(activity);
+
+ mContactsPrefs = new ContactsPreferences(activity);
+
+ // Construct two base adapters which will become part of PhoneFavoriteMergedAdapter.
+ // We don't construct the resultant adapter at this moment since it requires LayoutInflater
+ // that will be available on onCreateView().
+
+ mContactTileAdapter = new ContactTileAdapter(activity, mContactTileAdapterListener,
+ getResources().getInteger(R.integer.contact_tile_column_count_in_favorites),
+ ContactTileAdapter.DisplayType.STREQUENT_PHONE_ONLY);
+ mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity));
+
+ // Setup the "all" adapter manually. See also the setup logic in ContactEntryListFragment.
+ mAllContactsAdapter = new PhoneNumberListAdapter(activity);
+ mAllContactsAdapter.setDisplayPhotos(true);
+ mAllContactsAdapter.setQuickContactEnabled(true);
+ mAllContactsAdapter.setSearchMode(false);
+ mAllContactsAdapter.setIncludeProfile(false);
+ mAllContactsAdapter.setSelectionVisible(false);
+ mAllContactsAdapter.setDarkTheme(true);
+ mAllContactsAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity));
+ // Disable directory header.
+ mAllContactsAdapter.setHasHeader(0, false);
+ // Show A-Z section index.
+ mAllContactsAdapter.setSectionHeaderDisplayEnabled(true);
+ // Disable pinned header. It doesn't work with this fragment.
+ mAllContactsAdapter.setPinnedPartitionHeadersEnabled(false);
+ // Put photos on left for consistency with "frequent" contacts section.
+ mAllContactsAdapter.setPhotoPosition(ContactListItemView.PhotoPosition.LEFT);
+
+ // Use Callable.CONTENT_URI which will include not only phone numbers but also SIP
+ // addresses.
+ mAllContactsAdapter.setUseCallableUri(true);
+
+ mAllContactsAdapter.setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder());
+ mAllContactsAdapter.setSortOrder(mContactsPrefs.getSortOrder());
+ }
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ if (DEBUG) Log.d(TAG, "onCreate()");
+ super.onCreate(savedState);
+ if (savedState != null) {
+ mFilter = savedState.getParcelable(KEY_FILTER);
+
+ if (mFilter != null) {
+ mAllContactsAdapter.setFilter(mFilter);
+ }
+ }
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(KEY_FILTER, mFilter);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ final View listLayout = inflater.inflate(
+ R.layout.phone_contact_tile_list, container, false);
+
+ mListView = (ListView) listLayout.findViewById(R.id.contact_tile_list);
+ mListView.setItemsCanFocus(true);
+ mListView.setOnItemClickListener(this);
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT);
+ mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+
+ // Create the account filter header but keep it hidden until "all" contacts are loaded.
+ mAccountFilterHeaderContainer = new FrameLayout(getActivity(), null);
+ mAccountFilterHeader = inflater.inflate(R.layout.account_filter_header_for_phone_favorite,
+ mListView, false);
+ mAccountFilterHeader.setOnClickListener(mFilterHeaderClickListener);
+ mAccountFilterHeaderContainer.addView(mAccountFilterHeader);
+
+ mLoadingView = inflater.inflate(R.layout.phone_loading_contacts, mListView, false);
+
+ mAdapter = new PhoneFavoriteMergedAdapter(getActivity(),
+ mContactTileAdapter, mAccountFilterHeaderContainer, mAllContactsAdapter,
+ mLoadingView);
+
+ mListView.setAdapter(mAdapter);
+
+ mListView.setOnScrollListener(mScrollListener);
+ mListView.setFastScrollEnabled(false);
+ mListView.setFastScrollAlwaysVisible(false);
+
+ mEmptyView = (TextView) listLayout.findViewById(R.id.contact_tile_list_empty);
+ mEmptyView.setText(getString(R.string.listTotalAllContactsZero));
+ mListView.setEmptyView(mEmptyView);
+
+ updateFilterHeaderView();
+
+ return listLayout;
+ }
+
+ private boolean isOptionsMenuChanged() {
+ return mOptionsMenuHasFrequents != hasFrequents();
+ }
+
+ private void invalidateOptionsMenuIfNeeded() {
+ if (isOptionsMenuChanged()) {
+ getActivity().invalidateOptionsMenu();
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ inflater.inflate(R.menu.phone_favorite_options, menu);
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents);
+ mOptionsMenuHasFrequents = hasFrequents();
+ clearFrequents.setVisible(mOptionsMenuHasFrequents);
+ }
+
+ private boolean hasFrequents() {
+ return mContactTileAdapter.getNumFrequents() > 0;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_import_export:
+ // We hard-code the "contactsAreAvailable" argument because doing it properly would
+ // involve querying a {@link ProviderStatusLoader}, which we don't want to do right
+ // now in Dialtacts for (potential) performance reasons. Compare with how it is
+ // done in {@link PeopleActivity}.
+ ImportExportDialogFragment.show(getFragmentManager(), true);
+ return true;
+ case R.id.menu_accounts:
+ final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
+ intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[] {
+ ContactsContract.AUTHORITY
+ });
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ startActivity(intent);
+ return true;
+ case R.id.menu_clear_frequents:
+ ClearFrequentsDialog.show(getFragmentManager());
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ mContactsPrefs.registerChangeListener(mContactsPreferenceChangeListener);
+
+ // If ContactsPreferences has changed, we need to reload "all" contacts with the new
+ // settings. If mAllContactsFoarceReload is already true, it should be kept.
+ if (loadContactsPreferences()) {
+ mAllContactsForceReload = true;
+ }
+
+ // Use initLoader() instead of restartLoader() to refraining unnecessary reload.
+ // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will
+ // be called, on which we'll check if "all" contacts should be reloaded again or not.
+ getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener);
+
+ // Delay showing "loading" view until certain amount of time so that users won't see
+ // instant flash of the view when the contacts load is fast enough.
+ // This will be kept shown until both tile and all sections are loaded.
+ mLoadingView.setVisibility(View.INVISIBLE);
+ mHandler.sendEmptyMessageDelayed(MESSAGE_SHOW_LOADING_EFFECT, LOADING_EFFECT_DELAY);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mContactsPrefs.unregisterChangeListener();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * This is only effective for elements provided by {@link #mContactTileAdapter}.
+ * {@link #mContactTileAdapter} has its own logic for click events.
+ */
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final int contactTileAdapterCount = mContactTileAdapter.getCount();
+ if (position <= contactTileAdapterCount) {
+ Log.e(TAG, "onItemClick() event for unexpected position. "
+ + "The position " + position + " is before \"all\" section. Ignored.");
+ } else {
+ final int localPosition = position - mContactTileAdapter.getCount() - 1;
+ if (mListener != null) {
+ mListener.onContactSelected(mAllContactsAdapter.getDataUri(localPosition));
+ }
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_CODE_ACCOUNT_FILTER) {
+ if (getActivity() != null) {
+ AccountFilterUtil.handleAccountFilterResult(
+ ContactListFilterController.getInstance(getActivity()), resultCode, data);
+ } else {
+ Log.e(TAG, "getActivity() returns null during Fragment#onActivityResult()");
+ }
+ }
+ }
+
+ private boolean loadContactsPreferences() {
+ if (mContactsPrefs == null || mAllContactsAdapter == null) {
+ return false;
+ }
+
+ boolean changed = false;
+ final int currentDisplayOrder = mContactsPrefs.getDisplayOrder();
+ if (mAllContactsAdapter.getContactNameDisplayOrder() != currentDisplayOrder) {
+ mAllContactsAdapter.setContactNameDisplayOrder(currentDisplayOrder);
+ changed = true;
+ }
+
+ final int currentSortOrder = mContactsPrefs.getSortOrder();
+ if (mAllContactsAdapter.getSortOrder() != currentSortOrder) {
+ mAllContactsAdapter.setSortOrder(currentSortOrder);
+ changed = true;
+ }
+
+ return changed;
+ }
+
+ /**
+ * Requests to reload "all" contacts. If the section is already loaded, this method will
+ * force reloading it now. If the section isn't loaded yet, the actual load may be done later
+ * (on {@link #onStart()}.
+ */
+ private void requestReloadAllContacts() {
+ if (DEBUG) {
+ Log.d(TAG, "requestReloadAllContacts()"
+ + " mAllContactsAdapter: " + mAllContactsAdapter
+ + ", mAllContactsLoaderStarted: " + mAllContactsLoaderStarted);
+ }
+
+ if (mAllContactsAdapter == null || !mAllContactsLoaderStarted) {
+ // Remember this request until next load on onStart().
+ mAllContactsForceReload = true;
+ return;
+ }
+
+ if (DEBUG) Log.d(TAG, "Reload \"all\" contacts now.");
+
+ mAllContactsAdapter.onDataReload();
+ // Use restartLoader() to make LoaderManager to load the section again.
+ getLoaderManager().restartLoader(LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener);
+ }
+
+ private void updateFilterHeaderView() {
+ final ContactListFilter filter = getFilter();
+ if (mAccountFilterHeader == null || mAllContactsAdapter == null || filter == null) {
+ return;
+ }
+ AccountFilterUtil.updateAccountFilterTitleForPhone(mAccountFilterHeader, filter, true);
+ }
+
+ public ContactListFilter getFilter() {
+ return mFilter;
+ }
+
+ public void setFilter(ContactListFilter filter) {
+ if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) {
+ return;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "setFilter(). old filter (" + mFilter
+ + ") will be replaced with new filter (" + filter + ")");
+ }
+
+ mFilter = filter;
+
+ if (mAllContactsAdapter != null) {
+ mAllContactsAdapter.setFilter(mFilter);
+ requestReloadAllContacts();
+ updateFilterHeaderView();
+ }
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+}
diff --git a/src/com/android/dialer/list/PhoneFavoriteMergedAdapter.java b/src/com/android/dialer/list/PhoneFavoriteMergedAdapter.java
new file mode 100644
index 000000000..8e2339961
--- /dev/null
+++ b/src/com/android/dialer/list/PhoneFavoriteMergedAdapter.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2011 Google Inc.
+ * Licensed to 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.dialer.list;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.SectionIndexer;
+
+import com.android.contacts.R;
+import com.android.contacts.list.ContactEntryListAdapter;
+import com.android.contacts.list.ContactListItemView;
+import com.android.contacts.list.ContactTileAdapter;
+
+/**
+ * An adapter that combines items from {@link com.android.contacts.list.ContactTileAdapter} and
+ * {@link com.android.contacts.list.ContactEntryListAdapter} into a single list. In between those two results,
+ * an account filter header will be inserted.
+ */
+public class PhoneFavoriteMergedAdapter extends BaseAdapter implements SectionIndexer {
+
+ private class CustomDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+ }
+
+ private final ContactTileAdapter mContactTileAdapter;
+ private final ContactEntryListAdapter mContactEntryListAdapter;
+ private final View mAccountFilterHeaderContainer;
+ private final View mLoadingView;
+
+ private final int mItemPaddingLeft;
+ private final int mItemPaddingRight;
+
+ // Make frequent header consistent with account filter header.
+ private final int mFrequentHeaderPaddingTop;
+
+ private final DataSetObserver mObserver;
+
+ public PhoneFavoriteMergedAdapter(Context context,
+ ContactTileAdapter contactTileAdapter,
+ View accountFilterHeaderContainer,
+ ContactEntryListAdapter contactEntryListAdapter,
+ View loadingView) {
+ Resources resources = context.getResources();
+ mItemPaddingLeft = resources.getDimensionPixelSize(R.dimen.detail_item_side_margin);
+ mItemPaddingRight = resources.getDimensionPixelSize(R.dimen.list_visible_scrollbar_padding);
+ mFrequentHeaderPaddingTop = resources.getDimensionPixelSize(
+ R.dimen.contact_browser_list_top_margin);
+ mContactTileAdapter = contactTileAdapter;
+ mContactEntryListAdapter = contactEntryListAdapter;
+
+ mAccountFilterHeaderContainer = accountFilterHeaderContainer;
+
+ mObserver = new CustomDataSetObserver();
+ mContactTileAdapter.registerDataSetObserver(mObserver);
+ mContactEntryListAdapter.registerDataSetObserver(mObserver);
+
+ mLoadingView = loadingView;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ // Cannot use the super's method here because we add extra rows in getCount() to account
+ // for headers
+ return mContactTileAdapter.getCount() + mContactEntryListAdapter.getCount() == 0;
+ }
+
+ @Override
+ public int getCount() {
+ final int contactTileAdapterCount = mContactTileAdapter.getCount();
+ final int contactEntryListAdapterCount = mContactEntryListAdapter.getCount();
+ if (mContactEntryListAdapter.isLoading()) {
+ // Hide "all" contacts during its being loaded. Instead show "loading" view.
+ //
+ // "+2" for mAccountFilterHeaderContainer and mLoadingView
+ return contactTileAdapterCount + 2;
+ } else {
+ // "+1" for mAccountFilterHeaderContainer
+ return contactTileAdapterCount + contactEntryListAdapterCount + 1;
+ }
+ }
+
+ @Override
+ public Object getItem(int position) {
+ final int contactTileAdapterCount = mContactTileAdapter.getCount();
+ final int contactEntryListAdapterCount = mContactEntryListAdapter.getCount();
+ if (position < contactTileAdapterCount) { // For "tile" and "frequent" sections
+ return mContactTileAdapter.getItem(position);
+ } else if (position == contactTileAdapterCount) { // For "all" section's account header
+ return mAccountFilterHeaderContainer;
+ } else { // For "all" section
+ if (mContactEntryListAdapter.isLoading()) { // "All" section is being loaded.
+ return mLoadingView;
+ } else {
+ // "-1" for mAccountFilterHeaderContainer
+ final int localPosition = position - contactTileAdapterCount - 1;
+ return mContactTileAdapter.getItem(localPosition);
+ }
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ // "+2" for mAccountFilterHeaderContainer and mLoadingView
+ return (mContactTileAdapter.getViewTypeCount()
+ + mContactEntryListAdapter.getViewTypeCount()
+ + 2);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ final int contactTileAdapterCount = mContactTileAdapter.getCount();
+ final int contactEntryListAdapterCount = mContactEntryListAdapter.getCount();
+ // There should be four kinds of types that are usually used, and one more exceptional
+ // type (IGNORE_ITEM_VIEW_TYPE), which sometimes comes from mContactTileAdapter.
+ //
+ // The four ordinary view types have the index equal to or more than 0, and less than
+ // mContactTileAdapter.getViewTypeCount()+ mContactEntryListAdapter.getViewTypeCount() + 2.
+ // (See also this class's getViewTypeCount())
+ //
+ // We have those values for:
+ // - The view types mContactTileAdapter originally has
+ // - The view types mContactEntryListAdapter originally has
+ // - mAccountFilterHeaderContainer ("all" section's account header), and
+ // - mLoadingView
+ //
+ // Those types should not be mixed, so we have a different range for each kinds of types:
+ // - Types for mContactTileAdapter ("tile" and "frequent" sections)
+ // They should have the index, >=0 and <mContactTileAdapter.getViewTypeCount()
+ //
+ // - Types for mContactEntryListAdapter ("all" sections)
+ // They should have the index, >=mContactTileAdapter.getViewTypeCount() and
+ // <(mContactTileAdapter.getViewTypeCount() + mContactEntryListAdapter.getViewTypeCount())
+ //
+ // - Type for "all" section's account header
+ // It should have the exact index
+ // mContactTileAdapter.getViewTypeCount()+ mContactEntryListAdapter.getViewTypeCount()
+ //
+ // - Type for "loading" view used during "all" section is being loaded.
+ // It should have the exact index
+ // mContactTileAdapter.getViewTypeCount()+ mContactEntryListAdapter.getViewTypeCount() + 1
+ //
+ // As an exception, IGNORE_ITEM_VIEW_TYPE (-1) will be remained as is, which will be used
+ // by framework's Adapter implementation and thus should be left as is.
+ if (position < contactTileAdapterCount) { // For "tile" and "frequent" sections
+ return mContactTileAdapter.getItemViewType(position);
+ } else if (position == contactTileAdapterCount) { // For "all" section's account header
+ return mContactTileAdapter.getViewTypeCount()
+ + mContactEntryListAdapter.getViewTypeCount();
+ } else { // For "all" section
+ if (mContactEntryListAdapter.isLoading()) { // "All" section is being loaded.
+ return mContactTileAdapter.getViewTypeCount()
+ + mContactEntryListAdapter.getViewTypeCount() + 1;
+ } else {
+ // "-1" for mAccountFilterHeaderContainer
+ final int localPosition = position - contactTileAdapterCount - 1;
+ final int type = mContactEntryListAdapter.getItemViewType(localPosition);
+ // IGNORE_ITEM_VIEW_TYPE must be handled differently.
+ return (type < 0) ? type : type + mContactTileAdapter.getViewTypeCount();
+ }
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final int contactTileAdapterCount = mContactTileAdapter.getCount();
+ final int contactEntryListAdapterCount = mContactEntryListAdapter.getCount();
+
+ // Obtain a View relevant for that position, and adjust its horizontal padding. Each
+ // View has different implementation, so we use different way to control those padding.
+ if (position < contactTileAdapterCount) { // For "tile" and "frequent" sections
+ final View view = mContactTileAdapter.getView(position, convertView, parent);
+ final int frequentHeaderPosition = mContactTileAdapter.getFrequentHeaderPosition();
+ if (position < frequentHeaderPosition) { // "starred" contacts
+ // No padding adjustment.
+ } else if (position == frequentHeaderPosition) {
+ view.setPadding(mItemPaddingLeft, mFrequentHeaderPaddingTop,
+ mItemPaddingRight, view.getPaddingBottom());
+ } else {
+ // Views for "frequent" contacts use FrameLayout's margins instead of padding.
+ final FrameLayout frameLayout = (FrameLayout) view;
+ final View child = frameLayout.getChildAt(0);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT);
+ params.setMargins(mItemPaddingLeft, 0, mItemPaddingRight, 0);
+ child.setLayoutParams(params);
+ }
+ return view;
+ } else if (position == contactTileAdapterCount) { // For "all" section's account header
+ mAccountFilterHeaderContainer.setPadding(mItemPaddingLeft,
+ mAccountFilterHeaderContainer.getPaddingTop(),
+ mItemPaddingRight,
+ mAccountFilterHeaderContainer.getPaddingBottom());
+ return mAccountFilterHeaderContainer;
+ } else { // For "all" section
+ if (mContactEntryListAdapter.isLoading()) { // "All" section is being loaded.
+ mLoadingView.setPadding(mItemPaddingLeft,
+ mLoadingView.getPaddingTop(),
+ mItemPaddingRight,
+ mLoadingView.getPaddingBottom());
+ return mLoadingView;
+ } else {
+ // "-1" for mAccountFilterHeaderContainer
+ final int localPosition = position - contactTileAdapterCount - 1;
+ final ContactListItemView itemView = (ContactListItemView)
+ mContactEntryListAdapter.getView(localPosition, convertView, null);
+ itemView.setPadding(mItemPaddingLeft, itemView.getPaddingTop(),
+ mItemPaddingRight, itemView.getPaddingBottom());
+ itemView.setSelectionBoundsHorizontalMargin(mItemPaddingLeft, mItemPaddingRight);
+ return itemView;
+ }
+ }
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ // If "all" section is being loaded we'll show mLoadingView, which is not enabled.
+ // Otherwise check the all the other components in the ListView and return appropriate
+ // result.
+ return !mContactEntryListAdapter.isLoading()
+ && (mContactTileAdapter.areAllItemsEnabled()
+ && mAccountFilterHeaderContainer.isEnabled()
+ && mContactEntryListAdapter.areAllItemsEnabled());
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ final int contactTileAdapterCount = mContactTileAdapter.getCount();
+ final int contactEntryListAdapterCount = mContactEntryListAdapter.getCount();
+ if (position < contactTileAdapterCount) { // For "tile" and "frequent" sections
+ return mContactTileAdapter.isEnabled(position);
+ } else if (position == contactTileAdapterCount) { // For "all" section's account header
+ // This will be handled by View's onClick event instead of ListView's onItemClick event.
+ return false;
+ } else { // For "all" section
+ if (mContactEntryListAdapter.isLoading()) { // "All" section is being loaded.
+ return false;
+ } else {
+ // "-1" for mAccountFilterHeaderContainer
+ final int localPosition = position - contactTileAdapterCount - 1;
+ return mContactEntryListAdapter.isEnabled(localPosition);
+ }
+ }
+ }
+
+ @Override
+ public int getPositionForSection(int sectionIndex) {
+ final int contactTileAdapterCount = mContactTileAdapter.getCount();
+ final int localPosition = mContactEntryListAdapter.getPositionForSection(sectionIndex);
+ return contactTileAdapterCount + 1 + localPosition;
+ }
+
+ @Override
+ public int getSectionForPosition(int position) {
+ final int contactTileAdapterCount = mContactTileAdapter.getCount();
+ if (position <= contactTileAdapterCount) {
+ return 0;
+ } else {
+ // "-1" for mAccountFilterHeaderContainer
+ final int localPosition = position - contactTileAdapterCount - 1;
+ return mContactEntryListAdapter.getSectionForPosition(localPosition);
+ }
+ }
+
+ @Override
+ public Object[] getSections() {
+ return mContactEntryListAdapter.getSections();
+ }
+
+ public boolean shouldShowFirstScroller(int firstVisibleItem) {
+ final int contactTileAdapterCount = mContactTileAdapter.getCount();
+ return firstVisibleItem > contactTileAdapterCount;
+ }
+}
diff --git a/src/com/android/dialer/util/AsyncTaskExecutor.java b/src/com/android/dialer/util/AsyncTaskExecutor.java
new file mode 100644
index 000000000..ca09f0878
--- /dev/null
+++ b/src/com/android/dialer/util/AsyncTaskExecutor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.util;
+
+import android.os.AsyncTask;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Interface used to submit {@link AsyncTask} objects to run in the background.
+ * <p>
+ * This interface has a direct parallel with the {@link Executor} interface. It exists to decouple
+ * the mechanics of AsyncTask submission from the description of how that AsyncTask will execute.
+ * <p>
+ * One immediate benefit of this approach is that testing becomes much easier, since it is easy to
+ * introduce a mock or fake AsyncTaskExecutor in unit/integration tests, and thus inspect which
+ * tasks have been submitted and control their execution in an orderly manner.
+ * <p>
+ * Another benefit in due course will be the management of the submitted tasks. An extension to this
+ * interface is planned to allow Activities to easily cancel all the submitted tasks that are still
+ * pending in the onDestroy() method of the Activity.
+ */
+public interface AsyncTaskExecutor {
+ /**
+ * Executes the given AsyncTask with the default Executor.
+ * <p>
+ * This method <b>must only be called from the ui thread</b>.
+ * <p>
+ * The identifier supplied is any Object that can be used to identify the task later. Most
+ * commonly this will be an enum which the tests can also refer to. {@code null} is also
+ * accepted, though of course this won't help in identifying the task later.
+ */
+ <T> AsyncTask<T, ?, ?> submit(Object identifier, AsyncTask<T, ?, ?> task, T... params);
+}
diff --git a/src/com/android/dialer/util/AsyncTaskExecutors.java b/src/com/android/dialer/util/AsyncTaskExecutors.java
new file mode 100644
index 000000000..4f06e2889
--- /dev/null
+++ b/src/com/android/dialer/util/AsyncTaskExecutors.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.util;
+
+import android.os.AsyncTask;
+import android.os.Looper;
+
+import com.android.contacts.test.NeededForTesting;
+import com.google.common.base.Preconditions;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Factory methods for creating AsyncTaskExecutors.
+ * <p>
+ * All of the factory methods on this class check first to see if you have set a static
+ * {@link AsyncTaskExecutorFactory} set through the
+ * {@link #setFactoryForTest(AsyncTaskExecutorFactory)} method, and if so delegate to that instead,
+ * which is one way of injecting dependencies for testing classes whose construction cannot be
+ * controlled such as {@link android.app.Activity}.
+ */
+public final class AsyncTaskExecutors {
+ /**
+ * A single instance of the {@link AsyncTaskExecutorFactory}, to which we delegate if it is
+ * non-null, for injecting when testing.
+ */
+ private static AsyncTaskExecutorFactory mInjectedAsyncTaskExecutorFactory = null;
+
+ /**
+ * Creates an AsyncTaskExecutor that submits tasks to run with
+ * {@link AsyncTask#SERIAL_EXECUTOR}.
+ */
+ public static AsyncTaskExecutor createAsyncTaskExecutor() {
+ synchronized (AsyncTaskExecutors.class) {
+ if (mInjectedAsyncTaskExecutorFactory != null) {
+ return mInjectedAsyncTaskExecutorFactory.createAsyncTaskExeuctor();
+ }
+ return new SimpleAsyncTaskExecutor(AsyncTask.SERIAL_EXECUTOR);
+ }
+ }
+
+ /**
+ * Creates an AsyncTaskExecutor that submits tasks to run with
+ * {@link AsyncTask#THREAD_POOL_EXECUTOR}.
+ */
+ public static AsyncTaskExecutor createThreadPoolExecutor() {
+ synchronized (AsyncTaskExecutors.class) {
+ if (mInjectedAsyncTaskExecutorFactory != null) {
+ return mInjectedAsyncTaskExecutorFactory.createAsyncTaskExeuctor();
+ }
+ return new SimpleAsyncTaskExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ /** Interface for creating AsyncTaskExecutor objects. */
+ public interface AsyncTaskExecutorFactory {
+ AsyncTaskExecutor createAsyncTaskExeuctor();
+ }
+
+ @NeededForTesting
+ public static void setFactoryForTest(AsyncTaskExecutorFactory factory) {
+ synchronized (AsyncTaskExecutors.class) {
+ mInjectedAsyncTaskExecutorFactory = factory;
+ }
+ }
+
+ public static void checkCalledFromUiThread() {
+ Preconditions.checkState(Thread.currentThread() == Looper.getMainLooper().getThread(),
+ "submit method must be called from ui thread, was: " + Thread.currentThread());
+ }
+
+ private static class SimpleAsyncTaskExecutor implements AsyncTaskExecutor {
+ private final Executor mExecutor;
+
+ public SimpleAsyncTaskExecutor(Executor executor) {
+ mExecutor = executor;
+ }
+
+ @Override
+ public <T> AsyncTask<T, ?, ?> submit(Object identifer, AsyncTask<T, ?, ?> task,
+ T... params) {
+ checkCalledFromUiThread();
+ return task.executeOnExecutor(mExecutor, params);
+ }
+ }
+}
diff --git a/src/com/android/dialer/util/EmptyLoader.java b/src/com/android/dialer/util/EmptyLoader.java
new file mode 100644
index 000000000..dd4c0a330
--- /dev/null
+++ b/src/com/android/dialer/util/EmptyLoader.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.util;
+
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.Loader;
+import android.os.Bundle;
+
+/**
+ * A {@link Loader} only used to make use of the {@link android.app.Fragment#setStartDeferred}
+ * feature from an old-style fragment which doesn't use {@link Loader}s to load data.
+ *
+ * This loader never delivers results. A caller fragment must destroy it when deferred fragments
+ * should be started.
+ */
+public class EmptyLoader extends Loader<Object> {
+ public EmptyLoader(Context context) {
+ super(context);
+ }
+
+ /**
+ * {@link LoaderCallbacks} which just generates {@link EmptyLoader}. {@link #onLoadFinished}
+ * and {@link #onLoaderReset} are no-op.
+ */
+ public static class Callback implements LoaderCallbacks<Object> {
+ private final Context mContext;
+
+ public Callback(Context context) {
+ mContext = context.getApplicationContext();
+ }
+
+ @Override
+ public Loader<Object> onCreateLoader(int id, Bundle args) {
+ return new EmptyLoader(mContext);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Object> loader, Object data) {
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Object> loader) {
+ }
+ }
+}
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java b/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java
index 473d40bc6..70580badc 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java
@@ -40,9 +40,9 @@ import android.widget.SeekBar;
import android.widget.TextView;
import com.android.common.io.MoreCloseables;
-import com.android.contacts.ProximitySensorAware;
import com.android.contacts.R;
-import com.android.contacts.util.AsyncTaskExecutors;
+import com.android.dialer.ProximitySensorAware;
+import com.android.dialer.util.AsyncTaskExecutors;
import com.android.ex.variablespeed.MediaPlayerProxy;
import com.android.ex.variablespeed.VariableSpeed;
import com.google.common.base.Preconditions;
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
index 93b60de1d..c87e6778a 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -31,7 +31,7 @@ import android.view.View;
import android.widget.SeekBar;
import com.android.contacts.R;
-import com.android.contacts.util.AsyncTaskExecutor;
+import com.android.dialer.util.AsyncTaskExecutor;
import com.android.ex.variablespeed.MediaPlayerProxy;
import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy;
import com.google.common.annotations.VisibleForTesting;
diff --git a/tests/src/com/android/dialer/CallDetailActivityTest.java b/tests/src/com/android/dialer/CallDetailActivityTest.java
index 43204652a..eebb681d9 100644
--- a/tests/src/com/android/dialer/CallDetailActivityTest.java
+++ b/tests/src/com/android/dialer/CallDetailActivityTest.java
@@ -34,7 +34,7 @@ import android.test.suitebuilder.annotation.Suppress;
import android.view.Menu;
import android.widget.TextView;
-import com.android.contacts.util.AsyncTaskExecutors;
+import com.android.dialer.util.AsyncTaskExecutors;
import com.android.dialer.util.FakeAsyncTaskExecutor;
import com.android.contacts.common.test.IntegrationTestUtils;
import com.android.dialer.util.LocaleTestUtils;
diff --git a/tests/src/com/android/dialer/util/FakeAsyncTaskExecutor.java b/tests/src/com/android/dialer/util/FakeAsyncTaskExecutor.java
index 064587e4b..52cdf7e77 100644
--- a/tests/src/com/android/dialer/util/FakeAsyncTaskExecutor.java
+++ b/tests/src/com/android/dialer/util/FakeAsyncTaskExecutor.java
@@ -19,8 +19,6 @@ package com.android.dialer.util;
import android.app.Instrumentation;
import android.os.AsyncTask;
-import com.android.contacts.util.AsyncTaskExecutor;
-import com.android.contacts.util.AsyncTaskExecutors;
import com.google.common.collect.Lists;
import junit.framework.Assert;