From 91197049c458f07092b31501d2ed512180b13d58 Mon Sep 17 00:00:00 2001 From: Chiao Cheng Date: Fri, 24 Aug 2012 14:19:37 -0700 Subject: Moving more classes from contacts into dialer. - These classes are only used by dialer code. - Fixed import order. Bug: 6993891 Change-Id: I7941a029989c4793b766fdc77a4666f9f99b750a --- src/com/android/dialer/BackScrollManager.java | 89 ++++ src/com/android/dialer/CallDetailActivity.java | 15 +- src/com/android/dialer/DialtactsActivity.java | 6 +- src/com/android/dialer/PhoneCallDetailsHelper.java | 2 +- src/com/android/dialer/ProximitySensorAware.java | 33 ++ src/com/android/dialer/ProximitySensorManager.java | 237 +++++++++ src/com/android/dialer/SpecialCharSequenceMgr.java | 407 +++++++++++++++ .../dialer/calllog/CallDetailHistoryAdapter.java | 2 +- src/com/android/dialer/calllog/CallLogAdapter.java | 4 +- .../android/dialer/calllog/CallLogFragment.java | 2 +- .../dialer/calllog/CallLogListItemHelper.java | 2 +- .../dialer/calllog/CallLogListItemViews.java | 2 +- .../dialer/calllog/DefaultVoicemailNotifier.java | 2 +- src/com/android/dialer/calllog/IntentProvider.java | 2 +- .../android/dialer/dialpad/DialpadFragment.java | 4 +- .../android/dialer/list/PhoneFavoriteFragment.java | 569 +++++++++++++++++++++ .../dialer/list/PhoneFavoriteMergedAdapter.java | 301 +++++++++++ src/com/android/dialer/util/AsyncTaskExecutor.java | 48 ++ .../android/dialer/util/AsyncTaskExecutors.java | 100 ++++ src/com/android/dialer/util/EmptyLoader.java | 60 +++ .../voicemail/VoicemailPlaybackFragment.java | 4 +- .../voicemail/VoicemailPlaybackPresenter.java | 2 +- .../com/android/dialer/CallDetailActivityTest.java | 2 +- .../android/dialer/util/FakeAsyncTaskExecutor.java | 2 - 24 files changed, 1868 insertions(+), 29 deletions(-) create mode 100644 src/com/android/dialer/BackScrollManager.java create mode 100644 src/com/android/dialer/ProximitySensorAware.java create mode 100644 src/com/android/dialer/ProximitySensorManager.java create mode 100644 src/com/android/dialer/SpecialCharSequenceMgr.java create mode 100644 src/com/android/dialer/list/PhoneFavoriteFragment.java create mode 100644 src/com/android/dialer/list/PhoneFavoriteMergedAdapter.java create mode 100644 src/com/android/dialer/util/AsyncTaskExecutor.java create mode 100644 src/com/android/dialer/util/AsyncTaskExecutors.java create mode 100644 src/com/android/dialer/util/EmptyLoader.java 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. + *

+ * 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. + *

+ * This interface abstracts two possible states for the proximity sensor, near and far. + *

+ * 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. + *

+ * Contains most of the logic concerning tracking of the sensor. + *

+ * After creating an instance of this object, one should call {@link #register()} and + * {@link #unregister()} to enable and disable the notifications. + *

+ * 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. + *

+ * 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. + *

+ * The listener will start getting notifications of events. + *

+ * This method is idempotent. + */ + public void enable() { + if (mProximitySensorListener != null && !mManagerEnabled) { + mProximitySensorListener.register(); + mManagerEnabled = true; + } + } + + /** + * Disables the proximity manager. + *

+ * The listener will stop receiving notifications of events, possibly after receiving a last + * {@link Listener#onFar()} callback. + *

+ * 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. + *

+ * 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. + *

+ * 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 *#*##*#*. + * If a secret code is encountered an Intent is started with the android_secret_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 *#*##*#* + 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 { + @Override + public CursorLoader onCreateLoader(int id, Bundle args) { + if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onCreateLoader."); + return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity()); + } + + @Override + public void onLoadFinished(Loader 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 loader) { + if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoaderReset. "); + } + } + + private class AllContactsLoaderListener implements LoaderManager.LoaderCallbacks { + @Override + public Loader 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 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 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 mContactTileLoaderListener = + new ContactTileLoaderListener(); + private final LoaderManager.LoaderCallbacks 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() 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * This method must only be called from the ui thread. + *

+ * 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. + */ + AsyncTask submit(Object identifier, AsyncTask 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. + *

+ * 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 AsyncTask submit(Object identifer, AsyncTask 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 { + 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 { + private final Context mContext; + + public Callback(Context context) { + mContext = context.getApplicationContext(); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new EmptyLoader(mContext); + } + + @Override + public void onLoadFinished(Loader loader, Object data) { + } + + @Override + public void onLoaderReset(Loader 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; -- cgit v1.2.3