diff options
Diffstat (limited to 'src')
44 files changed, 2331 insertions, 4321 deletions
diff --git a/src/com/android/dialer/CallDetailActivity.java b/src/com/android/dialer/CallDetailActivity.java index 6c576508c..5dae9d07d 100644 --- a/src/com/android/dialer/CallDetailActivity.java +++ b/src/com/android/dialer/CallDetailActivity.java @@ -27,6 +27,7 @@ import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.os.PowerManager; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.provider.ContactsContract.CommonDataKinds.Phone; @@ -50,9 +51,9 @@ import android.widget.TextView; import android.widget.Toast; import com.android.contacts.common.ContactPhotoManager; -import com.android.contacts.common.CallUtil; import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.CallUtil; import com.android.dialer.calllog.CallDetailHistoryAdapter; import com.android.dialer.calllog.CallTypeHelper; import com.android.dialer.calllog.ContactInfo; @@ -62,6 +63,7 @@ import com.android.dialer.calllog.PhoneNumberDisplayHelper; import com.android.dialer.calllog.PhoneNumberUtilsWrapper; import com.android.dialer.util.AsyncTaskExecutor; import com.android.dialer.util.AsyncTaskExecutors; +import com.android.dialer.util.PrivilegedCallUtil; import com.android.dialer.util.DialerUtils; import com.android.dialer.voicemail.VoicemailPlaybackFragment; import com.android.dialer.voicemail.VoicemailStatusHelper; @@ -82,11 +84,6 @@ public class CallDetailActivity extends Activity implements ProximitySensorAware private static final char LEFT_TO_RIGHT_EMBEDDING = '\u202A'; private static final char POP_DIRECTIONAL_FORMATTING = '\u202C'; - /** The time to wait before enabling the blank the screen due to the proximity sensor. */ - private static final long PROXIMITY_BLANK_DELAY_MILLIS = 100; - /** The time to wait before disabling the blank the screen due to the proximity sensor. */ - private static final long PROXIMITY_UNBLANK_DELAY_MILLIS = 500; - /** The enumeration of {@link AsyncTask} objects used in this class. */ public enum Tasks { MARK_VOICEMAIL_READ, @@ -143,59 +140,7 @@ public class CallDetailActivity extends Activity implements ProximitySensorAware /** Whether we should show "remove from call log" in the options menu. */ private boolean mHasRemoveFromCallLogOption; - private ProximitySensorManager mProximitySensorManager; - private final ProximitySensorListener mProximitySensorListener = new ProximitySensorListener(); - - /** Listener to changes in the proximity sensor state. */ - private class ProximitySensorListener implements ProximitySensorManager.Listener { - /** Used to show a blank view and hide the action bar. */ - private final Runnable mBlankRunnable = new Runnable() { - @Override - public void run() { - View blankView = findViewById(R.id.blank); - blankView.setVisibility(View.VISIBLE); - getActionBar().hide(); - } - }; - /** Used to remove the blank view and show the action bar. */ - private final Runnable mUnblankRunnable = new Runnable() { - @Override - public void run() { - View blankView = findViewById(R.id.blank); - blankView.setVisibility(View.GONE); - getActionBar().show(); - } - }; - - @Override - public synchronized void onNear() { - clearPendingRequests(); - postDelayed(mBlankRunnable, PROXIMITY_BLANK_DELAY_MILLIS); - } - - @Override - public synchronized void onFar() { - clearPendingRequests(); - postDelayed(mUnblankRunnable, PROXIMITY_UNBLANK_DELAY_MILLIS); - } - - /** Removed any delayed requests that may be pending. */ - public synchronized void clearPendingRequests() { - View blankView = findViewById(R.id.blank); - blankView.removeCallbacks(mBlankRunnable); - blankView.removeCallbacks(mUnblankRunnable); - } - - /** Post a {@link Runnable} with a delay on the main thread. */ - private synchronized void postDelayed(Runnable runnable, long delayMillis) { - // Post these instead of executing immediately so that: - // - They are guaranteed to be executed on the main thread. - // - If the sensor values changes rapidly for some time, the UI will not be - // updated immediately. - View blankView = findViewById(R.id.blank); - blankView.postDelayed(runnable, delayMillis); - } - } + private PowerManager.WakeLock mProximityWakeLock; static final String[] CALL_LOG_PROJECTION = new String[] { CallLog.Calls.DATE, @@ -244,12 +189,20 @@ public class CallDetailActivity extends Activity implements ProximitySensorAware mQuickContactBadge = (QuickContactBadge) findViewById(R.id.quick_contact_photo); mQuickContactBadge.setOverlay(null); + mQuickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); mCallerName = (TextView) findViewById(R.id.caller_name); mCallerNumber = (TextView) findViewById(R.id.caller_number); mAccountLabel = (TextView) findViewById(R.id.phone_account_label); mDefaultCountryIso = GeoUtil.getCurrentCountryIso(this); mContactPhotoManager = ContactPhotoManager.getInstance(this); - mProximitySensorManager = new ProximitySensorManager(this, mProximitySensorListener); + final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { + mProximityWakeLock = powerManager.newWakeLock( + PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); + } else { + mProximityWakeLock = null; + } + mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this)); getActionBar().setDisplayHomeAsUpEnabled(true); @@ -364,8 +317,8 @@ public class CallDetailActivity extends Activity implements ProximitySensorAware getSystemService(Context.TELEPHONY_SERVICE); if (tm.getCallState() == TelephonyManager.CALL_STATE_IDLE) { DialerUtils.startActivityWithErrorToast(this, - CallUtil.getCallIntent(Uri.fromParts(PhoneAccount.SCHEME_TEL, mNumber, - null)), R.string.call_not_available); + PrivilegedCallUtil.getCallIntent(Uri.fromParts(PhoneAccount.SCHEME_TEL, + mNumber, null)), R.string.call_not_available); return true; } } @@ -414,7 +367,8 @@ public class CallDetailActivity extends Activity implements ProximitySensorAware // We know that all calls are from the same number and the same contact, so pick the // first. PhoneCallDetails firstDetails = details[0]; - mNumber = firstDetails.number.toString(); + mNumber = TextUtils.isEmpty(firstDetails.number) ? + null : firstDetails.number.toString(); final int numberPresentation = firstDetails.numberPresentation; final Uri contactUri = firstDetails.contactUri; final Uri photoUri = firstDetails.photoUri; @@ -750,18 +704,34 @@ public class CallDetailActivity extends Activity implements ProximitySensorAware protected void onPause() { // Immediately stop the proximity sensor. disableProximitySensor(false); - mProximitySensorListener.clearPendingRequests(); super.onPause(); } @Override public void enableProximitySensor() { - mProximitySensorManager.enable(); + if (mProximityWakeLock == null) { + return; + } + if (!mProximityWakeLock.isHeld()) { + Log.i(TAG, "Acquiring proximity wake lock"); + mProximityWakeLock.acquire(); + } else { + Log.i(TAG, "Proximity wake lock already acquired"); + } } @Override public void disableProximitySensor(boolean waitForFarState) { - mProximitySensorManager.disable(waitForFarState); + if (mProximityWakeLock == null) { + return; + } + if (mProximityWakeLock.isHeld()) { + Log.i(TAG, "Releasing proximity wake lock"); + int flags = (waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0); + mProximityWakeLock.release(flags); + } else { + Log.i(TAG, "Proximity wake lock already released"); + } } private void closeSystemDialogs() { diff --git a/src/com/android/dialer/DialerApplication.java b/src/com/android/dialer/DialerApplication.java index c64530829..7bc3bb4d3 100644 --- a/src/com/android/dialer/DialerApplication.java +++ b/src/com/android/dialer/DialerApplication.java @@ -17,6 +17,7 @@ package com.android.dialer; import android.app.Application; +import android.os.Trace; import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.common.extensions.ExtensionsFactory; @@ -24,26 +25,19 @@ import com.android.contacts.commonbind.analytics.AnalyticsUtil; public class DialerApplication extends Application { + private static final String TAG = "DialerApplication"; private ContactPhotoManager mContactPhotoManager; @Override public void onCreate() { + Trace.beginSection(TAG + " onCreate"); super.onCreate(); + Trace.beginSection(TAG + " ExtensionsFactory initialization"); ExtensionsFactory.init(getApplicationContext()); + Trace.endSection(); + Trace.beginSection(TAG + " Analytics initialization"); AnalyticsUtil.initialize(this); - } - - @Override - public Object getSystemService(String name) { - if (ContactPhotoManager.CONTACT_PHOTO_SERVICE.equals(name)) { - if (mContactPhotoManager == null) { - mContactPhotoManager = ContactPhotoManager.createContactPhotoManager(this); - registerComponentCallbacks(mContactPhotoManager); - mContactPhotoManager.preloadPhotosInBackground(); - } - return mContactPhotoManager; - } - - return super.getSystemService(name); + Trace.endSection(); + Trace.endSection(); } } diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java index 95b72157e..787b6fae4 100644 --- a/src/com/android/dialer/DialtactsActivity.java +++ b/src/com/android/dialer/DialtactsActivity.java @@ -28,6 +28,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; +import android.os.Trace; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Intents; import android.speech.RecognizerIntent; @@ -58,7 +59,6 @@ import android.widget.ImageButton; import android.widget.PopupMenu; import android.widget.Toast; -import com.android.contacts.common.CallUtil; import com.android.contacts.common.activity.TransactionSafeActivity; import com.android.contacts.common.dialog.ClearFrequentsDialog; import com.android.contacts.common.interactions.ImportExportDialogFragment; @@ -82,6 +82,7 @@ import com.android.dialer.list.SearchFragment; import com.android.dialer.list.SmartDialSearchFragment; import com.android.dialer.list.SpeedDialFragment; import com.android.dialer.settings.DialerSettingsActivity; +import com.android.dialer.util.PrivilegedCallUtil; import com.android.dialer.util.DialerUtils; import com.android.dialer.widget.ActionBarController; import com.android.dialer.widget.SearchEditTextLayout; @@ -90,6 +91,8 @@ import com.android.dialerbind.DatabaseHelperManager; import com.android.phone.common.animation.AnimUtils; import com.android.phone.common.animation.AnimationListenerAdapter; +import junit.framework.Assert; + import java.util.ArrayList; import java.util.List; @@ -235,7 +238,7 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O */ private String mVoiceSearchQuery; - private class OptionsPopupMenu extends PopupMenu { + protected class OptionsPopupMenu extends PopupMenu { public OptionsPopupMenu(Context context, View anchor) { super(context, anchor, Gravity.END); } @@ -350,27 +353,30 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O @Override protected void onCreate(Bundle savedInstanceState) { + Trace.beginSection(TAG + " onCreate"); super.onCreate(savedInstanceState); mFirstLaunch = true; final Resources resources = getResources(); mActionBarHeight = resources.getDimensionPixelSize(R.dimen.action_bar_height_large); + Trace.beginSection(TAG + " setContentView"); setContentView(R.layout.dialtacts_activity); + Trace.endSection(); getWindow().setBackgroundDrawable(null); + Trace.beginSection(TAG + " setup Views"); final ActionBar actionBar = getActionBar(); actionBar.setCustomView(R.layout.search_edittext); actionBar.setDisplayShowCustomEnabled(true); actionBar.setBackgroundDrawable(null); - mActionBarController = new ActionBarController(this, - (SearchEditTextLayout) actionBar.getCustomView()); - SearchEditTextLayout searchEditTextLayout = - (SearchEditTextLayout) actionBar.getCustomView(); + (SearchEditTextLayout) actionBar.getCustomView().findViewById(R.id.search_view_container); searchEditTextLayout.setPreImeKeyListener(mSearchEditTextLayoutListener); + mActionBarController = new ActionBarController(this, searchEditTextLayout); + mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view); mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener); mVoiceSearchButton = searchEditTextLayout.findViewById(R.id.voice_search_button); @@ -401,12 +407,11 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O mOverflowMenu = buildOptionsMenu(searchEditTextLayout); optionsMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener()); - // Add the favorites fragment, and the dialpad fragment, but only if savedInstanceState - // is null. Otherwise the fragment manager takes care of recreating these fragments. + // Add the favorites fragment but only if savedInstanceState is null. Otherwise the + // fragment manager is responsible for recreating it. if (savedInstanceState == null) { getFragmentManager().beginTransaction() .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT) - .add(R.id.dialtacts_container, new DialpadFragment(), TAG_DIALPAD_FRAGMENT) .commit(); } else { mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY); @@ -453,8 +458,13 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O setupActivityOverlay(); + Trace.endSection(); + + Trace.beginSection(TAG + " initialize smart dialing"); mDialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper(this); SmartDialPrefix.initializeNanpSettings(this); + Trace.endSection(); + Trace.endSection(); } private void setupActivityOverlay() { @@ -472,6 +482,7 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O @Override protected void onResume() { + Trace.beginSection(TAG + " onResume"); super.onResume(); mStateSaved = false; if (mFirstLaunch) { @@ -506,6 +517,7 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O prepareVoiceSearchButton(); mDialerDatabaseHelper.startSmartDialUpdateThread(); updateFloatingActionButtonControllerAlignment(false /* animate */); + Trace.endSection(); } @Override @@ -542,7 +554,7 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O public void onAttachFragment(Fragment fragment) { if (fragment instanceof DialpadFragment) { mDialpadFragment = (DialpadFragment) fragment; - if (!mShowDialpadOnResume) { + if (!mIsDialpadShown && !mShowDialpadOnResume) { final FragmentTransaction transaction = getFragmentManager().beginTransaction(); transaction.hide(mDialpadFragment); transaction.commit(); @@ -655,12 +667,19 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O return; } mIsDialpadShown = true; - mDialpadFragment.setAnimate(animate); + mListsFragment.setUserVisibleHint(false); - AnalyticsUtil.sendScreenView(mDialpadFragment); final FragmentTransaction ft = getFragmentManager().beginTransaction(); - ft.show(mDialpadFragment); + if (mDialpadFragment == null) { + mDialpadFragment = new DialpadFragment(); + ft.add(R.id.dialtacts_container, mDialpadFragment, TAG_DIALPAD_FRAGMENT); + } else { + ft.show(mDialpadFragment); + } + + mDialpadFragment.setAnimate(animate); + AnalyticsUtil.sendScreenView(mDialpadFragment); ft.commit(); if (animate) { @@ -679,6 +698,7 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O * Callback from child DialpadFragment when the dialpad is shown. */ public void onDialpadShown() { + Assert.assertNotNull(mDialpadFragment); if (mDialpadFragment.getAnimate()) { mDialpadFragment.getView().startAnimation(mSlideIn); } else { @@ -730,7 +750,7 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O * Finishes hiding the dialpad fragment after any animations are completed. */ private void commitDialpadFragmentHide() { - if (!mStateSaved && !mDialpadFragment.isHidden()) { + if (!mStateSaved && mDialpadFragment != null && !mDialpadFragment.isHidden()) { final FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.hide(mDialpadFragment); ft.commit(); @@ -788,7 +808,7 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O } } - private OptionsPopupMenu buildOptionsMenu(View invoker) { + protected OptionsPopupMenu buildOptionsMenu(View invoker) { final OptionsPopupMenu popupMenu = new OptionsPopupMenu(this, invoker); popupMenu.inflate(R.menu.dialtacts_options); final Menu menu = popupMenu.getMenu(); @@ -838,14 +858,12 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O return; } - if (mDialpadFragment != null) { - final boolean phoneIsInUse = phoneIsInUse(); - if (phoneIsInUse || (intent.getData() != null && isDialIntent(intent))) { - mDialpadFragment.setStartedFromNewIntent(true); - if (phoneIsInUse && !mDialpadFragment.isVisible()) { - mInCallDialpadUp = true; - } - showDialpadFragment(false); + final boolean phoneIsInUse = phoneIsInUse(); + if (phoneIsInUse || (intent.getData() != null && isDialIntent(intent))) { + showDialpadFragment(false); + mDialpadFragment.setStartedFromNewIntent(true); + if (phoneIsInUse && !mDialpadFragment.isVisible()) { + mInCallDialpadUp = true; } } } @@ -947,7 +965,11 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O } mSearchView.setText(null); - mDialpadFragment.clearDialpad(); + + if (mDialpadFragment != null) { + mDialpadFragment.clearDialpad(); + } + setNotInSearchUi(); final FragmentTransaction transaction = getFragmentManager().beginTransaction(); @@ -960,7 +982,8 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O transaction.commit(); mListsFragment.getView().animate().alpha(1).withLayer(); - if (!mDialpadFragment.isVisible()) { + + if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { // If the dialpad fragment wasn't previously visible, then send a screen view because // we are exiting regular search. Otherwise, the screen view will be sent by // {@link #hideDialpadFragment}. @@ -1090,9 +1113,6 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O */ @Override public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) { - if (mListsFragment.isPaneOpen()) { - mActionBarController.setAlpha(ListsFragment.REMOVE_VIEW_SHOWN_ALPHA); - } mListsFragment.showRemoveView(true); } @@ -1105,9 +1125,6 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O */ @Override public void onDragFinished(int x, int y) { - if (mListsFragment.isPaneOpen()) { - mActionBarController.setAlpha(ListsFragment.REMOVE_VIEW_HIDDEN_ALPHA); - } mListsFragment.showRemoveView(false); } @@ -1141,8 +1158,8 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O @Override public void onCallNumberDirectly(String phoneNumber, boolean isVideoCall) { Intent intent = isVideoCall ? - CallUtil.getVideoCallIntent(phoneNumber, getCallOrigin()) : - CallUtil.getCallIntent(phoneNumber, getCallOrigin()); + PrivilegedCallUtil.getVideoCallIntent(phoneNumber, getCallOrigin()) : + PrivilegedCallUtil.getCallIntent(phoneNumber, getCallOrigin()); DialerUtils.startActivityWithErrorToast(this, intent); mClearSearchOnPause = true; } diff --git a/src/com/android/dialer/PhoneCallDetailsHelper.java b/src/com/android/dialer/PhoneCallDetailsHelper.java index b4e817174..7855a1d13 100644 --- a/src/com/android/dialer/PhoneCallDetailsHelper.java +++ b/src/com/android/dialer/PhoneCallDetailsHelper.java @@ -28,7 +28,6 @@ import android.text.format.DateUtils; import android.view.View; import android.widget.TextView; -import com.android.contacts.common.CallUtil; import com.android.contacts.common.testing.NeededForTesting; import com.android.contacts.common.util.PhoneNumberHelper; import com.android.dialer.calllog.ContactInfo; diff --git a/src/com/android/dialer/ProximitySensorManager.java b/src/com/android/dialer/ProximitySensorManager.java deleted file mode 100644 index 42d740fc1..000000000 --- a/src/com/android/dialer/ProximitySensorManager.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * 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 index f466c417c..1e686125f 100644 --- a/src/com/android/dialer/SpecialCharSequenceMgr.java +++ b/src/com/android/dialer/SpecialCharSequenceMgr.java @@ -18,6 +18,7 @@ package com.android.dialer; import android.app.Activity; import android.app.AlertDialog; +import android.app.DialogFragment; import android.app.KeyguardManager; import android.app.ProgressDialog; import android.content.ActivityNotFoundException; @@ -33,6 +34,7 @@ import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; import android.telephony.PhoneNumberUtils; import android.telephony.TelephonyManager; +import android.text.TextUtils; import android.util.Log; import android.view.WindowManager; import android.widget.EditText; @@ -63,6 +65,8 @@ import java.util.List; public class SpecialCharSequenceMgr { private static final String TAG = "SpecialCharSequenceMgr"; + private static final String TAG_SELECT_ACCT_FRAGMENT = "tag_select_acct_fragment"; + private static final String SECRET_CODE_ACTION = "android.provider.Telephony.SECRET_CODE"; private static final String MMI_IMEI_DISPLAY = "*#06#"; private static final String MMI_REGULATORY_INFO_DISPLAY = "*#07#"; @@ -228,9 +232,12 @@ public class SpecialCharSequenceMgr { public void onDialogDismissed() {} }; - SelectPhoneAccountDialogFragment.showAccountDialog( - ((Activity) context).getFragmentManager(), subscriptionAccountHandles, - listener); + // NOTE: If you want to support rotation of this dialog need + // to refactor the listener and set it in DialpadFragment.onCreate() + DialogFragment dialogFragment = SelectPhoneAccountDialogFragment.newInstance( + subscriptionAccountHandles, listener); + dialogFragment.show(((Activity) context).getFragmentManager(), + TAG_SELECT_ACCT_FRAGMENT); } else { return false; } @@ -289,9 +296,12 @@ public class SpecialCharSequenceMgr { public void onDialogDismissed() {} }; - SelectPhoneAccountDialogFragment.showAccountDialog( - ((Activity) context).getFragmentManager(), subscriptionAccountHandles, - listener); + // NOTE: If you want to support rotation of this dialog need + // to refactor the listener and set it in DialpadFragment.onCreate() + DialogFragment dialogFragment = SelectPhoneAccountDialogFragment.newInstance( + subscriptionAccountHandles, listener); + dialogFragment.show(((Activity) context).getFragmentManager(), + TAG_SELECT_ACCT_FRAGMENT); } return true; } @@ -310,13 +320,16 @@ public class SpecialCharSequenceMgr { List<String> deviceIds = new ArrayList<String>(); for (int slot = 0; slot < telephonyManager.getPhoneCount(); slot++) { - deviceIds.add(telephonyManager.getDeviceId(slot)); + String deviceId = telephonyManager.getDeviceId(slot); + if (!TextUtils.isEmpty(deviceId)) { + deviceIds.add(deviceId); + } } AlertDialog alert = new AlertDialog.Builder(context) .setTitle(labelResId) .setItems(deviceIds.toArray(new String[deviceIds.size()]), null) - .setPositiveButton(R.string.ok, null) + .setPositiveButton(android.R.string.ok, null) .setCancelable(false) .show(); return true; diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java index 225d34948..3e8efa0b7 100644 --- a/src/com/android/dialer/calllog/CallLogAdapter.java +++ b/src/com/android/dialer/calllog/CallLogAdapter.java @@ -16,18 +16,12 @@ package com.android.dialer.calllog; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.Loader; import android.content.res.Resources; import android.database.Cursor; -import android.database.sqlite.SQLiteFullException; import android.net.Uri; -import android.os.Handler; -import android.os.Message; -import android.provider.CallLog.Calls; -import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.PhoneLookup; import android.telecom.PhoneAccountHandle; import android.telephony.PhoneNumberUtils; @@ -37,34 +31,22 @@ import android.view.LayoutInflater; import android.view.View; import android.view.View.AccessibilityDelegate; import android.view.ViewGroup; -import android.view.ViewStub; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityEvent; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; - -import com.android.common.widget.GroupingListAdapter; -import com.android.contacts.common.CallUtil; -import com.android.contacts.common.ContactPhotoManager; -import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; -import com.android.contacts.common.util.PhoneNumberHelper; -import com.android.contacts.common.model.Contact; -import com.android.contacts.common.model.ContactLoader; + import com.android.contacts.common.util.UriUtils; -import com.android.dialer.DialtactsActivity; import com.android.dialer.PhoneCallDetails; import com.android.dialer.PhoneCallDetailsHelper; import com.android.dialer.R; +import com.android.dialer.contactinfo.ContactInfoCache; +import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener; import com.android.dialer.util.DialerUtils; -import com.android.dialer.util.ExpirableCache; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Objects; -import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedList; /** * Adapter class to fill in data for the Call Log. @@ -73,13 +55,6 @@ public class CallLogAdapter extends GroupingListAdapter implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator { private static final String TAG = CallLogAdapter.class.getSimpleName(); - private static final int VOICEMAIL_TRANSCRIPTION_MAX_LINES = 10; - - /** The enumeration of {@link android.os.AsyncTask} objects used in this class. */ - public enum Tasks { - REMOVE_CALL_LOG_ENTRIES, - } - /** Interface used to inform a parent UI element that a list item has been expanded. */ public interface CallItemExpandedListener { /** @@ -108,68 +83,16 @@ public class CallLogAdapter extends GroupingListAdapter public void onReportButtonClick(String number); } - /** - * Stores a phone number of a call with the country code where it originally occurred. - * <p> - * Note the country does not necessarily specifies the country of the phone number itself, but - * it is the country in which the user was in when the call was placed or received. - */ - private static final class NumberWithCountryIso { - public final String number; - public final String countryIso; - - public NumberWithCountryIso(String number, String countryIso) { - this.number = number; - this.countryIso = countryIso; - } - - @Override - public boolean equals(Object o) { - if (o == null) return false; - if (!(o instanceof NumberWithCountryIso)) return false; - NumberWithCountryIso other = (NumberWithCountryIso) o; - return TextUtils.equals(number, other.number) - && TextUtils.equals(countryIso, other.countryIso); - } - - @Override - public int hashCode() { - return (number == null ? 0 : number.hashCode()) - ^ (countryIso == null ? 0 : countryIso.hashCode()); - } - } - - /** The time in millis to delay starting the thread processing requests. */ - private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000; - - /** The size of the cache of contact info. */ - private static final int CONTACT_INFO_CACHE_SIZE = 100; - /** Constant used to indicate no row is expanded. */ private static final long NONE_EXPANDED = -1; protected final Context mContext; private final ContactInfoHelper mContactInfoHelper; private final CallFetcher mCallFetcher; - private final Toast mReportedToast; private final OnReportButtonClickListener mOnReportButtonClickListener; private ViewTreeObserver mViewTreeObserver = null; - /** - * A cache of the contact details for the phone numbers in the call log. - * <p> - * The content of the cache is expired (but not purged) whenever the application comes to - * the foreground. - * <p> - * The key is number with the country in which the call was placed or received. - */ - private ExpirableCache<NumberWithCountryIso, ContactInfo> mContactInfoCache; - - /** - * Tracks the call log row which was previously expanded. Used so that the closure of a - * previously expanded call log entry can be animated on rebind. - */ - private long mPreviouslyExpanded = NONE_EXPANDED; + protected ContactInfoCache mContactInfoCache; /** * Tracks the currently expanded call log row. @@ -190,71 +113,11 @@ public class CallLogAdapter extends GroupingListAdapter */ private HashMap<Long,Integer> mDayGroups = new HashMap<Long, Integer>(); - /** - * A request for contact details for the given number. - */ - private static final class ContactInfoRequest { - /** The number to look-up. */ - public final String number; - /** The country in which a call to or from this number was placed or received. */ - public final String countryIso; - /** The cached contact information stored in the call log. */ - public final ContactInfo callLogInfo; - - public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) { - this.number = number; - this.countryIso = countryIso; - this.callLogInfo = callLogInfo; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (!(obj instanceof ContactInfoRequest)) return false; - - ContactInfoRequest other = (ContactInfoRequest) obj; - - if (!TextUtils.equals(number, other.number)) return false; - if (!TextUtils.equals(countryIso, other.countryIso)) return false; - if (!Objects.equal(callLogInfo, other.callLogInfo)) return false; - - return true; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode()); - result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode()); - result = prime * result + ((number == null) ? 0 : number.hashCode()); - return result; - } - } - - /** - * List of requests to update contact details. - * <p> - * Each request is made of a phone number to look up, and the contact info currently stored in - * the call log for this number. - * <p> - * The requests are added when displaying the contacts and are processed by a background - * thread. - */ - private final LinkedList<ContactInfoRequest> mRequests; - private boolean mLoading = true; - private static final int REDRAW = 1; - private static final int START_THREAD = 2; - - private QueryThread mCallerIdThread; /** Instance of helper class for managing views. */ private final CallLogListItemHelper mCallLogViewsHelper; - /** Helper to set up contact photos. */ - private final ContactPhotoManager mContactPhotoManager; /** Helper to parse and process phone numbers. */ private PhoneNumberDisplayHelper mPhoneNumberHelper; /** Helper to access Telephony phone number utils class */ @@ -264,20 +127,6 @@ public class CallLogAdapter extends GroupingListAdapter private CallItemExpandedListener mCallItemExpandedListener; - /** Can be set to true by tests to disable processing of requests. */ - private volatile boolean mRequestProcessingDisabled = false; - - private boolean mIsCallLog = true; - - private View mBadgeContainer; - private ImageView mBadgeImageView; - private TextView mBadgeText; - - private int mCallLogBackgroundColor; - private int mExpandedBackgroundColor; - private float mExpandedTranslationZ; - private int mPhotoSize; - /** Listener for the primary or secondary actions in the list. * Primary opens the call details. * Secondary calls or plays. @@ -285,7 +134,14 @@ public class CallLogAdapter extends GroupingListAdapter private final View.OnClickListener mActionListener = new View.OnClickListener() { @Override public void onClick(View view) { - startActivityForAction(view); + final IntentProvider intentProvider = (IntentProvider) view.getTag(); + if (intentProvider != null) { + final Intent intent = intentProvider.getIntent(mContext); + // See IntentProvider.getCallDetailIntentProvider() for why this may be null. + if (intent != null) { + DialerUtils.startActivityWithErrorToast(mContext, intent); + } + } } }; @@ -297,94 +153,63 @@ public class CallLogAdapter extends GroupingListAdapter @Override public void onClick(View v) { final View callLogItem = (View) v.getParent().getParent(); - handleRowExpanded(callLogItem, true /* animate */, false /* forceExpand */); + handleRowExpanded(callLogItem, false /* forceExpand */); } }; + protected final OnContactInfoChangedListener mOnContactInfoChangedListener = + new OnContactInfoChangedListener() { + @Override + public void onContactInfoChanged() { + notifyDataSetChanged(); + } + }; + private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() { @Override public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) { - handleRowExpanded(host, false /* animate */, - true /* forceExpand */); + handleRowExpanded(host, true /* forceExpand */); } return super.onRequestSendAccessibilityEvent(host, child, event); } }; - private void startActivityForAction(View view) { - final IntentProvider intentProvider = (IntentProvider) view.getTag(); - if (intentProvider != null) { - final Intent intent = intentProvider.getIntent(mContext); - // See IntentProvider.getCallDetailIntentProvider() for why this may be null. - if (intent != null) { - DialerUtils.startActivityWithErrorToast(mContext, intent); - } - } - } - @Override public boolean onPreDraw() { // We only wanted to listen for the first draw (and this is it). unregisterPreDrawListener(); - // Only schedule a thread-creation message if the thread hasn't been - // created yet. This is purely an optimization, to queue fewer messages. - if (mCallerIdThread == null) { - mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MILLIS); - } - + mContactInfoCache.start(); return true; } - private Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case REDRAW: - notifyDataSetChanged(); - break; - case START_THREAD: - startRequestProcessing(); - break; - } - } - }; - - public CallLogAdapter(Context context, CallFetcher callFetcher, - ContactInfoHelper contactInfoHelper, CallItemExpandedListener callItemExpandedListener, - OnReportButtonClickListener onReportButtonClickListener, boolean isCallLog) { + public CallLogAdapter( + Context context, + CallFetcher callFetcher, + ContactInfoHelper contactInfoHelper, + OnReportButtonClickListener onReportButtonClickListener) { super(context); mContext = context; mCallFetcher = callFetcher; mContactInfoHelper = contactInfoHelper; - mIsCallLog = isCallLog; - mCallItemExpandedListener = callItemExpandedListener; mOnReportButtonClickListener = onReportButtonClickListener; - mReportedToast = Toast.makeText(mContext, R.string.toast_caller_id_reported, - Toast.LENGTH_SHORT); - mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); - mRequests = new LinkedList<ContactInfoRequest>(); + mContactInfoCache = new ContactInfoCache( + mContactInfoHelper, mOnContactInfoChangedListener); Resources resources = mContext.getResources(); CallTypeHelper callTypeHelper = new CallTypeHelper(resources); - mCallLogBackgroundColor = resources.getColor(R.color.background_dialer_list_items); - mExpandedBackgroundColor = resources.getColor(R.color.call_log_expanded_background_color); - mExpandedTranslationZ = resources.getDimension(R.dimen.call_log_expanded_translation_z); - mPhotoSize = resources.getDimensionPixelSize(R.dimen.contact_photo_size); - mContactPhotoManager = ContactPhotoManager.getInstance(mContext); mPhoneNumberHelper = new PhoneNumberDisplayHelper(mContext, resources); mPhoneNumberUtilsWrapper = new PhoneNumberUtilsWrapper(mContext); PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(mContext, resources, mPhoneNumberUtilsWrapper); mCallLogViewsHelper = - new CallLogListItemHelper( - phoneCallDetailsHelper, mPhoneNumberHelper, resources); + new CallLogListItemHelper(phoneCallDetailsHelper, mPhoneNumberHelper, resources); mCallLogGroupBuilder = new CallLogGroupBuilder(this); } @@ -411,37 +236,6 @@ public class CallLogAdapter extends GroupingListAdapter } /** - * Starts a background thread to process contact-lookup requests, unless one - * has already been started. - */ - private synchronized void startRequestProcessing() { - // For unit-testing. - if (mRequestProcessingDisabled) return; - - // Idempotence... if a thread is already started, don't start another. - if (mCallerIdThread != null) return; - - mCallerIdThread = new QueryThread(); - mCallerIdThread.setPriority(Thread.MIN_PRIORITY); - mCallerIdThread.start(); - } - - /** - * Stops the background thread that processes updates and cancels any - * pending requests to start it. - */ - public synchronized void stopRequestProcessing() { - // Remove any pending requests to start the processing thread. - mHandler.removeMessages(START_THREAD); - if (mCallerIdThread != null) { - // Stop the thread; we are finished with it. - mCallerIdThread.stopProcessing(); - mCallerIdThread.interrupt(); - mCallerIdThread = null; - } - } - - /** * Stop receiving onPreDraw() notifications. */ private void unregisterPreDrawListener() { @@ -452,133 +246,14 @@ public class CallLogAdapter extends GroupingListAdapter } public void invalidateCache() { - mContactInfoCache.expireAll(); + mContactInfoCache.invalidate(); // Restart the request-processing thread after the next draw. - stopRequestProcessing(); unregisterPreDrawListener(); } - /** - * Enqueues a request to look up the contact details for the given phone number. - * <p> - * It also provides the current contact info stored in the call log for this number. - * <p> - * If the {@code immediate} parameter is true, it will start immediately the thread that looks - * up the contact information (if it has not been already started). Otherwise, it will be - * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}. - */ - protected void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo, - boolean immediate) { - ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo); - synchronized (mRequests) { - if (!mRequests.contains(request)) { - mRequests.add(request); - mRequests.notifyAll(); - } - } - if (immediate) startRequestProcessing(); - } - - /** - * Queries the appropriate content provider for the contact associated with the number. - * <p> - * Upon completion it also updates the cache in the call log, if it is different from - * {@code callLogInfo}. - * <p> - * The number might be either a SIP address or a phone number. - * <p> - * It returns true if it updated the content of the cache and we should therefore tell the - * view to update its content. - */ - private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) { - final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso); - - if (info == null) { - // The lookup failed, just return without requesting to update the view. - return false; - } - - // Check the existing entry in the cache: only if it has changed we should update the - // view. - NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); - ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso); - - final boolean isRemoteSource = info.sourceType != 0; - - // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY} - // to avoid updating the data set for every new row that is scrolled into view. - // see (https://googleplex-android-review.git.corp.google.com/#/c/166680/) - - // Exception: Photo uris for contacts from remote sources are not cached in the call log - // cache, so we have to force a redraw for these contacts regardless. - boolean updated = (existingInfo != ContactInfo.EMPTY || isRemoteSource) && - !info.equals(existingInfo); - - // Store the data in the cache so that the UI thread can use to display it. Store it - // even if it has not changed so that it is marked as not expired. - mContactInfoCache.put(numberCountryIso, info); - // Update the call log even if the cache it is up-to-date: it is possible that the cache - // contains the value from a different call log entry. - updateCallLogContactInfoCache(number, countryIso, info, callLogInfo); - return updated; - } - - /* - * Handles requests for contact name and number type. - */ - private class QueryThread extends Thread { - private volatile boolean mDone = false; - - public QueryThread() { - super("CallLogAdapter.QueryThread"); - } - - public void stopProcessing() { - mDone = true; - } - - @Override - public void run() { - boolean needRedraw = false; - while (true) { - // Check if thread is finished, and if so return immediately. - if (mDone) return; - - // Obtain next request, if any is available. - // Keep synchronized section small. - ContactInfoRequest req = null; - synchronized (mRequests) { - if (!mRequests.isEmpty()) { - req = mRequests.removeFirst(); - } - } - - if (req != null) { - // Process the request. If the lookup succeeds, schedule a - // redraw. - needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo); - } else { - // Throttle redraw rate by only sending them when there are - // more requests. - if (needRedraw) { - needRedraw = false; - mHandler.sendEmptyMessage(REDRAW); - } - - // Wait until another request is available, or until this - // thread is no longer needed (as indicated by being - // interrupted). - try { - synchronized (mRequests) { - mRequests.wait(1000); - } - } catch (InterruptedException ie) { - // Ignore, and attempt to continue processing requests. - } - } - } - } + public void pauseCache() { + mContactInfoCache.stop(); } @Override @@ -602,7 +277,7 @@ public class CallLogAdapter extends GroupingListAdapter View view = inflater.inflate(R.layout.call_log_list_item, parent, false); // Get the views to bind to and cache them. - CallLogListItemViews views = CallLogListItemViews.fromView(view); + CallLogListItemViews views = CallLogListItemViews.fromView(context, view); view.setTag(views); // Set text height to false on the TextViews so they don't have extra padding. @@ -640,7 +315,7 @@ public class CallLogAdapter extends GroupingListAdapter * @param c the cursor pointing to the entry in the call log * @param count the number of entries in the current item, greater than 1 if it is a group */ - private void bindView(View callLogItemView, Cursor c, int count) { + public void bindView(View callLogItemView, Cursor c, int count) { callLogItemView.setAccessibilityDelegate(mAccessibilityDelegate); final CallLogListItemViews views = (CallLogListItemViews) callLogItemView.getTag(); @@ -660,17 +335,12 @@ public class CallLogAdapter extends GroupingListAdapter final long rowId = c.getLong(CallLogQuery.ID); views.rowId = rowId; - // For entries in the call log, check if the day group has changed and display a header - // if necessary. - if (mIsCallLog) { - int currentGroup = getDayGroupForCall(rowId); - int previousGroup = getPreviousDayGroup(c); - if (currentGroup != previousGroup) { - views.dayGroupHeader.setVisibility(View.VISIBLE); - views.dayGroupHeader.setText(getGroupDescription(currentGroup)); - } else { - views.dayGroupHeader.setVisibility(View.GONE); - } + // Check if the day group has changed and display a header if necessary. + int currentGroup = getDayGroupForCall(rowId); + int previousGroup = getPreviousDayGroup(c); + if (currentGroup != previousGroup) { + views.dayGroupHeader.setVisibility(View.VISIBLE); + views.dayGroupHeader.setText(getGroupDescription(currentGroup)); } else { views.dayGroupHeader.setVisibility(View.GONE); } @@ -685,74 +355,22 @@ public class CallLogAdapter extends GroupingListAdapter // Stash away the Ids of the calls so that we can support deleting a row in the call log. views.callIds = getCallIds(c, count); - final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c); + final ContactInfo cachedContactInfo = mContactInfoHelper.getContactInfo(c); final boolean isVoicemailNumber = mPhoneNumberUtilsWrapper.isVoicemailNumber(accountHandle, number); - // Where binding and not in the call log, use default behaviour of invoking a call when - // tapping the primary view. - if (!mIsCallLog) { - views.primaryActionView.setOnClickListener(this.mActionListener); - - // Set return call intent, otherwise null. - if (PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) { - // Sets the primary action to call the number. - if (isVoicemailNumber) { - views.primaryActionView.setTag( - IntentProvider.getReturnVoicemailCallIntentProvider()); - } else { - views.primaryActionView.setTag( - IntentProvider.getReturnCallIntentProvider(number)); - } - } else { - // Number is not callable, so hide button. - views.primaryActionView.setTag(null); - } - } else { - // In the call log, expand/collapse an actions section for the call log entry when - // the primary view is tapped. - views.primaryActionView.setOnClickListener(this.mExpandCollapseListener); - - // Note: Binding of the action buttons is done as required in configureActionViews - // when the user expands the actions ViewStub. - } + // Expand/collapse an actions section for the call log entry when the primary view is tapped. + views.primaryActionView.setOnClickListener(mExpandCollapseListener); - // Lookup contacts with this number - NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); - ExpirableCache.CachedValue<ContactInfo> cachedInfo = - mContactInfoCache.getCachedValue(numberCountryIso); - ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); - if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation) - || isVoicemailNumber) { - // If this is a number that cannot be dialed, there is no point in looking up a contact - // for it. - info = ContactInfo.EMPTY; - } else if (cachedInfo == null) { - mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY); - // Use the cached contact info from the call log. - info = cachedContactInfo; - // The db request should happen on a non-UI thread. - // Request the contact details immediately since they are currently missing. - enqueueRequest(number, countryIso, cachedContactInfo, true); - // We will format the phone number when we make the background request. - } else { - if (cachedInfo.isExpired()) { - // The contact info is no longer up to date, we should request it. However, we - // do not need to request them immediately. - enqueueRequest(number, countryIso, cachedContactInfo, false); - } else if (!callLogInfoMatches(cachedContactInfo, info)) { - // The call log information does not match the one we have, look it up again. - // We could simply update the call log directly, but that needs to be done in a - // background thread, so it is easier to simply request a new lookup, which will, as - // a side-effect, update the call log. - enqueueRequest(number, countryIso, cachedContactInfo, false); - } + // Note: Binding of the action buttons is done as required in configureActionViews when the + // user expands the actions ViewStub. - if (info == ContactInfo.EMPTY) { - // Use the cached contact info from the call log. - info = cachedContactInfo; - } + ContactInfo info = ContactInfo.EMPTY; + if (PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation) + && !isVoicemailNumber) { + // Lookup contacts with this number + info = mContactInfoCache.getValue(number, countryIso, cachedContactInfo); } final Uri lookupUri = info.lookupUri; @@ -762,7 +380,7 @@ public class CallLogAdapter extends GroupingListAdapter final long photoId = info.photoId; final Uri photoUri = info.photoUri; CharSequence formattedNumber = info.formattedNumber == null - ? null : PhoneNumberUtils.ttsSpanAsPhoneNumber(info.formattedNumber); + ? null : PhoneNumberUtils.getPhoneTtsSpannable(info.formattedNumber); final int[] callTypes = getCallTypes(c, count); final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION); final int sourceType = info.sourceType; @@ -784,7 +402,12 @@ public class CallLogAdapter extends GroupingListAdapter // Restore expansion state of the row on rebind. Inflate the actions ViewStub if required, // and set its visibility state accordingly. - expandOrCollapseActions(callLogItemView, isExpanded(rowId)); + views.expandOrCollapseActions( + isExpanded(rowId), + mOnReportButtonClickListener, + mActionListener, + mPhoneNumberUtilsWrapper, + mCallLogViewsHelper); if (TextUtils.isEmpty(name)) { details = new PhoneCallDetails(number, numberPresentation, formattedNumber, countryIso, @@ -798,17 +421,6 @@ public class CallLogAdapter extends GroupingListAdapter mCallLogViewsHelper.setPhoneCallDetails(mContext, views, details); - int contactType = ContactPhotoManager.TYPE_DEFAULT; - - if (isVoicemailNumber) { - contactType = ContactPhotoManager.TYPE_VOICEMAIL; - } else if (mContactInfoHelper.isBusiness(info.sourceType)) { - contactType = ContactPhotoManager.TYPE_BUSINESS; - } - - String lookupKey = lookupUri == null ? null - : ContactInfoHelper.getLookupKeyFromUri(lookupUri); - String nameForDefaultImage = null; if (TextUtils.isEmpty(name)) { nameForDefaultImage = mPhoneNumberHelper.getDisplayNumber(details.accountHandle, @@ -817,19 +429,15 @@ public class CallLogAdapter extends GroupingListAdapter nameForDefaultImage = name; } - if (photoId == 0 && photoUri != null) { - setPhoto(views, photoUri, lookupUri, nameForDefaultImage, lookupKey, contactType); - } else { - setPhoto(views, photoId, lookupUri, nameForDefaultImage, lookupKey, contactType); - } + views.setPhoto(photoId, photoUri, lookupUri, nameForDefaultImage, isVoicemailNumber, + mContactInfoHelper.isBusiness(info.sourceType)); + views.quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); // Listen for the first draw if (mViewTreeObserver == null) { mViewTreeObserver = callLogItemView.getViewTreeObserver(); mViewTreeObserver.addOnPreDrawListener(this); } - - bindBadge(callLogItemView, info, details, callType); } /** @@ -864,6 +472,7 @@ public class CallLogAdapter extends GroupingListAdapter } return CallLogGroupBuilder.DAY_GROUP_NONE; } + /** * Determines if a call log row with the given Id is expanded. * @param rowId The row Id of the call. @@ -884,342 +493,16 @@ public class CallLogAdapter extends GroupingListAdapter private boolean toggleExpansion(long rowId) { if (rowId == mCurrentlyExpanded) { // Collapsing currently expanded row. - mPreviouslyExpanded = NONE_EXPANDED; mCurrentlyExpanded = NONE_EXPANDED; - return false; } else { // Expanding a row (collapsing current expanded one). - - mPreviouslyExpanded = mCurrentlyExpanded; mCurrentlyExpanded = rowId; return true; } } /** - * Expands or collapses the view containing the CALLBACK/REDIAL, VOICEMAIL and DETAILS action - * buttons. - * - * @param callLogItem The call log entry parent view. - * @param isExpanded The new expansion state of the view. - */ - private void expandOrCollapseActions(View callLogItem, boolean isExpanded) { - final CallLogListItemViews views = (CallLogListItemViews)callLogItem.getTag(); - - expandVoicemailTranscriptionView(views, isExpanded); - if (isExpanded) { - // Inflate the view stub if necessary, and wire up the event handlers. - inflateActionViewStub(callLogItem); - - views.actionsView.setVisibility(View.VISIBLE); - views.actionsView.setAlpha(1.0f); - views.callLogEntryView.setBackgroundColor(mExpandedBackgroundColor); - views.callLogEntryView.setTranslationZ(mExpandedTranslationZ); - callLogItem.setTranslationZ(mExpandedTranslationZ); // WAR - } else { - // When recycling a view, it is possible the actionsView ViewStub was previously - // inflated so we should hide it in this case. - if (views.actionsView != null) { - views.actionsView.setVisibility(View.GONE); - } - - views.callLogEntryView.setBackgroundColor(mCallLogBackgroundColor); - views.callLogEntryView.setTranslationZ(0); - callLogItem.setTranslationZ(0); // WAR - } - } - - public static void expandVoicemailTranscriptionView(CallLogListItemViews views, - boolean isExpanded) { - if (views.callType != Calls.VOICEMAIL_TYPE) { - return; - } - - final TextView view = views.phoneCallDetailsViews.voicemailTranscriptionView; - if (TextUtils.isEmpty(view.getText())) { - return; - } - view.setMaxLines(isExpanded ? VOICEMAIL_TRANSCRIPTION_MAX_LINES : 1); - view.setSingleLine(!isExpanded); - } - - /** - * Configures the action buttons in the expandable actions ViewStub. The ViewStub is not - * inflated during initial binding, so click handlers, tags and accessibility text must be set - * here, if necessary. - * - * @param callLogItem The call log list item view. - */ - private void inflateActionViewStub(final View callLogItem) { - final CallLogListItemViews views = (CallLogListItemViews)callLogItem.getTag(); - - ViewStub stub = (ViewStub)callLogItem.findViewById(R.id.call_log_entry_actions_stub); - if (stub != null) { - views.actionsView = (ViewGroup) stub.inflate(); - } - - if (views.callBackButtonView == null) { - views.callBackButtonView = (TextView)views.actionsView.findViewById( - R.id.call_back_action); - } - - if (views.videoCallButtonView == null) { - views.videoCallButtonView = (TextView)views.actionsView.findViewById( - R.id.video_call_action); - } - - if (views.voicemailButtonView == null) { - views.voicemailButtonView = (TextView)views.actionsView.findViewById( - R.id.voicemail_action); - } - - if (views.detailsButtonView == null) { - views.detailsButtonView = (TextView)views.actionsView.findViewById(R.id.details_action); - } - - if (views.reportButtonView == null) { - views.reportButtonView = (TextView)views.actionsView.findViewById(R.id.report_action); - views.reportButtonView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (mOnReportButtonClickListener != null) { - mOnReportButtonClickListener.onReportButtonClick(views.number); - } - } - }); - } - - bindActionButtons(views); - } - - /*** - * Binds text titles, click handlers and intents to the voicemail, details and callback action - * buttons. - * - * @param views The call log item views. - */ - private void bindActionButtons(CallLogListItemViews views) { - boolean canPlaceCallToNumber = - PhoneNumberUtilsWrapper.canPlaceCallsTo(views.number, views.numberPresentation); - // Set return call intent, otherwise null. - if (canPlaceCallToNumber) { - boolean isVoicemailNumber = - mPhoneNumberUtilsWrapper.isVoicemailNumber(views.accountHandle, views.number); - if (isVoicemailNumber) { - // Make a general call to voicemail to ensure that if there are multiple accounts - // it does not call the voicemail number of a specific phone account. - views.callBackButtonView.setTag( - IntentProvider.getReturnVoicemailCallIntentProvider()); - } else { - // Sets the primary action to call the number. - views.callBackButtonView.setTag( - IntentProvider.getReturnCallIntentProvider(views.number)); - } - views.callBackButtonView.setVisibility(View.VISIBLE); - views.callBackButtonView.setOnClickListener(mActionListener); - - final int titleId; - if (views.callType == Calls.VOICEMAIL_TYPE || views.callType == Calls.OUTGOING_TYPE) { - titleId = R.string.call_log_action_redial; - } else { - titleId = R.string.call_log_action_call_back; - } - views.callBackButtonView.setText(mContext.getString(titleId)); - } else { - // Number is not callable, so hide button. - views.callBackButtonView.setTag(null); - views.callBackButtonView.setVisibility(View.GONE); - } - - // If one of the calls had video capabilities, show the video call button. - if (CallUtil.isVideoEnabled(mContext) && canPlaceCallToNumber && - views.phoneCallDetailsViews.callTypeIcons.isVideoShown()) { - views.videoCallButtonView.setTag( - IntentProvider.getReturnVideoCallIntentProvider(views.number)); - views.videoCallButtonView.setVisibility(View.VISIBLE); - views.videoCallButtonView.setOnClickListener(mActionListener); - } else { - views.videoCallButtonView.setTag(null); - views.videoCallButtonView.setVisibility(View.GONE); - } - - // For voicemail calls, show the "VOICEMAIL" action button; hide otherwise. - if (views.callType == Calls.VOICEMAIL_TYPE) { - views.voicemailButtonView.setOnClickListener(mActionListener); - views.voicemailButtonView.setTag( - IntentProvider.getPlayVoicemailIntentProvider( - views.rowId, views.voicemailUri)); - views.voicemailButtonView.setVisibility(View.VISIBLE); - - views.detailsButtonView.setVisibility(View.GONE); - } else { - views.voicemailButtonView.setTag(null); - views.voicemailButtonView.setVisibility(View.GONE); - - views.detailsButtonView.setOnClickListener(mActionListener); - views.detailsButtonView.setTag( - IntentProvider.getCallDetailIntentProvider( - views.rowId, views.callIds, null) - ); - - if (views.canBeReportedAsInvalid && !views.reported) { - views.reportButtonView.setVisibility(View.VISIBLE); - } else { - views.reportButtonView.setVisibility(View.GONE); - } - } - - mCallLogViewsHelper.setActionContentDescriptions(views); - } - - protected void bindBadge( - View view, final ContactInfo info, final PhoneCallDetails details, int callType) { - // Do not show badge in call log. - if (!mIsCallLog) { - final ViewStub stub = (ViewStub) view.findViewById(R.id.link_stub); - if (UriUtils.isEncodedContactUri(info.lookupUri)) { - if (stub != null) { - mBadgeContainer = stub.inflate(); - } else { - mBadgeContainer = view.findViewById(R.id.badge_container); - } - - mBadgeContainer.setVisibility(View.VISIBLE); - mBadgeImageView = (ImageView) mBadgeContainer.findViewById(R.id.badge_image); - mBadgeText = (TextView) mBadgeContainer.findViewById(R.id.badge_text); - - final View clickableArea = mBadgeContainer.findViewById(R.id.badge_link_container); - if (clickableArea != null) { - clickableArea.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // If no lookup uri is provided, we need to rely on what information - // we have available; namely the phone number and name. - if (info.lookupUri == null) { - final Intent intent = - DialtactsActivity.getAddToContactIntent(details.name, - details.number, - details.numberType); - DialerUtils.startActivityWithErrorToast(mContext, intent, - R.string.add_contact_not_available); - } else { - addContactFromLookupUri(info.lookupUri); - } - } - }); - } - mBadgeImageView.setImageResource(R.drawable.ic_person_add_24dp); - mBadgeText.setText(R.string.recentCalls_addToContact); - } else { - // Hide badge if it was previously shown. - mBadgeContainer = view.findViewById(R.id.badge_container); - if (mBadgeContainer != null) { - mBadgeContainer.setVisibility(View.GONE); - } - } - } - } - - /** Checks whether the contact info from the call log matches the one from the contacts db. */ - private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) { - // The call log only contains a subset of the fields in the contacts db. - // Only check those. - return TextUtils.equals(callLogInfo.name, info.name) - && callLogInfo.type == info.type - && TextUtils.equals(callLogInfo.label, info.label); - } - - /** Stores the updated contact info in the call log if it is different from the current one. */ - private void updateCallLogContactInfoCache(String number, String countryIso, - ContactInfo updatedInfo, ContactInfo callLogInfo) { - final ContentValues values = new ContentValues(); - boolean needsUpdate = false; - - if (callLogInfo != null) { - if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) { - values.put(Calls.CACHED_NAME, updatedInfo.name); - needsUpdate = true; - } - - if (updatedInfo.type != callLogInfo.type) { - values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type); - needsUpdate = true; - } - - if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) { - values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label); - needsUpdate = true; - } - if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) { - values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); - needsUpdate = true; - } - // Only replace the normalized number if the new updated normalized number isn't empty. - if (!TextUtils.isEmpty(updatedInfo.normalizedNumber) && - !TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) { - values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); - needsUpdate = true; - } - if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) { - values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number); - needsUpdate = true; - } - if (updatedInfo.photoId != callLogInfo.photoId) { - values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId); - needsUpdate = true; - } - if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) { - values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); - needsUpdate = true; - } - } else { - // No previous values, store all of them. - values.put(Calls.CACHED_NAME, updatedInfo.name); - values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type); - values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label); - values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); - values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number); - values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); - values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId); - values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); - needsUpdate = true; - } - - if (!needsUpdate) return; - - try { - if (countryIso == null) { - mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values, - Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL", - new String[]{ number }); - } else { - mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values, - Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?", - new String[]{ number, countryIso }); - } - } catch (SQLiteFullException e) { - Log.e(TAG, "Unable to update contact info in call log db", e); - } - } - - /** Returns the contact information as stored in the call log. */ - private ContactInfo getContactInfoFromCallLog(Cursor c) { - ContactInfo info = new ContactInfo(); - info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI)); - info.name = c.getString(CallLogQuery.CACHED_NAME); - info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE); - info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL); - String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER); - info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber; - info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER); - info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID); - info.photoUri = null; // We do not cache the photo URI. - info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER); - return info; - } - - /** * Returns the call types for the given number of items in the cursor. * <p> * It uses the next {@code count} rows in the cursor to extract the types. @@ -1256,55 +539,22 @@ public class CallLogAdapter extends GroupingListAdapter return features; } - private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri, - String displayName, String identifier, int contactType) { - views.quickContactView.assignContactUri(contactUri); - views.quickContactView.setOverlay(null); - DefaultImageRequest request = new DefaultImageRequest(displayName, identifier, - contactType, true /* isCircular */); - mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, false /* darkTheme */, - true /* isCircular */, request); - } - - private void setPhoto(CallLogListItemViews views, Uri photoUri, Uri contactUri, - String displayName, String identifier, int contactType) { - views.quickContactView.assignContactUri(contactUri); - views.quickContactView.setOverlay(null); - DefaultImageRequest request = new DefaultImageRequest(displayName, identifier, - contactType, true /* isCircular */); - mContactPhotoManager.loadPhoto(views.quickContactView, photoUri, mPhotoSize, - false /* darkTheme */, true /* isCircular */, request); - } - - /** - * Bind a call log entry view for testing purposes. Also inflates the action view stub so - * unit tests can access the buttons contained within. - * - * @param view The current call log row. - * @param context The current context. - * @param cursor The cursor to bind from. - */ - @VisibleForTesting - void bindViewForTest(View view, Context context, Cursor cursor) { - bindStandAloneView(view, context, cursor); - inflateActionViewStub(view); - } - /** * Sets whether processing of requests for contact details should be enabled. - * <p> + * * This method should be called in tests to disable such processing of requests when not * needed. */ @VisibleForTesting void disableRequestProcessingForTest() { - mRequestProcessingDisabled = true; + // TODO: Remove this and test the cache directly. + mContactInfoCache.disableRequestProcessingForTest(); } @VisibleForTesting void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { - NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); - mContactInfoCache.put(numberCountryIso, contactInfo); + // TODO: Remove this and test the cache directly. + mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo); } @Override @@ -1333,48 +583,6 @@ public class CallLogAdapter extends GroupingListAdapter mDayGroups.clear(); } - /* - * Get the number from the Contacts, if available, since sometimes - * the number provided by caller id may not be formatted properly - * depending on the carrier (roaming) in use at the time of the - * incoming call. - * Logic : If the caller-id number starts with a "+", use it - * Else if the number in the contacts starts with a "+", use that one - * Else if the number in the contacts is longer, use that one - */ - public String getBetterNumberFromContacts(String number, String countryIso) { - String matchingNumber = null; - // Look in the cache first. If it's not found then query the Phones db - NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); - ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso); - if (ci != null && ci != ContactInfo.EMPTY) { - matchingNumber = ci.number; - } else { - try { - Cursor phonesCursor = mContext.getContentResolver().query( - Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number), - PhoneQuery._PROJECTION, null, null, null); - if (phonesCursor != null) { - try { - if (phonesCursor.moveToFirst()) { - matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER); - } - } finally { - phonesCursor.close(); - } - } - } catch (Exception e) { - // Use the number from the call log - } - } - if (!TextUtils.isEmpty(matchingNumber) && - (matchingNumber.startsWith("+") - || matchingNumber.length() > number.length())) { - number = matchingNumber; - } - return number; - } - /** * Retrieves the call Ids represented by the current call log row. * @@ -1411,20 +619,14 @@ public class CallLogAdapter extends GroupingListAdapter } } - public void onBadDataReported(String number) { - mContactInfoCache.expireAll(); - mReportedToast.show(); - } - /** * Manages the state changes for the UI interaction where a call log row is expanded. * * @param view The view that was tapped - * @param animate Whether or not to animate the expansion/collapse * @param forceExpand Whether or not to force the call log row into an expanded state regardless * of its previous state */ - private void handleRowExpanded(View view, boolean animate, boolean forceExpand) { + private void handleRowExpanded(View view, boolean forceExpand) { final CallLogListItemViews views = (CallLogListItemViews) view.getTag(); if (forceExpand && isExpanded(views.rowId)) { @@ -1433,78 +635,20 @@ public class CallLogAdapter extends GroupingListAdapter // Hide or show the actions view. boolean expanded = toggleExpansion(views.rowId); - - // Trigger loading of the viewstub and visual expand or collapse. - expandOrCollapseActions(view, expanded); - - // Animate the expansion or collapse. - if (mCallItemExpandedListener != null) { - if (animate) { - mCallItemExpandedListener.onItemExpanded(view); - } - - // Animate the collapse of the previous item if it is still visible on screen. - if (mPreviouslyExpanded != NONE_EXPANDED) { - View previousItem = mCallItemExpandedListener.getViewForCallId( - mPreviouslyExpanded); - - if (previousItem != null) { - expandOrCollapseActions(previousItem, false); - if (animate) { - mCallItemExpandedListener.onItemExpanded(previousItem); - } - } - mPreviouslyExpanded = NONE_EXPANDED; - } - } + expandItem(views, expanded); } /** - * Invokes the "add contact" activity given the expanded contact information stored in a - * lookup URI. This can include, for example, address and website information. - * - * @param lookupUri The lookup URI. + * @param views The view holder for the item to expand or collapse. + * @param expand {@code true} to expand the item, {@code false} otherwise. */ - private void addContactFromLookupUri(Uri lookupUri) { - Contact contactToSave = ContactLoader.parseEncodedContactEntity(lookupUri); - if (contactToSave == null) { - return; - } - - // Note: This code mirrors code in Contacts/QuickContactsActivity. - final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); - intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); - - ArrayList<ContentValues> values = contactToSave.getContentValues(); - // Only pre-fill the name field if the provided display name is an nickname - // or better (e.g. structured name, nickname) - if (contactToSave.getDisplayNameSource() - >= ContactsContract.DisplayNameSources.NICKNAME) { - intent.putExtra(ContactsContract.Intents.Insert.NAME, - contactToSave.getDisplayName()); - } else if (contactToSave.getDisplayNameSource() - == ContactsContract.DisplayNameSources.ORGANIZATION) { - // This is probably an organization. Instead of copying the organization - // name into a name entry, copy it into the organization entry. This - // way we will still consider the contact an organization. - final ContentValues organization = new ContentValues(); - organization.put(ContactsContract.CommonDataKinds.Organization.COMPANY, - contactToSave.getDisplayName()); - organization.put(ContactsContract.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE); - values.add(organization); - } - - // Last time used and times used are aggregated values from the usage stat - // table. They need to be removed from data values so the SQL table can insert - // properly - for (ContentValues value : values) { - value.remove(ContactsContract.Data.LAST_TIME_USED); - value.remove(ContactsContract.Data.TIMES_USED); - } - intent.putExtra(ContactsContract.Intents.Insert.DATA, values); - - DialerUtils.startActivityWithErrorToast(mContext, intent, - R.string.add_contact_not_available); + public void expandItem(CallLogListItemViews views, boolean expand) { + // Trigger loading of the viewstub and visual expand or collapse. + views.expandOrCollapseActions( + expand, + mOnReportButtonClickListener, + mActionListener, + mPhoneNumberUtilsWrapper, + mCallLogViewsHelper); } } diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java index 9c95d3ec8..d69c2ed7e 100644 --- a/src/com/android/dialer/calllog/CallLogFragment.java +++ b/src/com/android/dialer/calllog/CallLogFragment.java @@ -62,13 +62,10 @@ import java.util.List; */ public class CallLogFragment extends ListFragment implements CallLogQueryHandler.Listener, CallLogAdapter.OnReportButtonClickListener, - CallLogAdapter.CallFetcher, - CallLogAdapter.CallItemExpandedListener { + CallLogAdapter.CallFetcher { private static final String TAG = "CallLogFragment"; private static final String REPORT_DIALOG_TAG = "report_dialog"; - private String mReportDialogNumber; - private boolean mIsReportDialogShowing; /** * ID of the empty loader to defer other fragments. @@ -78,9 +75,6 @@ public class CallLogFragment extends ListFragment private static final String KEY_FILTER_TYPE = "filter_type"; private static final String KEY_LOG_LIMIT = "log_limit"; private static final String KEY_DATE_LIMIT = "date_limit"; - private static final String KEY_SHOW_FOOTER = "show_footer"; - private static final String KEY_IS_REPORT_DIALOG_SHOWING = "is_report_dialog_showing"; - private static final String KEY_REPORT_DIALOG_NUMBER = "report_dialog_number"; private CallLogAdapter mAdapter; private CallLogQueryHandler mCallLogQueryHandler; @@ -91,21 +85,15 @@ public class CallLogFragment extends ListFragment private VoicemailStatusHelper mVoicemailStatusHelper; private View mStatusMessageView; + private View mEmptyListView; private TextView mStatusMessageText; private TextView mStatusMessageAction; private KeyguardManager mKeyguardManager; - private View mFooterView; private boolean mEmptyLoaderRunning; private boolean mCallLogFetched; private boolean mVoicemailStatusFetched; - private float mExpandedItemTranslationZ; - private int mFadeInDuration; - private int mFadeInStartDelay; - private int mFadeOutDuration; - private int mExpandCollapseDuration; - private final Handler mHandler = new Handler(); private class CustomContentObserver extends ContentObserver { @@ -138,9 +126,6 @@ public class CallLogFragment extends ListFragment // the date filter are included. If zero, no date-based filtering occurs. private long mDateLimit = 0; - // Whether or not to show the Show call history footer view - private boolean mHasFooterView = false; - public CallLogFragment() { this(CallLogQueryHandler.CALL_TYPE_ALL, -1); } @@ -184,44 +169,24 @@ public class CallLogFragment extends ListFragment mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter); mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit); mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit); - mHasFooterView = state.getBoolean(KEY_SHOW_FOOTER, mHasFooterView); - mIsReportDialogShowing = state.getBoolean(KEY_IS_REPORT_DIALOG_SHOWING, - mIsReportDialogShowing); - mReportDialogNumber = state.getString(KEY_REPORT_DIALOG_NUMBER, mReportDialogNumber); } String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this, - new ContactInfoHelper(getActivity(), currentCountryIso), this, this, true); + new ContactInfoHelper(getActivity(), currentCountryIso), this); setListAdapter(mAdapter); mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this, mLogLimit); mKeyguardManager = (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE); - getActivity().getContentResolver().registerContentObserver(CallLog.CONTENT_URI, true, - mCallLogObserver); + getActivity().getContentResolver().registerContentObserver( + CallLog.CONTENT_URI, true, mCallLogObserver); getActivity().getContentResolver().registerContentObserver( ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver); getActivity().getContentResolver().registerContentObserver( Status.CONTENT_URI, true, mVoicemailStatusObserver); setHasOptionsMenu(true); - updateCallList(mCallTypeFilter, mDateLimit); - - mExpandedItemTranslationZ = - getResources().getDimension(R.dimen.call_log_expanded_translation_z); - mFadeInDuration = getResources().getInteger(R.integer.call_log_actions_fade_in_duration); - mFadeInStartDelay = getResources().getInteger(R.integer.call_log_actions_fade_start); - mFadeOutDuration = getResources().getInteger(R.integer.call_log_actions_fade_out_duration); - mExpandCollapseDuration = getResources().getInteger( - R.integer.call_log_expand_collapse_duration); - - if (mIsReportDialogShowing) { - DialogFragment df = ObjectFactory.getReportDialogFragment(mReportDialogNumber); - if (df != null) { - df.setTargetFragment(this, 0); - df.show(getActivity().getFragmentManager(), REPORT_DIALOG_TAG); - } - } + fetchCalls(); } /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ @@ -235,8 +200,13 @@ public class CallLogFragment extends ListFragment mAdapter.changeCursor(cursor); // This will update the state of the "Clear call log" menu item. getActivity().invalidateOptionsMenu(); + + final ListView listView = getListView(); + boolean showListView = cursor.getCount() > 0; + listView.setVisibility(showListView ? View.VISIBLE : View.GONE); + mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE); + if (mScrollToTop) { - final ListView listView = getListView(); // The smooth-scroll animation happens over a fixed time period. // As a result, if it scrolls through a large portion of the list, // each frame will jump so far from the previous one that the user @@ -270,13 +240,21 @@ public class CallLogFragment extends ListFragment */ @Override public void onVoicemailStatusFetched(Cursor statusCursor) { - if (getActivity() == null || getActivity().isFinishing()) { + Activity activity = getActivity(); + if (activity == null || activity.isFinishing()) { return; } updateVoicemailStatusMessage(statusCursor); - int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor); - setVoicemailSourcesAvailable(activeSources != 0); + // If there are any changes to the presence of active voicemail services, invalidate the + // options menu so that it will be updated. + boolean hasActiveVoicemailSources = + mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor) != 0; + if (mVoicemailSourcesAvailable != hasActiveVoicemailSources) { + mVoicemailSourcesAvailable = hasActiveVoicemailSources; + activity.invalidateOptionsMenu(); + } + mVoicemailStatusFetched = true; destroyEmptyLoaderIfAllDataFetched(); } @@ -288,18 +266,6 @@ public class CallLogFragment extends ListFragment } } - /** Sets whether there are any voicemail sources available in the platform. */ - private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) { - if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return; - mVoicemailSourcesAvailable = voicemailSourcesAvailable; - - Activity activity = getActivity(); - if (activity != null) { - // This is so that the options menu content is updated. - activity.invalidateOptionsMenu(); - } - } - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { View view = inflater.inflate(R.layout.call_log_fragment, container, false); @@ -313,9 +279,8 @@ public class CallLogFragment extends ListFragment @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - getListView().setEmptyView(view.findViewById(R.id.empty_list_view)); + mEmptyListView = view.findViewById(R.id.empty_list_view); getListView().setItemsCanFocus(true); - maybeAddFooterView(); updateEmptyMessage(mCallTypeFilter); } @@ -382,20 +347,20 @@ public class CallLogFragment extends ListFragment @Override public void onPause() { super.onPause(); - // Kill the requests thread - mAdapter.stopRequestProcessing(); + mAdapter.pauseCache(); } @Override public void onStop() { super.onStop(); - updateOnExit(); + + updateOnTransition(false /* onEntry */); } @Override public void onDestroy() { super.onDestroy(); - mAdapter.stopRequestProcessing(); + mAdapter.pauseCache(); mAdapter.changeCursor(null); getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver); getActivity().getContentResolver().unregisterContentObserver(mContactsObserver); @@ -408,9 +373,6 @@ public class CallLogFragment extends ListFragment outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter); outState.putInt(KEY_LOG_LIMIT, mLogLimit); outState.putLong(KEY_DATE_LIMIT, mDateLimit); - outState.putBoolean(KEY_SHOW_FOOTER, mHasFooterView); - outState.putBoolean(KEY_IS_REPORT_DIALOG_SHOWING, mIsReportDialogShowing); - outState.putString(KEY_REPORT_DIALOG_NUMBER, mReportDialogNumber); } @Override @@ -418,19 +380,6 @@ public class CallLogFragment extends ListFragment mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit); } - public void startCallsQuery() { - mAdapter.setLoading(true); - mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit); - } - - private void startVoicemailStatusQuery() { - mCallLogQueryHandler.fetchVoicemailStatus(); - } - - private void updateCallList(int filterType, long dateLimit) { - mCallLogQueryHandler.fetchCalls(filterType, dateLimit); - } - private void updateEmptyMessage(int filterType) { final int messageId; switch (filterType) { @@ -448,7 +397,7 @@ public class CallLogFragment extends ListFragment + filterType); } DialerUtils.configureEmptyListView( - getListView().getEmptyView(), R.drawable.empty_call_log, messageId, getResources()); + mEmptyListView, R.drawable.empty_call_log, messageId, getResources()); } CallLogAdapter getAdapter() { @@ -461,7 +410,7 @@ public class CallLogFragment extends ListFragment if (mMenuVisible != menuVisible) { mMenuVisible = menuVisible; if (!menuVisible) { - updateOnExit(); + updateOnTransition(false /* onEntry */); } else if (isResumed()) { refreshData(); } @@ -475,24 +424,26 @@ public class CallLogFragment extends ListFragment // Mark all entries in the contact info cache as out of date, so they will be looked up // again once being shown. mAdapter.invalidateCache(); - startCallsQuery(); - startVoicemailStatusQuery(); - updateOnEntry(); - mRefreshDataRequired = false; - } - } + mAdapter.setLoading(true); - /** Updates call data and notification state while leaving the call log tab. */ - private void updateOnExit() { - updateOnTransition(false); - } + fetchCalls(); + mCallLogQueryHandler.fetchVoicemailStatus(); - /** Updates call data and notification state while entering the call log tab. */ - private void updateOnEntry() { - updateOnTransition(true); + updateOnTransition(true /* onEntry */); + mRefreshDataRequired = false; + } else { + // Refresh the display of the existing data to update the timestamp text descriptions. + mAdapter.notifyDataSetChanged(); + } } - // TODO: Move to CallLogActivity + /** + * Updates the call data and notification state on entering or leaving the call log tab. + * + * If we are leaving the call log tab, mark all the missed calls as read. + * + * TODO: Move to CallLogActivity + */ private void updateOnTransition(boolean onEntry) { // We don't want to update any call data when keyguard is on because the user has likely not // seen the new calls yet. @@ -509,184 +460,11 @@ public class CallLogFragment extends ListFragment } } - /** - * Enables/disables the showing of the view full call history footer - * - * @param hasFooterView Whether or not to show the footer - */ - public void setHasFooterView(boolean hasFooterView) { - mHasFooterView = hasFooterView; - maybeAddFooterView(); - } - - /** - * Determine whether or not the footer view should be added to the listview. If getView() - * is null, which means onCreateView hasn't been called yet, defer the addition of the footer - * until onViewCreated has been called. - */ - private void maybeAddFooterView() { - if (!mHasFooterView || getView() == null) { - return; - } - - if (mFooterView == null) { - mFooterView = getActivity().getLayoutInflater().inflate( - R.layout.recents_list_footer, getListView(), false); - mFooterView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - ((HostInterface) getActivity()).showCallHistory(); - } - }); - } - - final ListView listView = getListView(); - listView.removeFooterView(mFooterView); - listView.addFooterView(mFooterView); - - ViewUtil.addBottomPaddingToListViewForFab(listView, getResources()); - } - - @Override - public void onItemExpanded(final View view) { - final int startingHeight = view.getHeight(); - final CallLogListItemViews viewHolder = (CallLogListItemViews) view.getTag(); - final ViewTreeObserver observer = getListView().getViewTreeObserver(); - observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - // We don't want to continue getting called for every draw. - if (observer.isAlive()) { - observer.removeOnPreDrawListener(this); - } - // Calculate some values to help with the animation. - final int endingHeight = view.getHeight(); - final int distance = Math.abs(endingHeight - startingHeight); - final int baseHeight = Math.min(endingHeight, startingHeight); - final boolean isExpand = endingHeight > startingHeight; - - // Set the views back to the start state of the animation - view.getLayoutParams().height = startingHeight; - if (!isExpand) { - viewHolder.actionsView.setVisibility(View.VISIBLE); - } - CallLogAdapter.expandVoicemailTranscriptionView(viewHolder, !isExpand); - - // Set up the fade effect for the action buttons. - if (isExpand) { - // Start the fade in after the expansion has partly completed, otherwise it - // will be mostly over before the expansion completes. - viewHolder.actionsView.setAlpha(0f); - viewHolder.actionsView.animate() - .alpha(1f) - .setStartDelay(mFadeInStartDelay) - .setDuration(mFadeInDuration) - .start(); - } else { - viewHolder.actionsView.setAlpha(1f); - viewHolder.actionsView.animate() - .alpha(0f) - .setDuration(mFadeOutDuration) - .start(); - } - view.requestLayout(); - - // Set up the animator to animate the expansion and shadow depth. - ValueAnimator animator = isExpand ? ValueAnimator.ofFloat(0f, 1f) - : ValueAnimator.ofFloat(1f, 0f); - - // Figure out how much scrolling is needed to make the view fully visible. - final Rect localVisibleRect = new Rect(); - view.getLocalVisibleRect(localVisibleRect); - final int scrollingNeeded = localVisibleRect.top > 0 ? -localVisibleRect.top - : view.getMeasuredHeight() - localVisibleRect.height(); - final ListView listView = getListView(); - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - - private int mCurrentScroll = 0; - - @Override - public void onAnimationUpdate(ValueAnimator animator) { - Float value = (Float) animator.getAnimatedValue(); - - // For each value from 0 to 1, animate the various parts of the layout. - view.getLayoutParams().height = (int) (value * distance + baseHeight); - float z = mExpandedItemTranslationZ * value; - viewHolder.callLogEntryView.setTranslationZ(z); - view.setTranslationZ(z); // WAR - view.requestLayout(); - - if (isExpand) { - if (listView != null) { - int scrollBy = (int) (value * scrollingNeeded) - mCurrentScroll; - listView.smoothScrollBy(scrollBy, /* duration = */ 0); - mCurrentScroll += scrollBy; - } - } - } - }); - // Set everything to their final values when the animation's done. - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - view.getLayoutParams().height = LayoutParams.WRAP_CONTENT; - - if (!isExpand) { - viewHolder.actionsView.setVisibility(View.GONE); - } else { - // This seems like it should be unnecessary, but without this, after - // navigating out of the activity and then back, the action view alpha - // is defaulting to the value (0) at the start of the expand animation. - viewHolder.actionsView.setAlpha(1); - } - CallLogAdapter.expandVoicemailTranscriptionView(viewHolder, isExpand); - } - }); - - animator.setDuration(mExpandCollapseDuration); - animator.start(); - - // Return false so this draw does not occur to prevent the final frame from - // being drawn for the single frame before the animations start. - return false; - } - }); - } - - /** - * Retrieves the call log view for the specified call Id. If the view is not currently - * visible, returns null. - * - * @param callId The call Id. - * @return The call log view. - */ - @Override - public View getViewForCallId(long callId) { - ListView listView = getListView(); - - int firstPosition = listView.getFirstVisiblePosition(); - int lastPosition = listView.getLastVisiblePosition(); - - for (int position = 0; position <= lastPosition - firstPosition; position++) { - View view = listView.getChildAt(position); - - if (view != null) { - final CallLogListItemViews viewHolder = (CallLogListItemViews) view.getTag(); - if (viewHolder != null && viewHolder.rowId == callId) { - return view; - } - } - } - - return null; - } - public void onBadDataReported(String number) { - mIsReportDialogShowing = false; if (number == null) { return; } - mAdapter.onBadDataReported(number); + mAdapter.invalidateCache(); mAdapter.notifyDataSetChanged(); } @@ -695,8 +473,6 @@ public class CallLogFragment extends ListFragment if (df != null) { df.setTargetFragment(this, 0); df.show(getActivity().getFragmentManager(), REPORT_DIALOG_TAG); - mReportDialogNumber = number; - mIsReportDialogShowing = true; } } } diff --git a/src/com/android/dialer/calllog/CallLogGroupBuilder.java b/src/com/android/dialer/calllog/CallLogGroupBuilder.java index 1f11e1e60..0826aeb4a 100644 --- a/src/com/android/dialer/calllog/CallLogGroupBuilder.java +++ b/src/com/android/dialer/calllog/CallLogGroupBuilder.java @@ -21,7 +21,6 @@ import android.provider.CallLog.Calls; import android.telephony.PhoneNumberUtils; import android.text.format.Time; -import com.android.common.widget.GroupingListAdapter; import com.android.contacts.common.util.DateUtils; import com.android.contacts.common.util.PhoneNumberHelper; diff --git a/src/com/android/dialer/calllog/CallLogListItemViews.java b/src/com/android/dialer/calllog/CallLogListItemViews.java index 0ccdf00a1..9d11a3ab6 100644 --- a/src/com/android/dialer/calllog/CallLogListItemViews.java +++ b/src/com/android/dialer/calllog/CallLogListItemViews.java @@ -17,19 +17,36 @@ package com.android.dialer.calllog; import android.content.Context; +import android.content.res.Resources; +import android.net.Uri; +import android.provider.CallLog.Calls; import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStub; import android.widget.QuickContactBadge; import android.widget.TextView; +import com.android.contacts.common.CallUtil; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; import com.android.contacts.common.testing.NeededForTesting; import com.android.dialer.PhoneCallDetailsViews; import com.android.dialer.R; /** - * Simple value object containing the various views within a call log entry. + * This is an object containing the various views within a call log entry. It contains values + * pointing to views contained by a call log list item view, so that we can improve performance + * by reducing the frequency with which we need to find views by IDs. + * + * This object also contains methods for inflating action views and binding action behaviors. This + * is a way of isolating view logic from the CallLogAdapter. We should consider moving that logic + * if the call log list item is eventually represented as a UI component. */ public final class CallLogListItemViews { + /** The root view of the call log list item */ + public final View rootView; /** The quick contact badge for the contact. */ public final QuickContactBadge quickContactView; /** The primary action view of the entry. */ @@ -113,18 +130,43 @@ public final class CallLogListItemViews { */ public boolean canBeReportedAsInvalid; - private CallLogListItemViews(QuickContactBadge quickContactView, View primaryActionView, - PhoneCallDetailsViews phoneCallDetailsViews, View callLogEntryView, + private static final int VOICEMAIL_TRANSCRIPTION_MAX_LINES = 10; + + private Context mContext; + private int mPhotoSize; + + private int mCallLogBackgroundColor; + private int mExpandedBackgroundColor; + private float mExpandedTranslationZ; + + private CallLogListItemViews( + Context context, + View rootView, + QuickContactBadge quickContactView, + View primaryActionView, + PhoneCallDetailsViews phoneCallDetailsViews, + View callLogEntryView, TextView dayGroupHeader) { + mContext = context; + + this.rootView = rootView; this.quickContactView = quickContactView; this.primaryActionView = primaryActionView; this.phoneCallDetailsViews = phoneCallDetailsViews; this.callLogEntryView = callLogEntryView; this.dayGroupHeader = dayGroupHeader; + + Resources resources = mContext.getResources(); + mCallLogBackgroundColor = resources.getColor(R.color.background_dialer_list_items); + mExpandedBackgroundColor = resources.getColor(R.color.call_log_expanded_background_color); + mExpandedTranslationZ = resources.getDimension(R.dimen.call_log_expanded_translation_z); + mPhotoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size); } - public static CallLogListItemViews fromView(View view) { + public static CallLogListItemViews fromView(Context context, View view) { return new CallLogListItemViews( + context, + view, (QuickContactBadge) view.findViewById(R.id.quick_contact_photo), view.findViewById(R.id.primary_action_view), PhoneCallDetailsViews.fromView(view), @@ -132,9 +174,217 @@ public final class CallLogListItemViews { (TextView) view.findViewById(R.id.call_log_day_group_label)); } + /** + * Configures the action buttons in the expandable actions ViewStub. The ViewStub is not + * inflated during initial binding, so click handlers, tags and accessibility text must be set + * here, if necessary. + * + * @param callLogItem The call log list item view. + */ + public void inflateActionViewStub( + final CallLogAdapter.OnReportButtonClickListener onReportButtonClickListener, + View.OnClickListener actionListener, + PhoneNumberUtilsWrapper phoneNumberUtilsWrapper, + CallLogListItemHelper callLogViewsHelper) { + ViewStub stub = (ViewStub) rootView.findViewById(R.id.call_log_entry_actions_stub); + if (stub != null) { + actionsView = (ViewGroup) stub.inflate(); + } + + if (callBackButtonView == null) { + callBackButtonView = (TextView) actionsView.findViewById(R.id.call_back_action); + } + + if (videoCallButtonView == null) { + videoCallButtonView = (TextView) actionsView.findViewById(R.id.video_call_action); + } + + if (voicemailButtonView == null) { + voicemailButtonView = (TextView) actionsView.findViewById(R.id.voicemail_action); + } + + if (detailsButtonView == null) { + detailsButtonView = (TextView) actionsView.findViewById(R.id.details_action); + } + + if (reportButtonView == null) { + reportButtonView = (TextView) actionsView.findViewById(R.id.report_action); + reportButtonView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (onReportButtonClickListener != null) { + onReportButtonClickListener.onReportButtonClick(number); + } + } + }); + } + + bindActionButtons(actionListener, phoneNumberUtilsWrapper, callLogViewsHelper); + } + + /** + * Binds text titles, click handlers and intents to the voicemail, details and callback action + * buttons. + */ + private void bindActionButtons( + View.OnClickListener actionListener, + PhoneNumberUtilsWrapper phoneNumberUtilsWrapper, + CallLogListItemHelper callLogViewsHelper) { + boolean canPlaceCallToNumber = + PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation); + + // Set return call intent, otherwise null. + if (canPlaceCallToNumber) { + boolean isVoicemailNumber = + phoneNumberUtilsWrapper.isVoicemailNumber(accountHandle, number); + if (isVoicemailNumber) { + // Make a general call to voicemail to ensure that if there are multiple accounts + // it does not call the voicemail number of a specific phone account. + callBackButtonView.setTag(IntentProvider.getReturnVoicemailCallIntentProvider()); + } else { + // Sets the primary action to call the number. + callBackButtonView.setTag(IntentProvider.getReturnCallIntentProvider(number)); + } + callBackButtonView.setVisibility(View.VISIBLE); + callBackButtonView.setOnClickListener(actionListener); + + final int titleId; + if (callType == Calls.VOICEMAIL_TYPE || callType == Calls.OUTGOING_TYPE) { + titleId = R.string.call_log_action_redial; + } else { + titleId = R.string.call_log_action_call_back; + } + callBackButtonView.setText(mContext.getString(titleId)); + } else { + // Number is not callable, so hide button. + callBackButtonView.setTag(null); + callBackButtonView.setVisibility(View.GONE); + } + + // If one of the calls had video capabilities, show the video call button. + if (CallUtil.isVideoEnabled(mContext) && canPlaceCallToNumber && + phoneCallDetailsViews.callTypeIcons.isVideoShown()) { + videoCallButtonView.setTag(IntentProvider.getReturnVideoCallIntentProvider(number)); + videoCallButtonView.setVisibility(View.VISIBLE); + videoCallButtonView.setOnClickListener(actionListener); + } else { + videoCallButtonView.setTag(null); + videoCallButtonView.setVisibility(View.GONE); + } + + // For voicemail calls, show the "VOICEMAIL" action button; hide otherwise. + if (callType == Calls.VOICEMAIL_TYPE) { + voicemailButtonView.setOnClickListener(actionListener); + voicemailButtonView.setTag( + IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri)); + voicemailButtonView.setVisibility(View.VISIBLE); + + detailsButtonView.setVisibility(View.GONE); + } else { + voicemailButtonView.setTag(null); + voicemailButtonView.setVisibility(View.GONE); + + detailsButtonView.setOnClickListener(actionListener); + detailsButtonView.setTag( + IntentProvider.getCallDetailIntentProvider(rowId, callIds, null)); + + if (canBeReportedAsInvalid && !reported) { + reportButtonView.setVisibility(View.VISIBLE); + } else { + reportButtonView.setVisibility(View.GONE); + } + } + + callLogViewsHelper.setActionContentDescriptions(this); + } + + /** + * Expands or collapses the view containing the CALLBACK/REDIAL, VOICEMAIL and DETAILS action + * buttons. + * + * TODO: Reduce number of classes which need to be passed in to inflate the action view stub. + * 1) Instantiate them in this class, and store local references. + * 2) Set them on the CallLogListItemHelper and use it for inflation. + * 3) Implement a parent view for a call log list item, and store references in that class. + */ + public void expandOrCollapseActions( + boolean isExpanded, + final CallLogAdapter.OnReportButtonClickListener onReportButtonClickListener, + View.OnClickListener actionListener, + PhoneNumberUtilsWrapper phoneNumberUtilsWrapper, + CallLogListItemHelper callLogViewsHelper) { + expandVoicemailTranscriptionView(isExpanded); + + if (isExpanded) { + // Inflate the view stub if necessary, and wire up the event handlers. + inflateActionViewStub(onReportButtonClickListener, actionListener, + phoneNumberUtilsWrapper, callLogViewsHelper); + + actionsView.setVisibility(View.VISIBLE); + actionsView.setAlpha(1.0f); + callLogEntryView.setBackgroundColor(mExpandedBackgroundColor); + callLogEntryView.setTranslationZ(mExpandedTranslationZ); + rootView.setTranslationZ(mExpandedTranslationZ); // WAR + } else { + // When recycling a view, it is possible the actionsView ViewStub was previously + // inflated so we should hide it in this case. + if (actionsView != null) { + actionsView.setVisibility(View.GONE); + } + + callLogEntryView.setBackgroundColor(mCallLogBackgroundColor); + callLogEntryView.setTranslationZ(0); + rootView.setTranslationZ(0); // WAR + } + } + + public void expandVoicemailTranscriptionView(boolean isExpanded) { + if (callType != Calls.VOICEMAIL_TYPE) { + return; + } + + final TextView view = phoneCallDetailsViews.voicemailTranscriptionView; + if (TextUtils.isEmpty(view.getText())) { + return; + } + view.setMaxLines(isExpanded ? VOICEMAIL_TRANSCRIPTION_MAX_LINES : 1); + view.setSingleLine(!isExpanded); + } + + public void setPhoto(long photoId, Uri photoUri, Uri contactUri, String displayName, + boolean isVoicemail, boolean isBusiness) { + quickContactView.assignContactUri(contactUri); + quickContactView.setOverlay(null); + + int contactType = ContactPhotoManager.TYPE_DEFAULT; + if (isVoicemail) { + contactType = ContactPhotoManager.TYPE_VOICEMAIL; + } else if (isBusiness) { + contactType = ContactPhotoManager.TYPE_BUSINESS; + } + + String lookupKey = null; + if (contactUri != null) { + lookupKey = ContactInfoHelper.getLookupKeyFromUri(contactUri); + } + + DefaultImageRequest request = new DefaultImageRequest( + displayName, lookupKey, contactType, true /* isCircular */); + + if (photoId == 0 && photoUri != null) { + ContactPhotoManager.getInstance(mContext).loadPhoto(quickContactView, photoUri, + mPhotoSize, false /* darkTheme */, true /* isCircular */, request); + } else { + ContactPhotoManager.getInstance(mContext).loadThumbnail(quickContactView, photoId, + false /* darkTheme */, true /* isCircular */, request); + } + } + @NeededForTesting public static CallLogListItemViews createForTest(Context context) { CallLogListItemViews views = new CallLogListItemViews( + context, + new View(context), new QuickContactBadge(context), new View(context), PhoneCallDetailsViews.createForTest(context), diff --git a/src/com/android/dialer/calllog/CallLogQuery.java b/src/com/android/dialer/calllog/CallLogQuery.java index 0ae4cda33..2b43c2857 100644 --- a/src/com/android/dialer/calllog/CallLogQuery.java +++ b/src/com/android/dialer/calllog/CallLogQuery.java @@ -45,7 +45,8 @@ public final class CallLogQuery { Calls.PHONE_ACCOUNT_ID, // 19 Calls.FEATURES, // 20 Calls.DATA_USAGE, // 21 - Calls.TRANSCRIPTION // 22 + Calls.TRANSCRIPTION, // 22 + Calls.CACHED_PHOTO_URI // 23 }; public static final int ID = 0; @@ -71,4 +72,5 @@ public final class CallLogQuery { public static final int FEATURES = 20; public static final int DATA_USAGE = 21; public static final int TRANSCRIPTION = 22; + public static final int CACHED_PHOTO_URI = 23; } diff --git a/src/com/android/dialer/calllog/ContactInfo.java b/src/com/android/dialer/calllog/ContactInfo.java index 7b6014dd1..effe14270 100644 --- a/src/com/android/dialer/calllog/ContactInfo.java +++ b/src/com/android/dialer/calllog/ContactInfo.java @@ -27,6 +27,11 @@ import com.google.common.base.Objects; */ public class ContactInfo { public Uri lookupUri; + + /** + * Contact lookup key. Note this may be a lookup key for a corp contact, in which case + * "lookup by lookup key" doesn't work on the personal profile. + */ public String lookupKey; public String name; public int type; diff --git a/src/com/android/dialer/calllog/ContactInfoHelper.java b/src/com/android/dialer/calllog/ContactInfoHelper.java index 01749fc22..8e8aa3ce1 100644 --- a/src/com/android/dialer/calllog/ContactInfoHelper.java +++ b/src/com/android/dialer/calllog/ContactInfoHelper.java @@ -14,9 +14,12 @@ package com.android.dialer.calllog; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.database.sqlite.SQLiteFullException; import android.net.Uri; +import android.provider.CallLog.Calls; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; @@ -24,6 +27,7 @@ import android.provider.ContactsContract.DisplayNameSources; import android.provider.ContactsContract.PhoneLookup; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; +import android.util.Log; import com.android.contacts.common.util.Constants; import com.android.contacts.common.util.PhoneNumberHelper; @@ -41,6 +45,8 @@ import java.util.List; * Utility class to look up the contact information for a given number. */ public class ContactInfoHelper { + private static final String TAG = ContactInfoHelper.class.getSimpleName(); + private final Context mContext; private final String mCurrentCountryIso; @@ -64,6 +70,9 @@ public class ContactInfoHelper { * @param countryIso the country associated with this number */ public ContactInfo lookupNumber(String number, String countryIso) { + if (TextUtils.isEmpty(number)) { + return null; + } final ContactInfo info; // Determine the contact info. @@ -149,6 +158,9 @@ public class ContactInfoHelper { * value. */ private ContactInfo lookupContactFromUri(Uri uri) { + if (uri == null) { + return null; + } final ContactInfo info; Cursor phonesCursor = mContext.getContentResolver().query(uri, PhoneQuery._PROJECTION, null, null, null); @@ -193,10 +205,13 @@ public class ContactInfoHelper { * If the lookup fails for some other reason, it returns null. */ private ContactInfo queryContactInfoForSipAddress(String sipAddress) { + if (TextUtils.isEmpty(sipAddress)) { + return null; + } final ContactInfo info; // "contactNumber" is a SIP address, so use the PhoneLookup table with the SIP parameter. - Uri.Builder uriBuilder = PhoneLookup.CONTENT_FILTER_URI.buildUpon(); + Uri.Builder uriBuilder = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI.buildUpon(); uriBuilder.appendPath(Uri.encode(sipAddress)); uriBuilder.appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, "1"); return lookupContactFromUri(uriBuilder.build()); @@ -212,6 +227,9 @@ public class ContactInfoHelper { * If the lookup fails for some other reason, it returns null. */ private ContactInfo queryContactInfoForPhoneNumber(String number, String countryIso) { + if (TextUtils.isEmpty(number)) { + return null; + } String contactNumber = number; if (!TextUtils.isEmpty(countryIso)) { // Normalize the number: this is needed because the PhoneLookup query below does not @@ -224,7 +242,8 @@ public class ContactInfoHelper { } // The "contactNumber" is a regular phone number, so use the PhoneLookup table. - Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(contactNumber)); + Uri uri = Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, + Uri.encode(contactNumber)); ContactInfo info = lookupContactFromUri(uri); if (info != null && info != ContactInfo.EMPTY) { info.formattedNumber = formatPhoneNumber(number, null, countryIso); @@ -265,6 +284,107 @@ public class ContactInfoHelper { } /** + * Stores differences between the updated contact info and the current call log contact info. + * + * @param number The number of the contact. + * @param countryIso The country associated with this number. + * @param updatedInfo The updated contact info. + * @param callLogInfo The call log entry's current contact info. + */ + public void updateCallLogContactInfo(String number, String countryIso, ContactInfo updatedInfo, + ContactInfo callLogInfo) { + final ContentValues values = new ContentValues(); + boolean needsUpdate = false; + + if (callLogInfo != null) { + if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) { + values.put(Calls.CACHED_NAME, updatedInfo.name); + needsUpdate = true; + } + + if (updatedInfo.type != callLogInfo.type) { + values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type); + needsUpdate = true; + } + + if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) { + values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label); + needsUpdate = true; + } + + if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) { + values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); + needsUpdate = true; + } + + // Only replace the normalized number if the new updated normalized number isn't empty. + if (!TextUtils.isEmpty(updatedInfo.normalizedNumber) && + !TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) { + values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); + needsUpdate = true; + } + + if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) { + values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number); + needsUpdate = true; + } + + if (updatedInfo.photoId != callLogInfo.photoId) { + values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId); + needsUpdate = true; + } + + final Uri updatedPhotoUriContactsOnly = + UriUtils.nullForNonContactsUri(updatedInfo.photoUri); + if (!UriUtils.areEqual(updatedPhotoUriContactsOnly, callLogInfo.photoUri)) { + values.put(Calls.CACHED_PHOTO_URI, + UriUtils.uriToString(updatedPhotoUriContactsOnly)); + needsUpdate = true; + } + + if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) { + values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); + needsUpdate = true; + } + } else { + // No previous values, store all of them. + values.put(Calls.CACHED_NAME, updatedInfo.name); + values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type); + values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label); + values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); + values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number); + values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); + values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId); + values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString( + UriUtils.nullForNonContactsUri(updatedInfo.photoUri))); + values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); + needsUpdate = true; + } + + if (!needsUpdate) { + return; + } + + try { + if (countryIso == null) { + mContext.getContentResolver().update( + Calls.CONTENT_URI_WITH_VOICEMAIL, + values, + Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL", + new String[]{ number }); + } else { + mContext.getContentResolver().update( + Calls.CONTENT_URI_WITH_VOICEMAIL, + values, + Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?", + new String[]{ number, countryIso }); + } + } catch (SQLiteFullException e) { + Log.e(TAG, "Unable to update contact info in call log db", e); + } + } + + /** * Parses the given URI to determine the original lookup key of the contact. */ public static String getLookupKeyFromUri(Uri lookupUri) { @@ -283,6 +403,29 @@ public class ContactInfoHelper { } /** + * Returns the contact information stored in an entry of the call log. + * + * @param c A cursor pointing to an entry in the call log. + */ + public static ContactInfo getContactInfo(Cursor c) { + ContactInfo info = new ContactInfo(); + + info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI)); + info.name = c.getString(CallLogQuery.CACHED_NAME); + info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE); + info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL); + String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER); + info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber; + info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER); + info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID); + info.photoUri = UriUtils.nullForNonContactsUri( + UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_PHOTO_URI))); + info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER); + + return info; + } + + /** * Given a contact's sourceType, return true if the contact is a business * * @param sourceType sourceType of the contact. This is usually populated by diff --git a/src/com/android/dialer/calllog/GroupingListAdapter.java b/src/com/android/dialer/calllog/GroupingListAdapter.java new file mode 100644 index 000000000..78955492e --- /dev/null +++ b/src/com/android/dialer/calllog/GroupingListAdapter.java @@ -0,0 +1,490 @@ +/* + * Copyright (C) 2015 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.calllog; + +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.os.Handler; +import android.util.SparseIntArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +import com.android.contacts.common.testing.NeededForTesting; + +/** + * Maintains a list that groups adjacent items sharing the same value of a "group-by" field. + * + * The list has three types of elements: stand-alone, group header and group child. Groups are + * collapsible and collapsed by default. This is used by the call log to group related entries. + */ +abstract class GroupingListAdapter extends BaseAdapter { + + private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16; + private static final int GROUP_METADATA_ARRAY_INCREMENT = 128; + private static final long GROUP_OFFSET_MASK = 0x00000000FFFFFFFFL; + private static final long GROUP_SIZE_MASK = 0x7FFFFFFF00000000L; + private static final long EXPANDED_GROUP_MASK = 0x8000000000000000L; + + public static final int ITEM_TYPE_STANDALONE = 0; + public static final int ITEM_TYPE_GROUP_HEADER = 1; + public static final int ITEM_TYPE_IN_GROUP = 2; + + /** + * Information about a specific list item: is it a group, if so is it expanded. + * Otherwise, is it a stand-alone item or a group member. + */ + protected static class PositionMetadata { + int itemType; + boolean isExpanded; + int cursorPosition; + int childCount; + private int groupPosition; + private int listPosition = -1; + } + + private Context mContext; + private Cursor mCursor; + + /** + * Count of list items. + */ + private int mCount; + + private int mRowIdColumnIndex; + + /** + * Count of groups in the list. + */ + private int mGroupCount; + + /** + * Information about where these groups are located in the list, how large they are + * and whether they are expanded. + */ + private long[] mGroupMetadata; + + private SparseIntArray mPositionCache = new SparseIntArray(); + private int mLastCachedListPosition; + private int mLastCachedCursorPosition; + private int mLastCachedGroup; + + /** + * A reusable temporary instance of PositionMetadata + */ + private PositionMetadata mPositionMetadata = new PositionMetadata(); + + protected ContentObserver mChangeObserver = new ContentObserver(new Handler()) { + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + onContentChanged(); + } + }; + + protected DataSetObserver mDataSetObserver = new DataSetObserver() { + + @Override + public void onChanged() { + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + notifyDataSetInvalidated(); + } + }; + + public GroupingListAdapter(Context context) { + mContext = context; + resetCache(); + } + + /** + * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for + * each of them. + */ + protected abstract void addGroups(Cursor cursor); + + protected abstract View newStandAloneView(Context context, ViewGroup parent); + protected abstract void bindStandAloneView(View view, Context context, Cursor cursor); + + protected abstract View newGroupView(Context context, ViewGroup parent); + protected abstract void bindGroupView(View view, Context context, Cursor cursor, int groupSize, + boolean expanded); + + protected abstract View newChildView(Context context, ViewGroup parent); + protected abstract void bindChildView(View view, Context context, Cursor cursor); + + /** + * Cache should be reset whenever the cursor changes or groups are expanded or collapsed. + */ + private void resetCache() { + mCount = -1; + mLastCachedListPosition = -1; + mLastCachedCursorPosition = -1; + mLastCachedGroup = -1; + mPositionMetadata.listPosition = -1; + mPositionCache.clear(); + } + + protected void onContentChanged() { + } + + public void changeCursor(Cursor cursor) { + if (cursor == mCursor) { + return; + } + + if (mCursor != null) { + mCursor.unregisterContentObserver(mChangeObserver); + mCursor.unregisterDataSetObserver(mDataSetObserver); + mCursor.close(); + } + mCursor = cursor; + resetCache(); + findGroups(); + + if (cursor != null) { + cursor.registerContentObserver(mChangeObserver); + cursor.registerDataSetObserver(mDataSetObserver); + mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id"); + notifyDataSetChanged(); + } else { + // notify the observers about the lack of a data set + notifyDataSetInvalidated(); + } + + } + + public Cursor getCursor() { + return mCursor; + } + + /** + * Scans over the entire cursor looking for duplicate phone numbers that need + * to be collapsed. + */ + private void findGroups() { + mGroupCount = 0; + mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE]; + + if (mCursor == null) { + return; + } + + addGroups(mCursor); + } + + /** + * Records information about grouping in the list. Should be called by the overridden + * {@link #addGroups} method. + */ + protected void addGroup(int cursorPosition, int size, boolean expanded) { + if (mGroupCount >= mGroupMetadata.length) { + int newSize = idealLongArraySize( + mGroupMetadata.length + GROUP_METADATA_ARRAY_INCREMENT); + long[] array = new long[newSize]; + System.arraycopy(mGroupMetadata, 0, array, 0, mGroupCount); + mGroupMetadata = array; + } + + long metadata = ((long)size << 32) | cursorPosition; + if (expanded) { + metadata |= EXPANDED_GROUP_MASK; + } + mGroupMetadata[mGroupCount++] = metadata; + } + + // Copy/paste from ArrayUtils + private int idealLongArraySize(int need) { + return idealByteArraySize(need * 8) / 8; + } + + // Copy/paste from ArrayUtils + private int idealByteArraySize(int need) { + for (int i = 4; i < 32; i++) + if (need <= (1 << i) - 12) + return (1 << i) - 12; + + return need; + } + + public int getCount() { + if (mCursor == null) { + return 0; + } + + if (mCount != -1) { + return mCount; + } + + int cursorPosition = 0; + int count = 0; + for (int i = 0; i < mGroupCount; i++) { + long metadata = mGroupMetadata[i]; + int offset = (int)(metadata & GROUP_OFFSET_MASK); + boolean expanded = (metadata & EXPANDED_GROUP_MASK) != 0; + int size = (int)((metadata & GROUP_SIZE_MASK) >> 32); + + count += (offset - cursorPosition); + + if (expanded) { + count += size + 1; + } else { + count++; + } + + cursorPosition = offset + size; + } + + mCount = count + mCursor.getCount() - cursorPosition; + return mCount; + } + + /** + * Figures out whether the item at the specified position represents a + * stand-alone element, a group or a group child. Also computes the + * corresponding cursor position. + */ + public void obtainPositionMetadata(PositionMetadata metadata, int position) { + + // If the description object already contains requested information, just return + if (metadata.listPosition == position) { + return; + } + + int listPosition = 0; + int cursorPosition = 0; + int firstGroupToCheck = 0; + + // Check cache for the supplied position. What we are looking for is + // the group descriptor immediately preceding the supplied position. + // Once we have that, we will be able to tell whether the position + // is the header of the group, a member of the group or a standalone item. + if (mLastCachedListPosition != -1) { + if (position <= mLastCachedListPosition) { + + // Have SparceIntArray do a binary search for us. + int index = mPositionCache.indexOfKey(position); + + // If we get back a positive number, the position corresponds to + // a group header. + if (index < 0) { + + // We had a cache miss, but we did obtain valuable information anyway. + // The negative number will allow us to compute the location of + // the group header immediately preceding the supplied position. + index = ~index - 1; + + if (index >= mPositionCache.size()) { + index--; + } + } + + // A non-negative index gives us the position of the group header + // corresponding or preceding the position, so we can + // search for the group information at the supplied position + // starting with the cached group we just found + if (index >= 0) { + listPosition = mPositionCache.keyAt(index); + firstGroupToCheck = mPositionCache.valueAt(index); + long descriptor = mGroupMetadata[firstGroupToCheck]; + cursorPosition = (int)(descriptor & GROUP_OFFSET_MASK); + } + } else { + + // If we haven't examined groups beyond the supplied position, + // we will start where we left off previously + firstGroupToCheck = mLastCachedGroup; + listPosition = mLastCachedListPosition; + cursorPosition = mLastCachedCursorPosition; + } + } + + for (int i = firstGroupToCheck; i < mGroupCount; i++) { + long group = mGroupMetadata[i]; + int offset = (int)(group & GROUP_OFFSET_MASK); + + // Move pointers to the beginning of the group + listPosition += (offset - cursorPosition); + cursorPosition = offset; + + if (i > mLastCachedGroup) { + mPositionCache.append(listPosition, i); + mLastCachedListPosition = listPosition; + mLastCachedCursorPosition = cursorPosition; + mLastCachedGroup = i; + } + + // Now we have several possibilities: + // A) The requested position precedes the group + if (position < listPosition) { + metadata.itemType = ITEM_TYPE_STANDALONE; + metadata.cursorPosition = cursorPosition - (listPosition - position); + return; + } + + boolean expanded = (group & EXPANDED_GROUP_MASK) != 0; + int size = (int) ((group & GROUP_SIZE_MASK) >> 32); + + // B) The requested position is a group header + if (position == listPosition) { + metadata.itemType = ITEM_TYPE_GROUP_HEADER; + metadata.groupPosition = i; + metadata.isExpanded = expanded; + metadata.childCount = size; + metadata.cursorPosition = offset; + return; + } + + if (expanded) { + // C) The requested position is an element in the expanded group + if (position < listPosition + size + 1) { + metadata.itemType = ITEM_TYPE_IN_GROUP; + metadata.cursorPosition = cursorPosition + (position - listPosition) - 1; + return; + } + + // D) The element is past the expanded group + listPosition += size + 1; + } else { + + // E) The element is past the collapsed group + listPosition++; + } + + // Move cursor past the group + cursorPosition += size; + } + + // The required item is past the last group + metadata.itemType = ITEM_TYPE_STANDALONE; + metadata.cursorPosition = cursorPosition + (position - listPosition); + } + + /** + * Returns true if the specified position in the list corresponds to a + * group header. + */ + public boolean isGroupHeader(int position) { + obtainPositionMetadata(mPositionMetadata, position); + return mPositionMetadata.itemType == ITEM_TYPE_GROUP_HEADER; + } + + /** + * Given a position of a groups header in the list, returns the size of + * the corresponding group. + */ + public int getGroupSize(int position) { + obtainPositionMetadata(mPositionMetadata, position); + return mPositionMetadata.childCount; + } + + /** + * Mark group as expanded if it is collapsed and vice versa. + */ + @NeededForTesting + public void toggleGroup(int position) { + obtainPositionMetadata(mPositionMetadata, position); + if (mPositionMetadata.itemType != ITEM_TYPE_GROUP_HEADER) { + throw new IllegalArgumentException("Not a group at position " + position); + } + + if (mPositionMetadata.isExpanded) { + mGroupMetadata[mPositionMetadata.groupPosition] &= ~EXPANDED_GROUP_MASK; + } else { + mGroupMetadata[mPositionMetadata.groupPosition] |= EXPANDED_GROUP_MASK; + } + resetCache(); + notifyDataSetChanged(); + } + + @Override + public int getViewTypeCount() { + return 3; + } + + @Override + public int getItemViewType(int position) { + obtainPositionMetadata(mPositionMetadata, position); + return mPositionMetadata.itemType; + } + + public Object getItem(int position) { + if (mCursor == null) { + return null; + } + + obtainPositionMetadata(mPositionMetadata, position); + if (mCursor.moveToPosition(mPositionMetadata.cursorPosition)) { + return mCursor; + } else { + return null; + } + } + + public long getItemId(int position) { + Object item = getItem(position); + if (item != null) { + return mCursor.getLong(mRowIdColumnIndex); + } else { + return -1; + } + } + + public View getView(int position, View convertView, ViewGroup parent) { + obtainPositionMetadata(mPositionMetadata, position); + View view = convertView; + if (view == null) { + switch (mPositionMetadata.itemType) { + case ITEM_TYPE_STANDALONE: + view = newStandAloneView(mContext, parent); + break; + case ITEM_TYPE_GROUP_HEADER: + view = newGroupView(mContext, parent); + break; + case ITEM_TYPE_IN_GROUP: + view = newChildView(mContext, parent); + break; + } + } + + mCursor.moveToPosition(mPositionMetadata.cursorPosition); + switch (mPositionMetadata.itemType) { + case ITEM_TYPE_STANDALONE: + bindStandAloneView(view, mContext, mCursor); + break; + case ITEM_TYPE_GROUP_HEADER: + bindGroupView(view, mContext, mCursor, mPositionMetadata.childCount, + mPositionMetadata.isExpanded); + break; + case ITEM_TYPE_IN_GROUP: + bindChildView(view, mContext, mCursor); + break; + + } + return view; + } +} diff --git a/src/com/android/dialer/calllog/IntentProvider.java b/src/com/android/dialer/calllog/IntentProvider.java index 3084e2424..2bd3f2eef 100644 --- a/src/com/android/dialer/calllog/IntentProvider.java +++ b/src/com/android/dialer/calllog/IntentProvider.java @@ -16,15 +16,23 @@ package com.android.dialer.calllog; +import android.content.ContentValues; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.provider.CallLog.Calls; +import android.provider.ContactsContract; import android.telecom.PhoneAccountHandle; -import com.android.contacts.common.CallUtil; +import com.android.contacts.common.model.Contact; +import com.android.contacts.common.model.ContactLoader; import com.android.dialer.CallDetailActivity; +import com.android.dialer.DialtactsActivity; +import com.android.dialer.PhoneCallDetails; +import com.android.dialer.util.PrivilegedCallUtil; + +import java.util.ArrayList; /** * Used to create an intent to attach to an action in the call log. @@ -46,7 +54,7 @@ public abstract class IntentProvider { return new IntentProvider() { @Override public Intent getIntent(Context context) { - return CallUtil.getCallIntent(number, accountHandle); + return PrivilegedCallUtil.getCallIntent(number, accountHandle); } }; } @@ -60,7 +68,7 @@ public abstract class IntentProvider { return new IntentProvider() { @Override public Intent getIntent(Context context) { - return CallUtil.getVideoCallIntent(number, accountHandle); + return PrivilegedCallUtil.getVideoCallIntent(number, accountHandle); } }; } @@ -69,7 +77,7 @@ public abstract class IntentProvider { return new IntentProvider() { @Override public Intent getIntent(Context context) { - return CallUtil.getVoicemailIntent(); + return PrivilegedCallUtil.getVoicemailIntent(); } }; } @@ -124,4 +132,68 @@ public abstract class IntentProvider { } }; } + + /** + * Retrieves an add contact intent for the given contact and phone call details. + * + * @param info The contact info. + * @param details The phone call details. + */ + public static IntentProvider getAddContactIntentProvider( + final ContactInfo info, final PhoneCallDetails details) { + return new IntentProvider() { + @Override + public Intent getIntent(Context context) { + Contact contactToSave = null; + + if (info.lookupUri != null) { + contactToSave = ContactLoader.parseEncodedContactEntity(info.lookupUri); + } + + if (contactToSave != null) { + // Populate the intent with contact information stored in the lookup URI. + // Note: This code mirrors code in Contacts/QuickContactsActivity. + final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); + + ArrayList<ContentValues> values = contactToSave.getContentValues(); + // Only pre-fill the name field if the provided display name is an nickname + // or better (e.g. structured name, nickname) + if (contactToSave.getDisplayNameSource() + >= ContactsContract.DisplayNameSources.NICKNAME) { + intent.putExtra(ContactsContract.Intents.Insert.NAME, + contactToSave.getDisplayName()); + } else if (contactToSave.getDisplayNameSource() + == ContactsContract.DisplayNameSources.ORGANIZATION) { + // This is probably an organization. Instead of copying the organization + // name into a name entry, copy it into the organization entry. This + // way we will still consider the contact an organization. + final ContentValues organization = new ContentValues(); + organization.put(ContactsContract.CommonDataKinds.Organization.COMPANY, + contactToSave.getDisplayName()); + organization.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE); + values.add(organization); + } + + // Last time used and times used are aggregated values from the usage stat + // table. They need to be removed from data values so the SQL table can insert + // properly + for (ContentValues value : values) { + value.remove(ContactsContract.Data.LAST_TIME_USED); + value.remove(ContactsContract.Data.TIMES_USED); + } + + intent.putExtra(ContactsContract.Intents.Insert.DATA, values); + + return intent; + } else { + // If no lookup uri is provided, rely on the available phone number and name. + return DialtactsActivity.getAddToContactIntent(details.name, + details.number, + details.numberType); + } + } + }; + } } diff --git a/src/com/android/dialer/calllog/PhoneNumberDisplayHelper.java b/src/com/android/dialer/calllog/PhoneNumberDisplayHelper.java index 0dffd868a..c1a5abfe1 100644 --- a/src/com/android/dialer/calllog/PhoneNumberDisplayHelper.java +++ b/src/com/android/dialer/calllog/PhoneNumberDisplayHelper.java @@ -75,7 +75,6 @@ public class PhoneNumberDisplayHelper { */ public CharSequence getDisplayNumber(PhoneAccountHandle accountHandle, CharSequence number, int presentation, CharSequence formattedNumber) { - final CharSequence displayName = getDisplayName(accountHandle, number, presentation); if (!TextUtils.isEmpty(displayName)) { return displayName; diff --git a/src/com/android/dialer/contactinfo/ContactInfoCache.java b/src/com/android/dialer/contactinfo/ContactInfoCache.java new file mode 100644 index 000000000..2bb0f1e95 --- /dev/null +++ b/src/com/android/dialer/contactinfo/ContactInfoCache.java @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2015 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.contactinfo; + +import android.os.Handler; +import android.os.Message; +import android.text.TextUtils; + +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.calllog.ContactInfoHelper; +import com.android.dialer.util.ExpirableCache; +import com.google.common.annotations.VisibleForTesting; + +import java.util.LinkedList; + +/** + * This is a cache of contact details for the phone numbers in the c all log. The key is the + * phone number with the country in which teh call was placed or received. The content of the + * cache is expired (but not purged) whenever the application comes to the foreground. + * + * This cache queues request for information and queries for information on a background thread, + * so {@code start()} and {@code stop()} must be called to initiate or halt that thread's exeuction + * as needed. + * + * TODO: Explore whether there is a pattern to remove external dependencies for starting and + * stopping the query thread. + */ +public class ContactInfoCache { + public interface OnContactInfoChangedListener { + public void onContactInfoChanged(); + } + + /* + * Handles requests for contact name and number type. + */ + private class QueryThread extends Thread { + private volatile boolean mDone = false; + + public QueryThread() { + super("CallLogAdapter.QueryThread"); + } + + public void stopProcessing() { + mDone = true; + } + + @Override + public void run() { + boolean needRedraw = false; + while (true) { + // Check if thread is finished, and if so return immediately. + if (mDone) return; + + // Obtain next request, if any is available. + // Keep synchronized section small. + ContactInfoRequest req = null; + synchronized (mRequests) { + if (!mRequests.isEmpty()) { + req = mRequests.removeFirst(); + } + } + + if (req != null) { + // Process the request. If the lookup succeeds, schedule a redraw. + needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo); + } else { + // Throttle redraw rate by only sending them when there are + // more requests. + if (needRedraw) { + needRedraw = false; + mHandler.sendEmptyMessage(REDRAW); + } + + // Wait until another request is available, or until this + // thread is no longer needed (as indicated by being + // interrupted). + try { + synchronized (mRequests) { + mRequests.wait(1000); + } + } catch (InterruptedException ie) { + // Ignore, and attempt to continue processing requests. + } + } + } + } + } + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case REDRAW: + mOnContactInfoChangedListener.onContactInfoChanged(); + break; + case START_THREAD: + startRequestProcessing(); + break; + } + } + }; + + private static final int REDRAW = 1; + private static final int START_THREAD = 2; + + private static final int CONTACT_INFO_CACHE_SIZE = 100; + private static final int START_PROCESSING_REQUESTS_DELAY_MS = 1000; + + + /** + * List of requests to update contact details. Each request contains a phone number to look up, + * and the contact info currently stored in the call log for this number. + * + * The requests are added when displaying contacts and are processed by a background thread. + */ + private final LinkedList<ContactInfoRequest> mRequests; + + private ExpirableCache<NumberWithCountryIso, ContactInfo> mCache; + + private ContactInfoHelper mContactInfoHelper; + private QueryThread mContactInfoQueryThread; + private OnContactInfoChangedListener mOnContactInfoChangedListener; + + public ContactInfoCache(ContactInfoHelper contactInfoHelper, + OnContactInfoChangedListener onContactInfoChangedListener) { + mContactInfoHelper = contactInfoHelper; + mOnContactInfoChangedListener = onContactInfoChangedListener; + + mRequests = new LinkedList<ContactInfoRequest>(); + mCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); + } + + public ContactInfo getValue(String number, String countryIso, ContactInfo cachedContactInfo) { + NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); + ExpirableCache.CachedValue<ContactInfo> cachedInfo = + mCache.getCachedValue(numberCountryIso); + ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); + if (cachedInfo == null) { + mCache.put(numberCountryIso, ContactInfo.EMPTY); + // Use the cached contact info from the call log. + info = cachedContactInfo; + // The db request should happen on a non-UI thread. + // Request the contact details immediately since they are currently missing. + enqueueRequest(number, countryIso, cachedContactInfo, true); + // We will format the phone number when we make the background request. + } else { + if (cachedInfo.isExpired()) { + // The contact info is no longer up to date, we should request it. However, we + // do not need to request them immediately. + enqueueRequest(number, countryIso, cachedContactInfo, false); + } else if (!callLogInfoMatches(cachedContactInfo, info)) { + // The call log information does not match the one we have, look it up again. + // We could simply update the call log directly, but that needs to be done in a + // background thread, so it is easier to simply request a new lookup, which will, as + // a side-effect, update the call log. + enqueueRequest(number, countryIso, cachedContactInfo, false); + } + + if (info == ContactInfo.EMPTY) { + // Use the cached contact info from the call log. + info = cachedContactInfo; + } + } + return info; + } + + /** + * Queries the appropriate content provider for the contact associated with the number. + * + * Upon completion it also updates the cache in the call log, if it is different from + * {@code callLogInfo}. + * + * The number might be either a SIP address or a phone number. + * + * It returns true if it updated the content of the cache and we should therefore tell the + * view to update its content. + */ + private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) { + final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso); + + if (info == null) { + // The lookup failed, just return without requesting to update the view. + return false; + } + + // Check the existing entry in the cache: only if it has changed we should update the + // view. + NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); + ContactInfo existingInfo = mCache.getPossiblyExpired(numberCountryIso); + + final boolean isRemoteSource = info.sourceType != 0; + + // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY} + // to avoid updating the data set for every new row that is scrolled into view. + // see (https://googleplex-android-review.git.corp.google.com/#/c/166680/) + + // Exception: Photo uris for contacts from remote sources are not cached in the call log + // cache, so we have to force a redraw for these contacts regardless. + boolean updated = (existingInfo != ContactInfo.EMPTY || isRemoteSource) && + !info.equals(existingInfo); + + // Store the data in the cache so that the UI thread can use to display it. Store it + // even if it has not changed so that it is marked as not expired. + mCache.put(numberCountryIso, info); + + // Update the call log even if the cache it is up-to-date: it is possible that the cache + // contains the value from a different call log entry. + mContactInfoHelper.updateCallLogContactInfo(number, countryIso, info, callLogInfo); + return updated; + } + + /** + * After a delay, start the thread to begin processing requests. We perform lookups on a + * background thread, but this must be called to indicate the thread should be running. + */ + public void start() { + // Schedule a thread-creation message if the thread hasn't been created yet, as an + // optimization to queue fewer messages. + if (mContactInfoQueryThread == null) { + // TODO: Check whether this delay before starting to process is necessary. + mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MS); + } + } + + /** + * Stops the thread and clears the queue of messages to process. This cleans up the thread + * for lookups so that it is not perpetually running. + */ + public void stop() { + stopRequestProcessing(); + } + + /** + * Starts a background thread to process contact-lookup requests, unless one + * has already been started. + */ + private synchronized void startRequestProcessing() { + // For unit-testing. + if (mRequestProcessingDisabled) return; + + // If a thread is already started, don't start another. + if (mContactInfoQueryThread != null) { + return; + } + + mContactInfoQueryThread = new QueryThread(); + mContactInfoQueryThread.setPriority(Thread.MIN_PRIORITY); + mContactInfoQueryThread.start(); + } + + public void invalidate() { + mCache.expireAll(); + stopRequestProcessing(); + } + + /** + * Stops the background thread that processes updates and cancels any + * pending requests to start it. + */ + private synchronized void stopRequestProcessing() { + // Remove any pending requests to start the processing thread. + mHandler.removeMessages(START_THREAD); + if (mContactInfoQueryThread != null) { + // Stop the thread; we are finished with it. + mContactInfoQueryThread.stopProcessing(); + mContactInfoQueryThread.interrupt(); + mContactInfoQueryThread = null; + } + } + + /** + * Enqueues a request to look up the contact details for the given phone number. + * <p> + * It also provides the current contact info stored in the call log for this number. + * <p> + * If the {@code immediate} parameter is true, it will start immediately the thread that looks + * up the contact information (if it has not been already started). Otherwise, it will be + * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}. + */ + protected void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo, + boolean immediate) { + ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo); + synchronized (mRequests) { + if (!mRequests.contains(request)) { + mRequests.add(request); + mRequests.notifyAll(); + } + } + if (immediate) { + startRequestProcessing(); + } + } + + /** + * Checks whether the contact info from the call log matches the one from the contacts db. + */ + private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) { + // The call log only contains a subset of the fields in the contacts db. + // Only check those. + return TextUtils.equals(callLogInfo.name, info.name) + && callLogInfo.type == info.type + && TextUtils.equals(callLogInfo.label, info.label); + } + + /** + * Can be set to true by tests to disable processing of requests. + */ + @VisibleForTesting + private volatile boolean mRequestProcessingDisabled = false; + + /** + * Sets whether processing of requests for contact details should be enabled. + * + * This method should be called in tests to disable such processing of requests when not + * needed. + */ + @VisibleForTesting + public void disableRequestProcessingForTest() { + mRequestProcessingDisabled = true; + } + + @VisibleForTesting + public void injectContactInfoForTest( + String number, String countryIso, ContactInfo contactInfo) { + NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); + mCache.put(numberCountryIso, contactInfo); + } +} diff --git a/src/com/android/dialer/contactinfo/ContactInfoRequest.java b/src/com/android/dialer/contactinfo/ContactInfoRequest.java new file mode 100644 index 000000000..ec5c1198e --- /dev/null +++ b/src/com/android/dialer/contactinfo/ContactInfoRequest.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 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.contactinfo; + +import android.text.TextUtils; + +import com.android.dialer.calllog.ContactInfo; +import com.google.common.base.Objects; + +/** + * A request for contact details for the given number, used by the ContactInfoCache. + */ +public final class ContactInfoRequest { + /** The number to look-up. */ + public final String number; + /** The country in which a call to or from this number was placed or received. */ + public final String countryIso; + /** The cached contact information stored in the call log. */ + public final ContactInfo callLogInfo; + + public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) { + this.number = number; + this.countryIso = countryIso; + this.callLogInfo = callLogInfo; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof ContactInfoRequest)) return false; + + ContactInfoRequest other = (ContactInfoRequest) obj; + + if (!TextUtils.equals(number, other.number)) return false; + if (!TextUtils.equals(countryIso, other.countryIso)) return false; + if (!Objects.equal(callLogInfo, other.callLogInfo)) return false; + + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode()); + result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode()); + result = prime * result + ((number == null) ? 0 : number.hashCode()); + return result; + } +} diff --git a/src/com/android/dialer/contactinfo/NumberWithCountryIso.java b/src/com/android/dialer/contactinfo/NumberWithCountryIso.java new file mode 100644 index 000000000..1383fb7e9 --- /dev/null +++ b/src/com/android/dialer/contactinfo/NumberWithCountryIso.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 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.contactinfo; + +import android.text.TextUtils; + +/** + * Stores a phone number of a call with the country code where it originally occurred. This object + * is used as a key in the {@code ContactInfoCache}. + * + * The country does not necessarily specify the country of the phone number itself, but rather + * it is the country in which the user was in when the call was placed or received. + */ +public final class NumberWithCountryIso { + public final String number; + public final String countryIso; + + public NumberWithCountryIso(String number, String countryIso) { + this.number = number; + this.countryIso = countryIso; + } + + @Override + public boolean equals(Object o) { + if (o == null) return false; + if (!(o instanceof NumberWithCountryIso)) return false; + NumberWithCountryIso other = (NumberWithCountryIso) o; + return TextUtils.equals(number, other.number) + && TextUtils.equals(countryIso, other.countryIso); + } + + @Override + public int hashCode() { + int numberHashCode = number == null ? 0 : number.hashCode(); + int countryHashCode = countryIso == null ? 0 : countryIso.hashCode(); + + return numberHashCode ^ countryHashCode; + } +} diff --git a/src/com/android/dialer/database/DialerDatabaseHelper.java b/src/com/android/dialer/database/DialerDatabaseHelper.java index 511c2a7bc..2177878c6 100644 --- a/src/com/android/dialer/database/DialerDatabaseHelper.java +++ b/src/com/android/dialer/database/DialerDatabaseHelper.java @@ -93,6 +93,9 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { static final String PROPERTIES = "properties"; } + public static final Uri SMART_DIAL_UPDATED_URI = + Uri.parse("content://com.android.dialer/smart_dial_updated"); + public interface SmartDialDbColumns { static final String _ID = "id"; static final String DATA_ID = "data_id"; @@ -893,6 +896,9 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit(); editor.putLong(LAST_UPDATED_MILLIS, currentMillis); editor.commit(); + + // Notify content observers that smart dial database has been updated. + mContext.getContentResolver().notifyChange(SMART_DIAL_UPDATED_URI, null, false); } } diff --git a/src/com/android/dialer/dialpad/DialpadFragment.java b/src/com/android/dialer/dialpad/DialpadFragment.java index f1112f175..a910f7a1e 100644 --- a/src/com/android/dialer/dialpad/DialpadFragment.java +++ b/src/com/android/dialer/dialpad/DialpadFragment.java @@ -36,6 +36,7 @@ import android.media.AudioManager; import android.media.ToneGenerator; import android.net.Uri; import android.os.Bundle; +import android.os.Trace; import android.provider.Contacts.People; import android.provider.Contacts.Phones; import android.provider.Contacts.PhonesColumns; @@ -44,13 +45,10 @@ import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; import android.telephony.PhoneNumberUtils; -import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.text.Editable; -import android.text.SpannableString; import android.text.TextUtils; import android.text.TextWatcher; -import android.text.style.RelativeSizeSpan; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; @@ -70,7 +68,6 @@ import android.widget.PopupMenu; import android.widget.RelativeLayout; import android.widget.TextView; -import com.android.contacts.common.CallUtil; import com.android.contacts.common.ContactsUtils; import com.android.contacts.common.GeoUtil; import com.android.contacts.common.util.PhoneNumberFormatter; @@ -81,6 +78,7 @@ import com.android.dialer.NeededForReflection; import com.android.dialer.R; import com.android.dialer.SpecialCharSequenceMgr; import com.android.dialer.calllog.PhoneAccountUtils; +import com.android.dialer.util.PrivilegedCallUtil; import com.android.dialer.util.DialerUtils; import com.android.phone.common.CallLogAsync; import com.android.phone.common.HapticFeedback; @@ -102,7 +100,7 @@ public class DialpadFragment extends Fragment AdapterView.OnItemClickListener, TextWatcher, PopupMenu.OnMenuItemClickListener, DialpadKeyButton.OnPressedListener { - private static final String TAG = DialpadFragment.class.getSimpleName(); + private static final String TAG = "DialpadFragment"; /** * LinearLayout with getter and setter methods for the translationY property using floats, @@ -260,8 +258,6 @@ public class DialpadFragment extends Fragment private boolean mFirstLaunch = false; private boolean mAnimate = false; - private ComponentName mSmsPackageComponentName; - private static final String PREF_DIGITS_FILLED_BY_INTENT = "pref_digits_filled_by_intent"; private TelephonyManager getTelephonyManager() { @@ -315,8 +311,11 @@ public class DialpadFragment extends Fragment @Override public void onCreate(Bundle state) { + Trace.beginSection(TAG + " onCreate"); super.onCreate(state); - mFirstLaunch = true; + + mFirstLaunch = state == null; + mCurrentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); try { @@ -341,15 +340,21 @@ public class DialpadFragment extends Fragment mCallStateReceiver = new CallStateReceiver(); ((Context) getActivity()).registerReceiver(mCallStateReceiver, callStateIntentFilter); } + Trace.endSection(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { + Trace.beginSection(TAG + " onCreateView"); + Trace.beginSection(TAG + " inflate view"); final View fragmentView = inflater.inflate(R.layout.dialpad_fragment, container, false); + Trace.endSection(); + Trace.beginSection(TAG + " buildLayer"); fragmentView.buildLayer(); + Trace.endSection(); - Resources r = getResources(); + Trace.beginSection(TAG + " setup views"); mDialpadView = (DialpadView) fragmentView.findViewById(R.id.dialpad_view); mDialpadView.setCanDigitsBeEdited(true); @@ -399,7 +404,8 @@ public class DialpadFragment extends Fragment floatingActionButton.setOnClickListener(this); mFloatingActionButtonController = new FloatingActionButtonController(getActivity(), floatingActionButtonContainer, floatingActionButton); - + Trace.endSection(); + Trace.endSection(); return fragmentView; } @@ -582,6 +588,7 @@ public class DialpadFragment extends Fragment @Override public void onStart() { + Trace.beginSection(TAG + " onStart"); super.onStart(); // if the mToneGenerator creation fails, just continue without it. It is // a local audio signal, and is not as important as the dtmf tone itself. @@ -600,10 +607,12 @@ public class DialpadFragment extends Fragment if (total > 50) { Log.i(TAG, "Time for ToneGenerator creation: " + total); } + Trace.endSection(); }; @Override public void onResume() { + Trace.beginSection(TAG + " onResume"); super.onResume(); final DialtactsActivity activity = (DialtactsActivity) getActivity(); @@ -641,8 +650,6 @@ public class DialpadFragment extends Fragment showDialpadChooser(false); } - mFirstLaunch = false; - stopWatch.lap("hnt"); updateDeleteButtonEnabledState(); @@ -651,8 +658,6 @@ public class DialpadFragment extends Fragment stopWatch.stopAndLog(TAG, 50); - mSmsPackageComponentName = DialerUtils.getSmsComponent(activity); - // Populate the overflow menu in onResume instead of onCreate, so that if the SMS activity // is disabled while Dialer is paused, the "Send a text message" option can be correctly // removed when resumed. @@ -661,6 +666,15 @@ public class DialpadFragment extends Fragment mOverflowMenuButton.setOnTouchListener(mOverflowPopupMenu.getDragToOpenListener()); mOverflowMenuButton.setOnClickListener(this); mOverflowMenuButton.setVisibility(isDigitsEmpty() ? View.INVISIBLE : View.VISIBLE); + + if (mFirstLaunch) { + // The onHiddenChanged callback does not get called the first time the fragment is + // attached, so call it ourselves here. + onHiddenChanged(false); + } + + mFirstLaunch = false; + Trace.endSection(); } @Override @@ -860,8 +874,6 @@ public class DialpadFragment extends Fragment @Override public void show() { final Menu menu = getMenu(); - final MenuItem sendMessage = menu.findItem(R.id.menu_send_message); - sendMessage.setVisible(mSmsPackageComponentName != null); boolean enable = !isDigitsEmpty(); for (int i = 0; i < menu.size(); i++) { @@ -988,7 +1000,8 @@ public class DialpadFragment extends Fragment } public void callVoicemail() { - DialerUtils.startActivityWithErrorToast(getActivity(), CallUtil.getVoicemailIntent()); + DialerUtils.startActivityWithErrorToast(getActivity(), PrivilegedCallUtil + .getVoicemailIntent()); hideAndClearDialpad(false); } @@ -1084,7 +1097,7 @@ public class DialpadFragment extends Fragment // Clear the digits just in case. clearDialpad(); } else { - final Intent intent = CallUtil.getCallIntent(number, + final Intent intent = PrivilegedCallUtil.getCallIntent(number, (getActivity() instanceof DialtactsActivity ? ((DialtactsActivity) getActivity()).getCallOrigin() : null)); DialerUtils.startActivityWithErrorToast(getActivity(), intent); @@ -1430,26 +1443,12 @@ public class DialpadFragment extends Fragment @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { - case R.id.menu_add_contact: { - final CharSequence digits = mDigits.getText(); - DialerUtils.startActivityWithErrorToast(getActivity(), - DialtactsActivity.getAddNumberToContactIntent(digits)); - return true; - } case R.id.menu_2s_pause: updateDialString(PAUSE); return true; case R.id.menu_add_wait: updateDialString(WAIT); return true; - case R.id.menu_send_message: { - final CharSequence digits = mDigits.getText(); - final Intent smsIntent = new Intent(Intent.ACTION_SENDTO, - Uri.fromParts(ContactsUtils.SCHEME_SMSTO, digits.toString(), null)); - smsIntent.setComponent(mSmsPackageComponentName); - DialerUtils.startActivityWithErrorToast(getActivity(), smsIntent); - return true; - } default: return false; } @@ -1614,7 +1613,7 @@ public class DialpadFragment extends Fragment } private Intent newFlashIntent() { - final Intent intent = CallUtil.getCallIntent(EMPTY_NUMBER); + final Intent intent = PrivilegedCallUtil.getCallIntent(EMPTY_NUMBER); intent.putExtra(EXTRA_SEND_EMPTY_FLASH, true); return intent; } diff --git a/src/com/android/dialer/dialpad/SmartDialCursorLoader.java b/src/com/android/dialer/dialpad/SmartDialCursorLoader.java index d01776c61..372692eae 100644 --- a/src/com/android/dialer/dialpad/SmartDialCursorLoader.java +++ b/src/com/android/dialer/dialpad/SmartDialCursorLoader.java @@ -18,8 +18,10 @@ package com.android.dialer.dialpad; import android.content.AsyncTaskLoader; import android.content.Context; +import android.content.Loader.ForceLoadContentObserver; import android.database.Cursor; import android.database.MatrixCursor; +import android.net.Uri; import android.util.Log; import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; @@ -44,6 +46,8 @@ public class SmartDialCursorLoader extends AsyncTaskLoader<Cursor> { private String mQuery; private SmartDialNameMatcher mNameMatcher; + private ForceLoadContentObserver mObserver; + public SmartDialCursorLoader(Context context) { super(context); mContext = context; @@ -110,6 +114,12 @@ public class SmartDialCursorLoader extends AsyncTaskLoader<Cursor> { Cursor oldCursor = mCursor; mCursor = cursor; + if (mObserver == null) { + mObserver = new ForceLoadContentObserver(); + mContext.getContentResolver().registerContentObserver( + DialerDatabaseHelper.SMART_DIAL_UPDATED_URI, true, mObserver); + } + if (isStarted()) { /** If the Loader is in a started state, deliver the results to the client. */ super.deliverResult(cursor); @@ -144,6 +154,11 @@ public class SmartDialCursorLoader extends AsyncTaskLoader<Cursor> { /** Ensure the loader has been stopped. */ onStopLoading(); + if (mObserver != null) { + mContext.getContentResolver().unregisterContentObserver(mObserver); + mObserver = null; + } + /** Release all previously saved query results. */ if (mCursor != null) { releaseResources(mCursor); @@ -155,6 +170,11 @@ public class SmartDialCursorLoader extends AsyncTaskLoader<Cursor> { public void onCanceled(Cursor cursor) { super.onCanceled(cursor); + if (mObserver != null) { + mContext.getContentResolver().unregisterContentObserver(mObserver); + mObserver = null; + } + /** The load has been canceled, so we should release the resources associated with 'data'.*/ releaseResources(cursor); } diff --git a/src/com/android/dialer/interactions/PhoneNumberInteraction.java b/src/com/android/dialer/interactions/PhoneNumberInteraction.java index de217ce7b..78c468bbc 100644 --- a/src/com/android/dialer/interactions/PhoneNumberInteraction.java +++ b/src/com/android/dialer/interactions/PhoneNumberInteraction.java @@ -45,7 +45,6 @@ import android.widget.CheckBox; import android.widget.ListAdapter; import android.widget.TextView; -import com.android.contacts.common.CallUtil; import com.android.contacts.common.Collapser; import com.android.contacts.common.Collapser.Collapsible; import com.android.contacts.common.MoreContactUtils; @@ -53,6 +52,7 @@ import com.android.contacts.common.activity.TransactionSafeActivity; import com.android.contacts.common.util.ContactDisplayUtils; import com.android.dialer.R; import com.android.dialer.contact.ContactUpdateService; +import com.android.dialer.util.PrivilegedCallUtil; import com.android.dialer.util.DialerUtils; import com.google.common.annotations.VisibleForTesting; @@ -322,7 +322,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> { Intent.ACTION_SENDTO, Uri.fromParts("sms", phoneNumber, null)); break; default: - intent = CallUtil.getCallIntent(phoneNumber, callOrigin); + intent = PrivilegedCallUtil.getCallIntent(phoneNumber, callOrigin); break; } DialerUtils.startActivityWithErrorToast(context, intent); diff --git a/src/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java b/src/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java index 6d74cd0f7..960a31bc3 100644 --- a/src/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java +++ b/src/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java @@ -30,6 +30,8 @@ import android.text.TextUtils; /** * This broadcast receiver is used to listen to outgoing calls and undemote formerly demoted * contacts if a phone call is made to a phone number belonging to that contact. + * + * NOTE This doesn't work for corp contacts. */ public class UndemoteOutgoingCallReceiver extends BroadcastReceiver { diff --git a/src/com/android/dialer/list/AllContactsFragment.java b/src/com/android/dialer/list/AllContactsFragment.java index deabb80b9..94efc4869 100644 --- a/src/com/android/dialer/list/AllContactsFragment.java +++ b/src/com/android/dialer/list/AllContactsFragment.java @@ -18,6 +18,7 @@ package com.android.dialer.list; import android.database.Cursor; import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.QuickContact; import android.view.LayoutInflater; import android.view.View; @@ -83,7 +84,8 @@ public class AllContactsFragment extends ContactEntryListFragment<ContactEntryLi public void onItemClick(AdapterView<?> parent, View view, int position, long id) { final Uri uri = (Uri) view.getTag(); if (uri != null) { - QuickContact.showQuickContact(getActivity(), view, uri, QuickContact.MODE_LARGE, null); + QuickContact.showQuickContact(getContext(), view, uri, null, + Phone.CONTENT_ITEM_TYPE); } } diff --git a/src/com/android/dialer/list/DialerPhoneNumberListAdapter.java b/src/com/android/dialer/list/DialerPhoneNumberListAdapter.java index da4f4a4c9..82daab5aa 100644 --- a/src/com/android/dialer/list/DialerPhoneNumberListAdapter.java +++ b/src/com/android/dialer/list/DialerPhoneNumberListAdapter.java @@ -27,10 +27,12 @@ public class DialerPhoneNumberListAdapter extends PhoneNumberListAdapter { public final static int SHORTCUT_INVALID = -1; public final static int SHORTCUT_DIRECT_CALL = 0; - public final static int SHORTCUT_ADD_NUMBER_TO_CONTACTS = 1; - public final static int SHORTCUT_MAKE_VIDEO_CALL = 2; + public final static int SHORTCUT_CREATE_NEW_CONTACT = 1; + public final static int SHORTCUT_ADD_TO_EXISTING_CONTACT = 2; + public final static int SHORTCUT_SEND_SMS_MESSAGE = 3; + public final static int SHORTCUT_MAKE_VIDEO_CALL = 4; - public final static int SHORTCUT_COUNT = 3; + public final static int SHORTCUT_COUNT = 5; private final boolean[] mShortcutEnabled = new boolean[SHORTCUT_COUNT]; @@ -142,10 +144,18 @@ public class DialerPhoneNumberListAdapter extends PhoneNumberListAdapter { text = resources.getString(R.string.search_shortcut_call_number, number); drawableId = R.drawable.ic_search_phone; break; - case SHORTCUT_ADD_NUMBER_TO_CONTACTS: - text = resources.getString(R.string.search_shortcut_add_to_contacts); + case SHORTCUT_CREATE_NEW_CONTACT: + text = resources.getString(R.string.search_shortcut_create_new_contact); drawableId = R.drawable.ic_search_add_contact; break; + case SHORTCUT_ADD_TO_EXISTING_CONTACT: + text = resources.getString(R.string.search_shortcut_add_to_existing_contact); + drawableId = R.drawable.ic_person_24dp; + break; + case SHORTCUT_SEND_SMS_MESSAGE: + text = resources.getString(R.string.search_shortcut_send_sms_message); + drawableId = R.drawable.ic_textsms_24dp; + break; case SHORTCUT_MAKE_VIDEO_CALL: text = resources.getString(R.string.search_shortcut_make_video_call); drawableId = R.drawable.ic_videocam; @@ -153,7 +163,7 @@ public class DialerPhoneNumberListAdapter extends PhoneNumberListAdapter { default: throw new IllegalArgumentException("Invalid shortcut type"); } - v.setDrawableResource(R.drawable.search_shortcut_background, drawableId); + v.setDrawableResource(drawableId); v.setDisplayName(text); v.setPhotoPosition(super.getPhotoPosition()); v.setAdjustSelectionBoundsEnabled(false); diff --git a/src/com/android/dialer/list/ListsFragment.java b/src/com/android/dialer/list/ListsFragment.java index af82e4075..f22a5d19c 100644 --- a/src/com/android/dialer/list/ListsFragment.java +++ b/src/com/android/dialer/list/ListsFragment.java @@ -8,6 +8,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.os.Bundle; +import android.os.Trace; import android.support.v13.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager.OnPageChangeListener; @@ -23,16 +24,11 @@ import com.android.contacts.common.list.ViewPagerTabs; import com.android.contacts.commonbind.analytics.AnalyticsUtil; import com.android.dialer.DialtactsActivity; import com.android.dialer.R; -import com.android.dialer.calllog.CallLogAdapter; import com.android.dialer.calllog.CallLogFragment; -import com.android.dialer.calllog.CallLogQuery; import com.android.dialer.calllog.CallLogQueryHandler; import com.android.dialer.calllog.ContactInfoHelper; -import com.android.dialer.list.ShortcutCardsAdapter.SwipeableShortcutCard; import com.android.dialer.util.DialerUtils; import com.android.dialer.widget.ActionBarController; -import com.android.dialer.widget.OverlappingPaneLayout; -import com.android.dialer.widget.OverlappingPaneLayout.PanelSlideCallbacks; import com.android.dialerbind.ObjectFactory; import java.util.ArrayList; @@ -42,11 +38,10 @@ import java.util.ArrayList; * * Contains a ViewPager that contains various contact lists like the Speed Dial list and the * All Contacts list. This will also eventually contain the logic that allows sliding the - * ViewPager containing the lists up above the shortcut cards and pin it against the top of the + * ViewPager containing the lists up above the search bar and pin it against the top of the * screen. */ -public class ListsFragment extends Fragment implements CallLogQueryHandler.Listener, - CallLogAdapter.CallFetcher, ViewPager.OnPageChangeListener { +public class ListsFragment extends Fragment implements ViewPager.OnPageChangeListener { private static final boolean DEBUG = DialtactsActivity.DEBUG; private static final String TAG = "ListsFragment"; @@ -64,9 +59,6 @@ public class ListsFragment extends Fragment implements CallLogQueryHandler.Liste private static final String KEY_LAST_DISMISSED_CALL_SHORTCUT_DATE = "key_last_dismissed_call_shortcut_date"; - public static final float REMOVE_VIEW_SHOWN_ALPHA = 0.5f; - public static final float REMOVE_VIEW_HIDDEN_ALPHA = 1; - public interface HostInterface { public void showCallHistory(); public ActionBarController getActionBarController(); @@ -76,7 +68,6 @@ public class ListsFragment extends Fragment implements CallLogQueryHandler.Liste private ViewPager mViewPager; private ViewPagerTabs mViewPagerTabs; private ViewPagerAdapter mViewPagerAdapter; - private ListView mShortcutCardsListView; private RemoveView mRemoveView; private View mRemoveViewContent; private SpeedDialFragment mSpeedDialFragment; @@ -86,13 +77,7 @@ public class ListsFragment extends Fragment implements CallLogQueryHandler.Liste new ArrayList<OnPageChangeListener>(); private String[] mTabTitles; - - private ShortcutCardsAdapter mMergedAdapter; - private CallLogAdapter mCallLogAdapter; - private CallLogQueryHandler mCallLogQueryHandler; - private OverlappingPaneLayout mOverlappingPaneLayout; - - private boolean mIsPanelOpen = true; + private int[] mTabIcons; /** * Call shortcuts older than this date (persisted in shared preferences) will not show up in @@ -105,80 +90,6 @@ public class ListsFragment extends Fragment implements CallLogQueryHandler.Liste */ private long mCurrentCallShortcutDate = 0; - private PanelSlideCallbacks mPanelSlideCallbacks = new PanelSlideCallbacks() { - @Override - public void onPanelSlide(View panel, float slideOffset) { - // For every 1 percent that the panel is slid upwards, clip 1 percent off the top - // edge of the shortcut card, to achieve the animated effect of the shortcut card - // being pushed out of view when the panel is slid upwards. slideOffset is 1 when - // the shortcut card is fully exposed, and 0 when completely hidden. - float ratioCardHidden = (1 - slideOffset); - if (mShortcutCardsListView.getChildCount() > 0) { - final SwipeableShortcutCard v = - (SwipeableShortcutCard) mShortcutCardsListView.getChildAt(0); - v.clipCard(ratioCardHidden); - } - - if (mActionBar != null) { - // Amount of available space that is not being hidden by the bottom pane - final int topPaneHeight = (int) (slideOffset * mShortcutCardsListView.getHeight()); - - final int availableActionBarHeight = - Math.min(mActionBar.getHeight(), topPaneHeight); - final ActionBarController controller = - ((HostInterface) getActivity()).getActionBarController(); - controller.setHideOffset(mActionBar.getHeight() - availableActionBarHeight); - - if (!mActionBar.isShowing()) { - mActionBar.show(); - } - } - } - - @Override - public void onPanelOpened(View panel) { - if (DEBUG) { - Log.d(TAG, "onPanelOpened"); - } - mIsPanelOpen = true; - } - - @Override - public void onPanelClosed(View panel) { - if (DEBUG) { - Log.d(TAG, "onPanelClosed"); - } - mIsPanelOpen = false; - } - - @Override - public void onPanelFlingReachesEdge(int velocityY) { - if (getCurrentListView() != null) { - getCurrentListView().fling(velocityY); - } - } - - @Override - public boolean isScrollableChildUnscrolled() { - final AbsListView listView = getCurrentListView(); - return listView != null && (listView.getChildCount() == 0 - || listView.getChildAt(0).getTop() == listView.getPaddingTop()); - } - }; - - private AbsListView getCurrentListView() { - final int position = mViewPager.getCurrentItem(); - switch (getRtlPosition(position)) { - case TAB_INDEX_SPEED_DIAL: - return mSpeedDialFragment == null ? null : mSpeedDialFragment.getListView(); - case TAB_INDEX_RECENTS: - return mRecentsFragment == null ? null : mRecentsFragment.getListView(); - case TAB_INDEX_ALL_CONTACTS: - return mAllContactsFragment == null ? null : mAllContactsFragment.getListView(); - } - throw new IllegalStateException("No fragment at position " + position); - } - public class ViewPagerAdapter extends FragmentPagerAdapter { public ViewPagerAdapter(FragmentManager fm) { super(fm); @@ -198,7 +109,6 @@ public class ListsFragment extends Fragment implements CallLogQueryHandler.Liste case TAB_INDEX_RECENTS: mRecentsFragment = new CallLogFragment(CallLogQueryHandler.CALL_TYPE_ALL, MAX_RECENTS_ENTRIES, System.currentTimeMillis() - OLDEST_RECENTS_DATE); - mRecentsFragment.setHasFooterView(true); return mRecentsFragment; case TAB_INDEX_ALL_CONTACTS: mAllContactsFragment = new AllContactsFragment(); @@ -237,49 +147,38 @@ public class ListsFragment extends Fragment implements CallLogQueryHandler.Liste @Override public void onCreate(Bundle savedInstanceState) { + Trace.beginSection(TAG + " onCreate"); super.onCreate(savedInstanceState); - mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), - this, 1); + Trace.beginSection(TAG + " getCurrentCountryIso"); final String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); - mCallLogAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this, - new ContactInfoHelper(getActivity(), currentCountryIso), null, null, false); + Trace.endSection(); - mMergedAdapter = new ShortcutCardsAdapter(getActivity(), this, mCallLogAdapter); + Trace.endSection(); } @Override public void onResume() { + Trace.beginSection(TAG + " onResume"); super.onResume(); final SharedPreferences prefs = getActivity().getSharedPreferences( DialtactsActivity.SHARED_PREFS_NAME, Context.MODE_PRIVATE); mLastCallShortcutDate = prefs.getLong(KEY_LAST_DISMISSED_CALL_SHORTCUT_DATE, 0); mActionBar = getActivity().getActionBar(); - fetchCalls(); - mCallLogAdapter.setLoading(true); if (getUserVisibleHint()) { sendScreenViewForPosition(mViewPager.getCurrentItem()); } - } - - @Override - public void onPause() { - // Wipe the cache to refresh the call shortcut item. This is not that expensive because - // it only contains one item. - mCallLogAdapter.invalidateCache(); - super.onPause(); - } - - @Override - public void onDestroy() { - mCallLogAdapter.stopRequestProcessing(); - super.onDestroy(); + Trace.endSection(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Trace.beginSection(TAG + " onCreateView"); + Trace.beginSection(TAG + " inflate view"); final View parentView = inflater.inflate(R.layout.lists_fragment, container, false); + Trace.endSection(); + Trace.beginSection(TAG + " setup views"); mViewPager = (ViewPager) parentView.findViewById(R.id.lists_pager); mViewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager()); mViewPager.setAdapter(mViewPagerAdapter); @@ -292,61 +191,24 @@ public class ListsFragment extends Fragment implements CallLogQueryHandler.Liste mTabTitles[TAB_INDEX_RECENTS] = getResources().getString(R.string.tab_recents); mTabTitles[TAB_INDEX_ALL_CONTACTS] = getResources().getString(R.string.tab_all_contacts); + mTabIcons = new int[TAB_INDEX_COUNT]; + mTabIcons[TAB_INDEX_SPEED_DIAL] = R.drawable.tab_speed_dial; + mTabIcons[TAB_INDEX_RECENTS] = R.drawable.tab_recents; + mTabIcons[TAB_INDEX_ALL_CONTACTS] = R.drawable.tab_contacts; + mViewPagerTabs = (ViewPagerTabs) parentView.findViewById(R.id.lists_pager_header); + mViewPagerTabs.setTabIcons(mTabIcons); mViewPagerTabs.setViewPager(mViewPager); addOnPageChangeListener(mViewPagerTabs); - mShortcutCardsListView = (ListView) parentView.findViewById(R.id.shortcut_card_list); - mShortcutCardsListView.setAdapter(mMergedAdapter); - mRemoveView = (RemoveView) parentView.findViewById(R.id.remove_view); mRemoveViewContent = parentView.findViewById(R.id.remove_view_content); - setupPaneLayout((OverlappingPaneLayout) parentView); - mOverlappingPaneLayout = (OverlappingPaneLayout) parentView; - + Trace.endSection(); + Trace.endSection(); return parentView; } - @Override - public void onVoicemailStatusFetched(Cursor statusCursor) { - // no-op - } - - @Override - public boolean onCallsFetched(Cursor cursor) { - mCallLogAdapter.setLoading(false); - - // Save the date of the most recent call log item - if (cursor != null && cursor.moveToFirst()) { - mCurrentCallShortcutDate = cursor.getLong(CallLogQuery.DATE); - } - - mCallLogAdapter.changeCursor(cursor); - mMergedAdapter.notifyDataSetChanged(); - - // Refresh the overlapping pane to ensure that any changes in the shortcut card height - // are appropriately reflected in the overlap position. - mOverlappingPaneLayout.refresh(); - - // Return true; took ownership of cursor - return true; - } - - @Override - public void fetchCalls() { - mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL, mLastCallShortcutDate); - } - - public void dismissShortcut(View view) { - mLastCallShortcutDate = mCurrentCallShortcutDate; - final SharedPreferences prefs = view.getContext().getSharedPreferences( - DialtactsActivity.SHARED_PREFS_NAME, Context.MODE_PRIVATE); - prefs.edit().putLong(KEY_LAST_DISMISSED_CALL_SHORTCUT_DATE, mLastCallShortcutDate) - .apply(); - fetchCalls(); - } - public void addOnPageChangeListener(OnPageChangeListener onPageChangeListener) { if (!mOnPageChangeListeners.contains(onPageChangeListener)) { mOnPageChangeListeners.add(onPageChangeListener); @@ -383,36 +245,11 @@ public class ListsFragment extends Fragment implements CallLogQueryHandler.Liste mRemoveViewContent.setVisibility(show ? View.VISIBLE : View.GONE); mRemoveView.setAlpha(show ? 0 : 1); mRemoveView.animate().alpha(show ? 1 : 0).start(); - - if (mShortcutCardsListView.getChildCount() > 0) { - View v = mShortcutCardsListView.getChildAt(0); - v.animate().withLayer() - .alpha(show ? REMOVE_VIEW_SHOWN_ALPHA : REMOVE_VIEW_HIDDEN_ALPHA) - .start(); - } } public boolean shouldShowActionBar() { - return mIsPanelOpen && mActionBar != null; - } - - public boolean isPaneOpen() { - return mIsPanelOpen; - } - - private void setupPaneLayout(OverlappingPaneLayout paneLayout) { - // TODO: Remove the notion of a capturable view. The entire view be slideable, once - // the framework better supports nested scrolling. - paneLayout.setCapturableView(mViewPagerTabs); - paneLayout.openPane(); - paneLayout.setPanelSlideCallbacks(mPanelSlideCallbacks); - paneLayout.setIntermediatePinnedOffset( - ((HostInterface) getActivity()).getActionBarController().getActionBarHeight()); - - LayoutTransition transition = paneLayout.getLayoutTransition(); - // Turns on animations for all types of layout changes so that they occur for - // height changes. - transition.enableTransitionType(LayoutTransition.CHANGING); + // TODO: Update this based on scroll state. + return mActionBar != null; } public SpeedDialFragment getSpeedDialFragment() { diff --git a/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java b/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java index 5f8877616..05780c66a 100644 --- a/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java +++ b/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java @@ -17,6 +17,7 @@ package com.android.dialer.list; import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.QuickContact; import android.util.AttributeSet; import android.view.View; @@ -63,7 +64,7 @@ public class PhoneFavoriteSquareTileView extends PhoneFavoriteTileView { private void launchQuickContact() { QuickContact.showQuickContact(getContext(), PhoneFavoriteSquareTileView.this, - getLookupUri(), QuickContact.MODE_LARGE, null); + getLookupUri(), null, Phone.CONTENT_ITEM_TYPE); } @Override diff --git a/src/com/android/dialer/list/RegularSearchListAdapter.java b/src/com/android/dialer/list/RegularSearchListAdapter.java index f1f2ae039..3c55bc012 100644 --- a/src/com/android/dialer/list/RegularSearchListAdapter.java +++ b/src/com/android/dialer/list/RegularSearchListAdapter.java @@ -18,12 +18,10 @@ package com.android.dialer.list; import android.content.Context; import android.database.Cursor; import android.net.Uri; -import android.provider.ContactsContract; import android.text.TextUtils; import com.android.contacts.common.CallUtil; import com.android.contacts.common.list.DirectoryPartition; -import com.android.contacts.common.list.PhoneNumberListAdapter; import com.android.dialer.calllog.ContactInfo; import com.android.dialer.service.CachedNumberLookupService; import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo; @@ -35,6 +33,8 @@ public class RegularSearchListAdapter extends DialerPhoneNumberListAdapter { public RegularSearchListAdapter(Context context) { super(context); + setShortcutEnabled(SHORTCUT_CREATE_NEW_CONTACT, false); + setShortcutEnabled(SHORTCUT_ADD_TO_EXISTING_CONTACT, false); } public CachedContactInfo getContactInfo( @@ -71,10 +71,7 @@ public class RegularSearchListAdapter extends DialerPhoneNumberListAdapter { final boolean showNumberShortcuts = !TextUtils.isEmpty(getFormattedQueryString()); boolean changed = false; changed |= setShortcutEnabled(SHORTCUT_DIRECT_CALL, showNumberShortcuts); - // Either one of the add contacts options should be enabled. If the user entered - // a dialable number, then clicking add to contact should add it as a number. - // Otherwise, it should add it to a new contact as a name. - changed |= setShortcutEnabled(SHORTCUT_ADD_NUMBER_TO_CONTACTS, showNumberShortcuts); + changed |= setShortcutEnabled(SHORTCUT_SEND_SMS_MESSAGE, showNumberShortcuts); changed |= setShortcutEnabled(SHORTCUT_MAKE_VIDEO_CALL, showNumberShortcuts && CallUtil.isVideoEnabled(getContext())); if (changed) { diff --git a/src/com/android/dialer/list/SearchFragment.java b/src/com/android/dialer/list/SearchFragment.java index 1c88c4e81..73127a294 100644 --- a/src/com/android/dialer/list/SearchFragment.java +++ b/src/com/android/dialer/list/SearchFragment.java @@ -15,11 +15,16 @@ */ package com.android.dialer.list; +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.AnimatorListenerAdapter; import android.app.Activity; import android.app.DialogFragment; import android.content.Intent; import android.content.res.Resources; +import android.net.Uri; import android.os.Bundle; +import android.provider.ContactsContract; import android.text.TextUtils; import android.util.Log; import android.view.View; @@ -41,7 +46,8 @@ import com.android.dialer.util.DialerUtils; import com.android.phone.common.animation.AnimUtils; public class SearchFragment extends PhoneNumberPickerFragment { - private static final String TAG = "SearchFragment"; + private static final String TAG = SearchFragment.class.getSimpleName(); + private static final String SMS_URI_PREFIX = "sms:"; private OnListFragmentScrolledListener mActivityScrollListener; @@ -128,6 +134,26 @@ public class SearchFragment extends PhoneNumberPickerFragment { } @Override + public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { + Animator animator = null; + if (nextAnim != 0) { + animator = AnimatorInflater.loadAnimator(getActivity(), nextAnim); + } + if (animator != null) { + final View view = getView(); + final int oldLayerType = view.getLayerType(); + view.setLayerType(View.LAYER_TYPE_HARDWARE, null); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setLayerType(oldLayerType, null); + } + }); + } + return animator; + } + + @Override protected void setSearchMode(boolean flag) { super.setSearchMode(flag); // This hides the "All contacts with phone numbers" header in the search fragment @@ -180,31 +206,48 @@ public class SearchFragment extends PhoneNumberPickerFragment { final DialerPhoneNumberListAdapter adapter = (DialerPhoneNumberListAdapter) getAdapter(); final int shortcutType = adapter.getShortcutTypeFromPosition(position); final OnPhoneNumberPickerActionListener listener; - final String phoneNumber = TextUtils.isEmpty(mAddToContactNumber) ? - adapter.getQueryString() : mAddToContactNumber; + final Intent intent; + final String number; + + Log.i(TAG, "onItemClick: shortcutType=" + shortcutType); - boolean ret = checkForProhibitedPhoneNumber(phoneNumber); switch (shortcutType) { case DialerPhoneNumberListAdapter.SHORTCUT_INVALID: super.onItemClick(position, id); break; case DialerPhoneNumberListAdapter.SHORTCUT_DIRECT_CALL: + number = adapter.getQueryString(); listener = getOnPhoneNumberPickerListener(); - if (listener != null && !ret) { - listener.onCallNumberDirectly(phoneNumber); + if (listener != null && !checkForProhibitedPhoneNumber(number)) { + listener.onCallNumberDirectly(number); } break; - case DialerPhoneNumberListAdapter.SHORTCUT_ADD_NUMBER_TO_CONTACTS: - final String number = TextUtils.isEmpty(mAddToContactNumber) ? + case DialerPhoneNumberListAdapter.SHORTCUT_CREATE_NEW_CONTACT: + number = TextUtils.isEmpty(mAddToContactNumber) ? adapter.getFormattedQueryString() : mAddToContactNumber; - final Intent intent = DialtactsActivity.getAddNumberToContactIntent(number); + intent = new Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI); + intent.putExtra(ContactsContract.Intents.Insert.PHONE, number); + intent.putExtra(ContactsContract.Intents.Insert.PHONE_TYPE, + ContactsContract.CommonDataKinds.Phone.TYPE_MAIN); + DialerUtils.startActivityWithErrorToast(getActivity(), intent); + break; + case DialerPhoneNumberListAdapter.SHORTCUT_ADD_TO_EXISTING_CONTACT: + number = TextUtils.isEmpty(mAddToContactNumber) ? + adapter.getFormattedQueryString() : mAddToContactNumber; + intent = DialtactsActivity.getAddNumberToContactIntent(number); DialerUtils.startActivityWithErrorToast(getActivity(), intent, R.string.add_contact_not_available); break; + case DialerPhoneNumberListAdapter.SHORTCUT_SEND_SMS_MESSAGE: + intent = new Intent( + Intent.ACTION_VIEW, Uri.parse(SMS_URI_PREFIX + getQueryString())); + DialerUtils.startActivityWithErrorToast(getActivity(), intent); + break; case DialerPhoneNumberListAdapter.SHORTCUT_MAKE_VIDEO_CALL: + number = adapter.getQueryString(); listener = getOnPhoneNumberPickerListener(); - if (listener != null && !ret) { - listener.onCallNumberDirectly(phoneNumber, true /* isVideoCall */); + if (listener != null && !checkForProhibitedPhoneNumber(number)) { + listener.onCallNumberDirectly(number, true /* isVideoCall */); } break; } diff --git a/src/com/android/dialer/list/ShortcutCardsAdapter.java b/src/com/android/dialer/list/ShortcutCardsAdapter.java deleted file mode 100644 index 6410eabf6..000000000 --- a/src/com/android/dialer/list/ShortcutCardsAdapter.java +++ /dev/null @@ -1,393 +0,0 @@ -/* - * 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.Cursor; -import android.database.DataSetObserver; -import android.graphics.Rect; -import android.text.TextUtils; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.dialer.R; -import com.android.dialer.calllog.CallLogAdapter; -import com.android.dialer.calllog.CallLogNotificationsHelper; -import com.android.dialer.calllog.CallLogQueryHandler; -import com.android.dialer.list.SwipeHelper.OnItemGestureListener; -import com.android.dialer.list.SwipeHelper.SwipeHelperCallback; - -/** - * An adapter that displays call shortcuts from {@link com.android.dialer.calllog.CallLogAdapter} - * in the form of cards. - */ -public class ShortcutCardsAdapter extends BaseAdapter { - - private class CustomDataSetObserver extends DataSetObserver { - @Override - public void onChanged() { - notifyDataSetChanged(); - } - } - - private static final String TAG = ShortcutCardsAdapter.class.getSimpleName(); - private static final float CLIP_CARD_BARELY_HIDDEN_RATIO = 0.001f; - private static final float CLIP_CARD_MOSTLY_HIDDEN_RATIO = 0.9f; - // Fade out 5x faster than the hidden ratio. - private static final float CLIP_CARD_OPACITY_RATIO = 5f; - - private final CallLogAdapter mCallLogAdapter; - - private final ListsFragment mFragment; - - private final int mCallLogMarginHorizontal; - private final int mCallLogMarginTop; - private final int mCallLogMarginBottom; - private final int mCallLogPaddingStart; - private final int mCallLogPaddingTop; - private final int mCallLogPaddingBottom; - private final int mCardMaxHorizontalClip; - private final int mShortCardBackgroundColor; - - private final Context mContext; - - private final DataSetObserver mObserver; - - private final CallLogQueryHandler mCallLogQueryHandler; - - private final OnItemGestureListener mCallLogOnItemSwipeListener = - new OnItemGestureListener() { - @Override - public void onSwipe(View view) { - mCallLogQueryHandler.markNewCallsAsOld(); - mCallLogQueryHandler.markNewVoicemailsAsOld(); - CallLogNotificationsHelper.removeMissedCallNotifications(mContext); - CallLogNotificationsHelper.updateVoicemailNotifications(mContext); - mFragment.dismissShortcut(view); - } - - @Override - public void onTouch() {} - - @Override - public boolean isSwipeEnabled() { - return true; - } - }; - - private final CallLogQueryHandler.Listener mCallLogQueryHandlerListener = - new CallLogQueryHandler.Listener() { - @Override - public void onVoicemailStatusFetched(Cursor statusCursor) {} - - @Override - public boolean onCallsFetched(Cursor combinedCursor) { - mCallLogAdapter.invalidateCache(); - mCallLogAdapter.changeCursor(combinedCursor); - mCallLogAdapter.notifyDataSetChanged(); - // Return true; took ownership of cursor - return true; - } - }; - - public ShortcutCardsAdapter(Context context, - ListsFragment fragment, - CallLogAdapter callLogAdapter) { - final Resources resources = context.getResources(); - mContext = context; - mFragment = fragment; - mCardMaxHorizontalClip = - resources.getDimensionPixelSize(R.dimen.recent_call_log_item_horizontal_clip_limit); - mCallLogMarginHorizontal = - resources.getDimensionPixelSize(R.dimen.recent_call_log_item_margin_horizontal); - mCallLogMarginTop = - resources.getDimensionPixelSize(R.dimen.recent_call_log_item_margin_top); - mCallLogMarginBottom = - resources.getDimensionPixelSize(R.dimen.recent_call_log_item_margin_bottom); - mCallLogPaddingStart = - resources.getDimensionPixelSize(R.dimen.recent_call_log_item_padding_start); - mCallLogPaddingTop = - resources.getDimensionPixelSize(R.dimen.recent_call_log_item_padding_top); - mCallLogPaddingBottom = - resources.getDimensionPixelSize(R.dimen.recent_call_log_item_padding_bottom); - mShortCardBackgroundColor = resources.getColor(R.color.call_log_expanded_background_color); - - - mCallLogAdapter = callLogAdapter; - mObserver = new CustomDataSetObserver(); - mCallLogAdapter.registerDataSetObserver(mObserver); - mCallLogQueryHandler = new CallLogQueryHandler(mContext.getContentResolver(), - mCallLogQueryHandlerListener); - } - - /** - * Determines the number of items in the adapter. - * mCallLogAdapter contains the item for the most recent caller. - * mContactTileAdapter contains the starred contacts. - * The +1 is to account for the presence of the favorites menu. - * - * @return Number of items in the adapter. - */ - @Override - public int getCount() { - return mCallLogAdapter.getCount(); - } - - @Override - public Object getItem(int position) { - return mCallLogAdapter.getItem(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public boolean hasStableIds() { - return true; - } - - /** - * Determine the number of view types present. - */ - @Override - public int getViewTypeCount() { - return mCallLogAdapter.getViewTypeCount(); - } - - @Override - public int getItemViewType(int position) { - return mCallLogAdapter.getItemViewType(position); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - final SwipeableShortcutCard wrapper; - if (convertView == null) { - wrapper = new SwipeableShortcutCard(mContext); - wrapper.setOnItemSwipeListener(mCallLogOnItemSwipeListener); - } else { - wrapper = (SwipeableShortcutCard) convertView; - } - - // Special case wrapper view for the most recent call log item. This allows - // us to create a card-like effect for the more recent call log item in - // the PhoneFavoriteMergedAdapter, but keep the original look of the item in - // the CallLogAdapter. - final View view = mCallLogAdapter.getView(position, convertView == null ? - null : wrapper.getChildAt(0), parent - ); - wrapper.removeAllViews(); - wrapper.prepareChildView(view); - wrapper.addView(view); - wrapper.setVisibility(View.VISIBLE); - return wrapper; - } - - @Override - public boolean areAllItemsEnabled() { - return mCallLogAdapter.areAllItemsEnabled(); - } - - @Override - public boolean isEnabled(int position) { - return mCallLogAdapter.isEnabled(position); - } - - /** - * The swipeable call log row. - */ - class SwipeableShortcutCard extends FrameLayout implements SwipeHelperCallback { - private SwipeHelper mSwipeHelper; - private OnItemGestureListener mOnItemSwipeListener; - - private float mPreviousTranslationZ = 0; - private Rect mClipRect = new Rect(); - - public SwipeableShortcutCard(Context context) { - super(context); - final float densityScale = getResources().getDisplayMetrics().density; - final float pagingTouchSlop = ViewConfiguration.get(context) - .getScaledPagingTouchSlop(); - mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, - densityScale, pagingTouchSlop); - } - - private void prepareChildView(View view) { - // Override CallLogAdapter's accessibility behavior; don't expand the shortcut card. - view.setAccessibilityDelegate(null); - view.setBackgroundResource(R.drawable.rounded_corner_bg); - - final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.WRAP_CONTENT); - params.setMargins( - mCallLogMarginHorizontal, - mCallLogMarginTop, - mCallLogMarginHorizontal, - mCallLogMarginBottom); - view.setLayoutParams(params); - - LinearLayout actionView = - (LinearLayout) view.findViewById(R.id.primary_action_view); - actionView.setPaddingRelative( - mCallLogPaddingStart, - mCallLogPaddingTop, - actionView.getPaddingEnd(), - mCallLogPaddingBottom); - - // TODO: Set content description including type/location and time information. - TextView nameView = (TextView) actionView.findViewById(R.id.name); - - actionView.setContentDescription( - TextUtils.expandTemplate( - getResources().getString(R.string.description_call_back_action), - nameView.getText())); - - mPreviousTranslationZ = getResources().getDimensionPixelSize( - R.dimen.recent_call_log_item_translation_z); - view.setTranslationZ(mPreviousTranslationZ); - - final ViewGroup callLogItem = (ViewGroup) view.findViewById(R.id.call_log_list_item); - // Reset the internal call log item view if it is being recycled - callLogItem.setTranslationX(0); - callLogItem.setTranslationY(0); - callLogItem.setAlpha(1); - callLogItem.setClipBounds(null); - setChildrenOpacity(callLogItem, 1.0f); - - callLogItem.findViewById(R.id.call_log_row) - .setBackgroundColor(mShortCardBackgroundColor); - - callLogItem.findViewById(R.id.call_indicator_icon).setVisibility(View.VISIBLE); - } - - @Override - public View getChildAtPosition(MotionEvent ev) { - return getChildCount() > 0 ? getChildAt(0) : null; - } - - @Override - public View getChildContentView(View v) { - return v.findViewById(R.id.call_log_list_item); - } - - @Override - public void onScroll() {} - - @Override - public boolean canChildBeDismissed(View v) { - return true; - } - - @Override - public void onBeginDrag(View v) { - // We do this so the underlying ScrollView knows that it won't get - // the chance to intercept events anymore - requestDisallowInterceptTouchEvent(true); - } - - @Override - public void onChildDismissed(View v) { - if (v != null && mOnItemSwipeListener != null) { - mOnItemSwipeListener.onSwipe(v); - } - } - - @Override - public void onDragCancelled(View v) {} - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - if (mSwipeHelper != null) { - return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev); - } else { - return super.onInterceptTouchEvent(ev); - } - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (mSwipeHelper != null) { - return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev); - } else { - return super.onTouchEvent(ev); - } - } - - public void setOnItemSwipeListener(OnItemGestureListener listener) { - mOnItemSwipeListener = listener; - } - - /** - * Clips the card by a specified amount. - * - * @param ratioHidden A float indicating how much of each edge of the card should be - * clipped. If 0, the entire card is displayed. If 0.5f, each edge is hidden - * entirely, thus obscuring the entire card. - */ - public void clipCard(float ratioHidden) { - final View viewToClip = getChildAt(0); - if (viewToClip == null) { - return; - } - int width = viewToClip.getWidth(); - int height = viewToClip.getHeight(); - - if (ratioHidden <= CLIP_CARD_BARELY_HIDDEN_RATIO) { - viewToClip.setTranslationZ(mPreviousTranslationZ); - } else if (viewToClip.getTranslationZ() != 0){ - mPreviousTranslationZ = viewToClip.getTranslationZ(); - viewToClip.setTranslationZ(0); - } - - if (ratioHidden > CLIP_CARD_MOSTLY_HIDDEN_RATIO) { - mClipRect.set(0, 0 , 0, 0); - setVisibility(View.INVISIBLE); - } else { - setVisibility(View.VISIBLE); - int newTop = (int) (ratioHidden * height); - mClipRect.set(0, newTop, width, height); - - // Since the pane will be overlapping with the action bar, apply a vertical offset - // to top align the clipped card in the viewable area; - viewToClip.setTranslationY(-newTop); - } - viewToClip.setClipBounds(mClipRect); - - // If the view has any children, fade them out of view. - final ViewGroup viewGroup = (ViewGroup) viewToClip; - setChildrenOpacity( - viewGroup, Math.max(0, 1 - (CLIP_CARD_OPACITY_RATIO * ratioHidden))); - } - - private void setChildrenOpacity(ViewGroup viewGroup, float alpha) { - final int count = viewGroup.getChildCount(); - for (int i = 0; i < count; i++) { - viewGroup.getChildAt(i).setAlpha(alpha); - } - } - } -} diff --git a/src/com/android/dialer/list/SmartDialNumberListAdapter.java b/src/com/android/dialer/list/SmartDialNumberListAdapter.java index 04c0d620d..fe27a25ab 100644 --- a/src/com/android/dialer/list/SmartDialNumberListAdapter.java +++ b/src/com/android/dialer/list/SmartDialNumberListAdapter.java @@ -20,15 +20,12 @@ import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds.Callable; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import android.util.Log; import com.android.contacts.common.CallUtil; import com.android.contacts.common.list.ContactListItemView; -import com.android.contacts.common.list.PhoneNumberListAdapter; -import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; import com.android.dialer.dialpad.SmartDialCursorLoader; import com.android.dialer.dialpad.SmartDialNameMatcher; import com.android.dialer.dialpad.SmartDialPrefix; @@ -49,6 +46,7 @@ public class SmartDialNumberListAdapter extends DialerPhoneNumberListAdapter { public SmartDialNumberListAdapter(Context context) { super(context); mNameMatcher = new SmartDialNameMatcher("", SmartDialPrefix.getMap()); + setShortcutEnabled(SmartDialNumberListAdapter.SHORTCUT_DIRECT_CALL, false); if (DEBUG) { Log.v(TAG, "Constructing List Adapter"); @@ -119,7 +117,9 @@ public class SmartDialNumberListAdapter extends DialerPhoneNumberListAdapter { public void setQueryString(String queryString) { final boolean showNumberShortcuts = !TextUtils.isEmpty(getFormattedQueryString()); boolean changed = false; - changed |= setShortcutEnabled(SHORTCUT_ADD_NUMBER_TO_CONTACTS, showNumberShortcuts); + changed |= setShortcutEnabled(SHORTCUT_CREATE_NEW_CONTACT, showNumberShortcuts); + changed |= setShortcutEnabled(SHORTCUT_ADD_TO_EXISTING_CONTACT, showNumberShortcuts); + changed |= setShortcutEnabled(SHORTCUT_SEND_SMS_MESSAGE, showNumberShortcuts); changed |= setShortcutEnabled(SHORTCUT_MAKE_VIDEO_CALL, showNumberShortcuts && CallUtil.isVideoEnabled(getContext())); if (changed) { diff --git a/src/com/android/dialer/list/SmartDialSearchFragment.java b/src/com/android/dialer/list/SmartDialSearchFragment.java index 4f0ce7216..082bc4360 100644 --- a/src/com/android/dialer/list/SmartDialSearchFragment.java +++ b/src/com/android/dialer/list/SmartDialSearchFragment.java @@ -38,11 +38,8 @@ public class SmartDialSearchFragment extends SearchFragment { SmartDialNumberListAdapter adapter = new SmartDialNumberListAdapter(getActivity()); adapter.setUseCallableUri(super.usesCallableUri()); adapter.setQuickContactEnabled(true); - // Disable the direct call shortcut for the smart dial fragment, since the call button - // will already be showing anyway. - adapter.setShortcutEnabled(SmartDialNumberListAdapter.SHORTCUT_DIRECT_CALL, false); - adapter.setShortcutEnabled(SmartDialNumberListAdapter.SHORTCUT_ADD_NUMBER_TO_CONTACTS, - false); + // Set adapter's query string to restore previous instance state. + adapter.setQueryString(getQueryString()); return adapter; } diff --git a/src/com/android/dialer/list/SpeedDialFragment.java b/src/com/android/dialer/list/SpeedDialFragment.java index 63f1f34c6..e72b25059 100644 --- a/src/com/android/dialer/list/SpeedDialFragment.java +++ b/src/com/android/dialer/list/SpeedDialFragment.java @@ -28,6 +28,7 @@ import android.database.Cursor; import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; +import android.os.Trace; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -38,10 +39,11 @@ import android.view.animation.LayoutAnimationController; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; import android.widget.ImageView; import android.widget.ListView; import android.widget.RelativeLayout; -import android.widget.RelativeLayout.LayoutParams; import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.common.ContactTileLoaderFactory; @@ -68,7 +70,7 @@ public class SpeedDialFragment extends Fragment implements OnItemClickListener, */ private static final long KEY_REMOVED_ITEM_HEIGHT = Long.MAX_VALUE; - private static final String TAG = SpeedDialFragment.class.getSimpleName(); + private static final String TAG = "SpeedDialFragment"; private static final boolean DEBUG = false; private int mAnimationDuration; @@ -180,21 +182,26 @@ public class SpeedDialFragment extends Fragment implements OnItemClickListener, @Override public void onCreate(Bundle savedState) { if (DEBUG) Log.d(TAG, "onCreate()"); + Trace.beginSection(TAG + " onCreate"); super.onCreate(savedState); mAnimationDuration = getResources().getInteger(R.integer.fade_duration); + Trace.endSection(); } @Override public void onResume() { + Trace.beginSection(TAG + " onResume"); super.onResume(); getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad(); + Trace.endSection(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Trace.beginSection(TAG + " onCreateView"); mParentView = inflater.inflate(R.layout.speed_dial_fragment, container, false); mListView = (PhoneFavoriteListView) mParentView.findViewById(R.id.contact_tile_list); @@ -208,7 +215,6 @@ public class SpeedDialFragment extends Fragment implements OnItemClickListener, (ImageView) getActivity().findViewById(R.id.contact_tile_drag_shadow_overlay); mListView.setDragShadowOverlay(dragShadowOverlay); - final Resources resources = getResources(); mEmptyView = mParentView.findViewById(R.id.empty_list_view); DialerUtils.configureEmptyListView( mEmptyView, R.drawable.empty_speed_dial, R.string.speed_dial_empty, getResources()); @@ -224,7 +230,7 @@ public class SpeedDialFragment extends Fragment implements OnItemClickListener, mListView.setOnScrollListener(mScrollListener); mListView.setFastScrollEnabled(false); mListView.setFastScrollAlwaysVisible(false); - + Trace.endSection(); return mParentView; } @@ -239,7 +245,7 @@ public class SpeedDialFragment extends Fragment implements OnItemClickListener, final int listViewVisibility = visible ? View.GONE : View.VISIBLE; if (previousVisibility != emptyViewVisibility) { - final RelativeLayout.LayoutParams params = (LayoutParams) mContactTileFrame + final FrameLayout.LayoutParams params = (LayoutParams) mContactTileFrame .getLayoutParams(); params.height = visible ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; mContactTileFrame.setLayoutParams(params); diff --git a/src/com/android/dialer/list/SwipeHelper.java b/src/com/android/dialer/list/SwipeHelper.java deleted file mode 100644 index 03300df42..000000000 --- a/src/com/android/dialer/list/SwipeHelper.java +++ /dev/null @@ -1,470 +0,0 @@ -/* - * Copyright (C) 2012 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.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.RectF; -import android.util.Log; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.View; -import android.view.animation.LinearInterpolator; - -import com.android.dialer.R; - -/** - * Copy of packages/apps/UnifiedEmail - com.android.mail.ui.SwipeHelper with changes. - */ -public class SwipeHelper { - static final String TAG = SwipeHelper.class.getSimpleName(); - private static final boolean DEBUG_INVALIDATE = false; - private static final boolean CONSTRAIN_SWIPE = true; - private static final boolean FADE_OUT_DURING_SWIPE = true; - private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true; - private static final boolean LOG_SWIPE_DISMISS_VELOCITY = false; // STOPSHIP - DEBUG ONLY - - public static final int IS_SWIPEABLE_TAG = R.id.is_swipeable_tag; - public static final Object IS_SWIPEABLE = new Object(); - - public static final int X = 0; - public static final int Y = 1; - - private static LinearInterpolator sLinearInterpolator = new LinearInterpolator(); - - private static int SWIPE_ESCAPE_VELOCITY = -1; - private static int DEFAULT_ESCAPE_ANIMATION_DURATION; - private static int MAX_ESCAPE_ANIMATION_DURATION; - private static int MAX_DISMISS_VELOCITY; - private static int SNAP_ANIM_LEN; - private static int SWIPE_SCROLL_SLOP; - private static float MIN_SWIPE; - private static float MIN_VERT; - private static float MIN_LOCK; - - public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width - // where fade starts - static final float ALPHA_FADE_END = 0.7f; // fraction of thumbnail width - // beyond which alpha->0 - private static final float FACTOR = 1.2f; - - private static final int PROTECTION_PADDING = 50; - - private float mMinAlpha = 0.3f; - - private float mPagingTouchSlop; - private final SwipeHelperCallback mCallback; - private final int mSwipeDirection; - private final VelocityTracker mVelocityTracker; - - private float mInitialTouchPosX; - private boolean mDragging; - private View mCurrView; - private View mCurrAnimView; - private boolean mCanCurrViewBeDimissed; - private float mDensityScale; - private float mLastY; - private float mInitialTouchPosY; - - private float mStartAlpha; - private boolean mProtected = false; - - private float mChildSwipedFarEnoughFactor = 0.4f; - private float mChildSwipedFastEnoughFactor = 0.05f; - - public SwipeHelper(Context context, int swipeDirection, SwipeHelperCallback callback, float densityScale, - float pagingTouchSlop) { - mCallback = callback; - mSwipeDirection = swipeDirection; - mVelocityTracker = VelocityTracker.obtain(); - mDensityScale = densityScale; - mPagingTouchSlop = pagingTouchSlop; - if (SWIPE_ESCAPE_VELOCITY == -1) { - Resources res = context.getResources(); - SWIPE_ESCAPE_VELOCITY = res.getInteger(R.integer.swipe_escape_velocity); - DEFAULT_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.escape_animation_duration); - MAX_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.max_escape_animation_duration); - MAX_DISMISS_VELOCITY = res.getInteger(R.integer.max_dismiss_velocity); - SNAP_ANIM_LEN = res.getInteger(R.integer.snap_animation_duration); - SWIPE_SCROLL_SLOP = res.getInteger(R.integer.swipe_scroll_slop); - MIN_SWIPE = res.getDimension(R.dimen.min_swipe); - MIN_VERT = res.getDimension(R.dimen.min_vert); - MIN_LOCK = res.getDimension(R.dimen.min_lock); - } - } - - public void setDensityScale(float densityScale) { - mDensityScale = densityScale; - } - - public void setPagingTouchSlop(float pagingTouchSlop) { - mPagingTouchSlop = pagingTouchSlop; - } - - public void setChildSwipedFarEnoughFactor(float factor) { - mChildSwipedFarEnoughFactor = factor; - } - - public void setChildSwipedFastEnoughFactor(float factor) { - mChildSwipedFastEnoughFactor = factor; - } - - private float getVelocity(VelocityTracker vt) { - return mSwipeDirection == X ? vt.getXVelocity() : - vt.getYVelocity(); - } - - private ObjectAnimator createTranslationAnimation(View v, float newPos) { - ObjectAnimator anim = ObjectAnimator.ofFloat(v, - mSwipeDirection == X ? "translationX" : "translationY", newPos); - return anim; - } - - private ObjectAnimator createDismissAnimation(View v, float newPos, int duration) { - ObjectAnimator anim = createTranslationAnimation(v, newPos); - anim.setInterpolator(sLinearInterpolator); - anim.setDuration(duration); - return anim; - } - - private float getPerpendicularVelocity(VelocityTracker vt) { - return mSwipeDirection == X ? vt.getYVelocity() : - vt.getXVelocity(); - } - - private void setTranslation(View v, float translate) { - if (mSwipeDirection == X) { - v.setTranslationX(translate); - } else { - v.setTranslationY(translate); - } - } - - private float getSize(View v) { - return mSwipeDirection == X ? v.getMeasuredWidth() : - v.getMeasuredHeight(); - } - - public void setMinAlpha(float minAlpha) { - mMinAlpha = minAlpha; - } - - private float getAlphaForOffset(View view) { - float viewSize = getSize(view); - final float fadeSize = ALPHA_FADE_END * viewSize; - float result = mStartAlpha; - float pos = view.getTranslationX(); - if (pos >= viewSize * ALPHA_FADE_START) { - result = mStartAlpha - (pos - viewSize * ALPHA_FADE_START) / fadeSize; - } else if (pos < viewSize * (mStartAlpha - ALPHA_FADE_START)) { - result = mStartAlpha + (viewSize * ALPHA_FADE_START + pos) / fadeSize; - } - return Math.max(mMinAlpha, result); - } - - // invalidate the view's own bounds all the way up the view hierarchy - public static void invalidateGlobalRegion(View view) { - invalidateGlobalRegion( - view, - new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom())); - } - - // invalidate a rectangle relative to the view's coordinate system all the way up the view - // hierarchy - public static void invalidateGlobalRegion(View view, RectF childBounds) { - // childBounds.offset(view.getTranslationX(), view.getTranslationY()); - if (DEBUG_INVALIDATE) - Log.v(TAG, "-------------"); - while (view.getParent() != null && view.getParent() instanceof View) { - view = (View) view.getParent(); - view.getMatrix().mapRect(childBounds); - view.invalidate((int) Math.floor(childBounds.left), - (int) Math.floor(childBounds.top), - (int) Math.ceil(childBounds.right), - (int) Math.ceil(childBounds.bottom)); - if (DEBUG_INVALIDATE) { - Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left) - + "," + (int) Math.floor(childBounds.top) - + "," + (int) Math.ceil(childBounds.right) - + "," + (int) Math.ceil(childBounds.bottom)); - } - } - } - - public boolean onInterceptTouchEvent(MotionEvent ev) { - final int action = ev.getAction(); - switch (action) { - case MotionEvent.ACTION_DOWN: - mLastY = ev.getY(); - mDragging = false; - mCurrView = mCallback.getChildAtPosition(ev); - mVelocityTracker.clear(); - if (mCurrView != null) { - mCurrAnimView = mCallback.getChildContentView(mCurrView); - mStartAlpha = mCurrAnimView.getAlpha(); - mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); - mVelocityTracker.addMovement(ev); - mInitialTouchPosX = ev.getX(); - mInitialTouchPosY = ev.getY(); - } - break; - case MotionEvent.ACTION_MOVE: - if (mCurrView != null) { - // Check the movement direction. - if (mLastY >= 0 && !mDragging) { - float currY = ev.getY(); - float currX = ev.getX(); - float deltaY = Math.abs(currY - mInitialTouchPosY); - float deltaX = Math.abs(currX - mInitialTouchPosX); - if (deltaY > SWIPE_SCROLL_SLOP && deltaY > (FACTOR * deltaX)) { - mLastY = ev.getY(); - mCallback.onScroll(); - return false; - } - } - mVelocityTracker.addMovement(ev); - float pos = ev.getX(); - float delta = pos - mInitialTouchPosX; - if (Math.abs(delta) > mPagingTouchSlop) { - mCallback.onBeginDrag(mCallback.getChildContentView(mCurrView)); - mDragging = true; - mInitialTouchPosX = ev.getX() - mCurrAnimView.getTranslationX(); - mInitialTouchPosY = ev.getY(); - } - } - mLastY = ev.getY(); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mDragging = false; - mCurrView = null; - mCurrAnimView = null; - mLastY = -1; - break; - } - return mDragging; - } - - /** - * @param view The view to be dismissed - * @param velocity The desired pixels/second speed at which the view should - * move - */ - private void dismissChild(final View view, float velocity) { - final View animView = mCallback.getChildContentView(view); - final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); - float newPos = determinePos(animView, velocity); - int duration = determineDuration(animView, newPos, velocity); - - animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); - ObjectAnimator anim = createDismissAnimation(animView, newPos, duration); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mCallback.onChildDismissed(view); - animView.setLayerType(View.LAYER_TYPE_NONE, null); - } - }); - anim.addUpdateListener(new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { - animView.setAlpha(getAlphaForOffset(animView)); - } - invalidateGlobalRegion(animView); - } - }); - anim.start(); - } - - private int determineDuration(View animView, float newPos, float velocity) { - int duration = MAX_ESCAPE_ANIMATION_DURATION; - if (velocity != 0) { - duration = Math - .min(duration, - (int) (Math.abs(newPos - animView.getTranslationX()) * 1000f / Math - .abs(velocity))); - } else { - duration = DEFAULT_ESCAPE_ANIMATION_DURATION; - } - return duration; - } - - private float determinePos(View animView, float velocity) { - float newPos = 0; - if (velocity < 0 || (velocity == 0 && animView.getTranslationX() < 0) - // if we use the Menu to dismiss an item in landscape, animate up - || (velocity == 0 && animView.getTranslationX() == 0 && mSwipeDirection == Y)) { - newPos = -getSize(animView); - } else { - newPos = getSize(animView); - } - return newPos; - } - - public void snapChild(final View view, float velocity) { - final View animView = mCallback.getChildContentView(view); - final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); - ObjectAnimator anim = createTranslationAnimation(animView, 0); - int duration = SNAP_ANIM_LEN; - anim.setDuration(duration); - anim.addUpdateListener(new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { - animView.setAlpha(getAlphaForOffset(animView)); - } - invalidateGlobalRegion(animView); - } - }); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animView.setAlpha(mStartAlpha); - mCallback.onDragCancelled(mCurrView); - } - }); - anim.start(); - } - - public boolean onTouchEvent(MotionEvent ev) { - if (!mDragging || mProtected) { - return false; - } - mVelocityTracker.addMovement(ev); - final int action = ev.getAction(); - switch (action) { - case MotionEvent.ACTION_OUTSIDE: - case MotionEvent.ACTION_MOVE: - if (mCurrView != null) { - float deltaX = ev.getX() - mInitialTouchPosX; - float deltaY = Math.abs(ev.getY() - mInitialTouchPosY); - // If the user has gone vertical and not gone horizontalish AT - // LEAST minBeforeLock, switch to scroll. Otherwise, cancel - // the swipe. - if (!mDragging && deltaY > MIN_VERT && (Math.abs(deltaX)) < MIN_LOCK - && deltaY > (FACTOR * Math.abs(deltaX))) { - mCallback.onScroll(); - return false; - } - float minDistance = MIN_SWIPE; - if (Math.abs(deltaX) < minDistance) { - // Don't start the drag until at least X distance has - // occurred. - return true; - } - // don't let items that can't be dismissed be dragged more - // than maxScrollDistance - if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { - float size = getSize(mCurrAnimView); - float maxScrollDistance = 0.15f * size; - if (Math.abs(deltaX) >= size) { - deltaX = deltaX > 0 ? maxScrollDistance : -maxScrollDistance; - } else { - deltaX = maxScrollDistance - * (float) Math.sin((deltaX / size) * (Math.PI / 2)); - } - } - setTranslation(mCurrAnimView, deltaX); - if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { - mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView)); - } - invalidateGlobalRegion(mCallback.getChildContentView(mCurrView)); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - if (mCurrView != null) { - float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; - mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); - float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; - float velocity = getVelocity(mVelocityTracker); - float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); - - // Decide whether to dismiss the current view - // Tweak constants below as required to prevent erroneous - // swipe/dismiss - float translation = Math.abs(mCurrAnimView.getTranslationX()); - float currAnimViewSize = getSize(mCurrAnimView); - // Long swipe = translation of {@link #mChildSwipedFarEnoughFactor} * width - boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH - && translation > mChildSwipedFarEnoughFactor * currAnimViewSize; - // Fast swipe = > escapeVelocity and translation of - // {@link #mChildSwipedFastEnoughFactor} * width - boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) - && (Math.abs(velocity) > Math.abs(perpendicularVelocity)) - && (velocity > 0) == (mCurrAnimView.getTranslationX() > 0) - && translation > mChildSwipedFastEnoughFactor * currAnimViewSize; - if (LOG_SWIPE_DISMISS_VELOCITY) { - Log.v(TAG, "Swipe/Dismiss: " + velocity + "/" + escapeVelocity + "/" - + perpendicularVelocity + ", x: " + translation + "/" - + currAnimViewSize); - } - - boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) - && (childSwipedFastEnough || childSwipedFarEnough); - - if (dismissChild) { - dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); - } else { - snapChild(mCurrView, velocity); - } - } - break; - } - return true; - } - - public static void setSwipeable(View view, boolean swipeable) { - view.setTag(IS_SWIPEABLE_TAG, swipeable ? IS_SWIPEABLE : null); - } - - public static boolean isSwipeable(View view) { - return IS_SWIPEABLE == view.getTag(IS_SWIPEABLE_TAG); - } - - public interface SwipeHelperCallback { - View getChildAtPosition(MotionEvent ev); - - View getChildContentView(View v); - - void onScroll(); - - boolean canChildBeDismissed(View v); - - void onBeginDrag(View v); - - void onChildDismissed(View v); - - void onDragCancelled(View v); - - } - - public interface OnItemGestureListener { - public void onSwipe(View view); - - public void onTouch(); - - public boolean isSwipeEnabled(); - } -} diff --git a/src/com/android/dialer/settings/DialerSettingsActivity.java b/src/com/android/dialer/settings/DialerSettingsActivity.java index de07128ca..8653dc9a7 100644 --- a/src/com/android/dialer/settings/DialerSettingsActivity.java +++ b/src/com/android/dialer/settings/DialerSettingsActivity.java @@ -2,6 +2,7 @@ package com.android.dialer.settings; import com.google.common.collect.Lists; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -14,13 +15,7 @@ import android.preference.PreferenceActivity.Header; import android.telecom.TelecomManager; import android.telephony.TelephonyManager; import android.text.TextUtils; -import android.view.LayoutInflater; import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListAdapter; -import android.widget.TextView; import com.android.dialer.DialtactsActivity; import com.android.dialer.R; @@ -30,7 +25,6 @@ import java.util.List; public class DialerSettingsActivity extends PreferenceActivity { protected SharedPreferences mPreferences; - private HeaderAdapter mHeaderAdapter; private static final int OWNER_HANDLE_ID = 0; @@ -42,18 +36,31 @@ public class DialerSettingsActivity extends PreferenceActivity { @Override public void onBuildHeaders(List<Header> target) { - final Header generalSettingsHeader = new Header(); - generalSettingsHeader.titleRes = R.string.general_settings_label; - generalSettingsHeader.fragment = GeneralSettingsFragment.class.getName(); - target.add(generalSettingsHeader); + Header displayOptionsHeader = new Header(); + displayOptionsHeader.titleRes = R.string.display_options_title; + displayOptionsHeader.fragment = DisplayOptionsSettingsFragment.class.getName(); + target.add(displayOptionsHeader); + + Header soundSettingsHeader = new Header(); + soundSettingsHeader.titleRes = R.string.sounds_and_vibration_title; + soundSettingsHeader.fragment = SoundSettingsFragment.class.getName(); + target.add(soundSettingsHeader); + + Header quickResponseSettingsHeader = new Header(); + Intent quickResponseSettingsIntent = + new Intent(TelecomManager.ACTION_SHOW_RESPOND_VIA_SMS_SETTINGS); + quickResponseSettingsHeader.titleRes = R.string.respond_via_sms_setting_title; + quickResponseSettingsHeader.intent = quickResponseSettingsIntent; + target.add(quickResponseSettingsHeader); + + TelephonyManager telephonyManager = + (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); // Only show call setting menus if the current user is the primary/owner user. if (isPrimaryUser()) { - final TelephonyManager telephonyManager = - (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); // Show "Call Settings" if there is one SIM and "Phone Accounts" if there are more. if (telephonyManager.getPhoneCount() <= 1) { - final Header callSettingsHeader = new Header(); + Header callSettingsHeader = new Header(); Intent callSettingsIntent = new Intent(TelecomManager.ACTION_SHOW_CALL_SETTINGS); callSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); @@ -61,7 +68,7 @@ public class DialerSettingsActivity extends PreferenceActivity { callSettingsHeader.intent = callSettingsIntent; target.add(callSettingsHeader); } else { - final Header phoneAccountSettingsHeader = new Header(); + Header phoneAccountSettingsHeader = new Header(); Intent phoneAccountSettingsIntent = new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS); phoneAccountSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); @@ -71,6 +78,16 @@ public class DialerSettingsActivity extends PreferenceActivity { target.add(phoneAccountSettingsHeader); } } + + if (telephonyManager.isTtyModeSupported() + || telephonyManager.isHearingAidCompatibilitySupported()) { + Header accessibilitySettingsHeader = new Header(); + Intent accessibilitySettingsIntent = + new Intent(TelecomManager.ACTION_SHOW_CALL_ACCESSIBILITY_SETTINGS); + accessibilitySettingsHeader.titleRes = R.string.accessibility_settings_title; + accessibilitySettingsHeader.intent = accessibilitySettingsIntent; + target.add(accessibilitySettingsHeader); + } } @Override @@ -87,23 +104,6 @@ public class DialerSettingsActivity extends PreferenceActivity { return true; } - @Override - public void setListAdapter(ListAdapter adapter) { - if (adapter == null) { - super.setListAdapter(null); - } else { - // We don't have access to the hidden getHeaders() method, so grab the headers from - // the intended adapter and then replace it with our own. - int headerCount = adapter.getCount(); - List<Header> headers = Lists.newArrayList(); - for (int i = 0; i < headerCount; i++) { - headers.add((Header) adapter.getItem(i)); - } - mHeaderAdapter = new HeaderAdapter(this, headers); - super.setListAdapter(mHeaderAdapter); - } - } - /** * Whether a user handle associated with the current user is that of the primary owner. That is, * whether there is a user handle which has an id which matches the owner's handle. @@ -120,51 +120,4 @@ public class DialerSettingsActivity extends PreferenceActivity { return false; } - - /** - * This custom {@code ArrayAdapter} is mostly identical to the equivalent one in - * {@code PreferenceActivity}, except with a local layout resource. - */ - private static class HeaderAdapter extends ArrayAdapter<Header> { - static class HeaderViewHolder { - TextView title; - TextView summary; - } - - private LayoutInflater mInflater; - - public HeaderAdapter(Context context, List<Header> objects) { - super(context, 0, objects); - mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - HeaderViewHolder holder; - View view; - - if (convertView == null) { - view = mInflater.inflate(R.layout.dialer_preferences, parent, false); - holder = new HeaderViewHolder(); - holder.title = (TextView) view.findViewById(R.id.title); - holder.summary = (TextView) view.findViewById(R.id.summary); - view.setTag(holder); - } else { - view = convertView; - holder = (HeaderViewHolder) view.getTag(); - } - - // All view fields must be updated every time, because the view may be recycled - Header header = getItem(position); - holder.title.setText(header.getTitle(getContext().getResources())); - CharSequence summary = header.getSummary(getContext().getResources()); - if (!TextUtils.isEmpty(summary)) { - holder.summary.setVisibility(View.VISIBLE); - holder.summary.setText(summary); - } else { - holder.summary.setVisibility(View.GONE); - } - return view; - } - } } diff --git a/src/com/android/dialer/settings/DisplayOptionsSettingsFragment.java b/src/com/android/dialer/settings/DisplayOptionsSettingsFragment.java new file mode 100644 index 000000000..4b2c8f6db --- /dev/null +++ b/src/com/android/dialer/settings/DisplayOptionsSettingsFragment.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2015 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.settings; + +import android.os.Bundle; +import android.preference.PreferenceFragment; + +import com.android.dialer.R; + +public class DisplayOptionsSettingsFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.display_options_settings); + } +} diff --git a/src/com/android/dialer/settings/GeneralSettingsFragment.java b/src/com/android/dialer/settings/GeneralSettingsFragment.java deleted file mode 100644 index 578ff3389..000000000 --- a/src/com/android/dialer/settings/GeneralSettingsFragment.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (C) 2014 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.settings; - -import android.content.Context; -import android.media.RingtoneManager; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.os.Vibrator; -import android.preference.CheckBoxPreference; -import android.preference.Preference; -import android.preference.PreferenceCategory; -import android.preference.PreferenceFragment; -import android.preference.PreferenceScreen; -import android.provider.Settings; - -import com.android.dialer.R; -import com.android.phone.common.util.SettingsUtil; - -import java.lang.Boolean; -import java.lang.CharSequence; -import java.lang.Object; -import java.lang.Override; -import java.lang.Runnable; -import java.lang.String; -import java.lang.Thread; - -public class GeneralSettingsFragment extends PreferenceFragment - implements Preference.OnPreferenceChangeListener { - private static final String CATEGORY_SOUNDS_KEY = "dialer_general_sounds_category_key"; - private static final String BUTTON_RINGTONE_KEY = "button_ringtone_key"; - private static final String BUTTON_VIBRATE_ON_RING = "button_vibrate_on_ring"; - private static final String BUTTON_PLAY_DTMF_TONE = "button_play_dtmf_tone"; - private static final String BUTTON_RESPOND_VIA_SMS_KEY = "button_respond_via_sms_key"; - - private static final int MSG_UPDATE_RINGTONE_SUMMARY = 1; - - private Context mContext; - - private Preference mRingtonePreference; - private CheckBoxPreference mVibrateWhenRinging; - private CheckBoxPreference mPlayDtmfTone; - private Preference mRespondViaSms; - - private Runnable mRingtoneLookupRunnable; - private final Handler mRingtoneLookupComplete = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_UPDATE_RINGTONE_SUMMARY: - mRingtonePreference.setSummary((CharSequence) msg.obj); - break; - } - } - }; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mContext = getActivity().getApplicationContext(); - - addPreferencesFromResource(R.xml.general_settings); - - mRingtonePreference = findPreference(BUTTON_RINGTONE_KEY); - mVibrateWhenRinging = (CheckBoxPreference) findPreference(BUTTON_VIBRATE_ON_RING); - mPlayDtmfTone = (CheckBoxPreference) findPreference(BUTTON_PLAY_DTMF_TONE); - mRespondViaSms = findPreference(BUTTON_RESPOND_VIA_SMS_KEY); - - PreferenceCategory soundCategory = (PreferenceCategory) findPreference(CATEGORY_SOUNDS_KEY); - if (mVibrateWhenRinging != null) { - Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); - if (vibrator != null && vibrator.hasVibrator()) { - mVibrateWhenRinging.setOnPreferenceChangeListener(this); - } else { - soundCategory.removePreference(mVibrateWhenRinging); - mVibrateWhenRinging = null; - } - } - - if (mPlayDtmfTone != null) { - mPlayDtmfTone.setOnPreferenceChangeListener(this); - mPlayDtmfTone.setChecked(Settings.System.getInt(mContext.getContentResolver(), - Settings.System.DTMF_TONE_WHEN_DIALING, 1) != 0); - } - - mRingtoneLookupRunnable = new Runnable() { - @Override - public void run() { - if (mRingtonePreference != null) { - SettingsUtil.updateRingtoneName( - mContext, - mRingtoneLookupComplete, - RingtoneManager.TYPE_RINGTONE, - mRingtonePreference.getKey(), - MSG_UPDATE_RINGTONE_SUMMARY); - } - } - }; - } - - /** - * Supports onPreferenceChangeListener to look for preference changes. - * - * @param preference The preference to be changed - * @param objValue The value of the selection, NOT its localized display value. - */ - @Override - public boolean onPreferenceChange(Preference preference, Object objValue) { - if (preference == mVibrateWhenRinging) { - boolean doVibrate = (Boolean) objValue; - Settings.System.putInt(mContext.getContentResolver(), - Settings.System.VIBRATE_WHEN_RINGING, doVibrate ? 1 : 0); - } - return true; - } - - /** - * Click listener for toggle events. - */ - @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { - if (preference == mPlayDtmfTone) { - Settings.System.putInt(mContext.getContentResolver(), - Settings.System.DTMF_TONE_WHEN_DIALING, mPlayDtmfTone.isChecked() ? 1 : 0); - } else if (preference == mRespondViaSms) { - // Needs to return false for the intent to launch. - return false; - } - return true; - } - - @Override - public void onResume() { - super.onResume(); - - if (mVibrateWhenRinging != null) { - mVibrateWhenRinging.setChecked(SettingsUtil.getVibrateWhenRingingSetting(mContext)); - } - - // Lookup the ringtone name asynchronously. - new Thread(mRingtoneLookupRunnable).start(); - } -} diff --git a/src/com/android/dialer/settings/SoundSettingsFragment.java b/src/com/android/dialer/settings/SoundSettingsFragment.java new file mode 100644 index 000000000..43297b5e5 --- /dev/null +++ b/src/com/android/dialer/settings/SoundSettingsFragment.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2014 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.settings; + +import android.content.Context; +import android.media.RingtoneManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Vibrator; +import android.preference.CheckBoxPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; +import android.provider.Settings; +import android.telephony.TelephonyManager; + +import com.android.dialer.R; +import com.android.phone.common.util.SettingsUtil; + +import java.lang.Boolean; +import java.lang.CharSequence; +import java.lang.Object; +import java.lang.Override; +import java.lang.Runnable; +import java.lang.String; +import java.lang.Thread; + +public class SoundSettingsFragment extends PreferenceFragment + implements Preference.OnPreferenceChangeListener { + + private static final int NO_DTMF_TONE = 0; + private static final int PLAY_DTMF_TONE = 1; + + private static final int NO_VIBRATION_FOR_CALLS = 0; + private static final int DO_VIBRATION_FOR_CALLS = 1; + + private static final int SHOW_CARRIER_SETTINGS = 0; + private static final int HIDE_CARRIER_SETTINGS = 1; + + private static final int MSG_UPDATE_RINGTONE_SUMMARY = 1; + + private Preference mRingtonePreference; + private CheckBoxPreference mVibrateWhenRinging; + private CheckBoxPreference mPlayDtmfTone; + private ListPreference mDtmfToneLength; + + private final Runnable mRingtoneLookupRunnable = new Runnable() { + @Override + public void run() { + updateRingtonePreferenceSummary(); + } + }; + + private final Handler mRingtoneLookupComplete = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_UPDATE_RINGTONE_SUMMARY: + mRingtonePreference.setSummary((CharSequence) msg.obj); + break; + } + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.sound_settings); + + Context context = getActivity(); + + mRingtonePreference = findPreference(context.getString(R.string.ringtone_preference_key)); + mVibrateWhenRinging = (CheckBoxPreference) findPreference( + context.getString(R.string.vibrate_on_preference_key)); + mPlayDtmfTone = (CheckBoxPreference) findPreference( + context.getString(R.string.play_dtmf_preference_key)); + mDtmfToneLength = (ListPreference) findPreference( + context.getString(R.string.dtmf_tone_length_preference_key)); + + if (hasVibrator()) { + mVibrateWhenRinging.setOnPreferenceChangeListener(this); + } else { + getPreferenceScreen().removePreference(mVibrateWhenRinging); + mVibrateWhenRinging = null; + } + + mPlayDtmfTone.setOnPreferenceChangeListener(this); + mPlayDtmfTone.setChecked(shouldPlayDtmfTone()); + + TelephonyManager telephonyManager = + (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE); + if (telephonyManager.canChangeDtmfToneLength() + && (telephonyManager.isWorldPhone() || !shouldHideCarrierSettings())) { + mDtmfToneLength.setOnPreferenceChangeListener(this); + mDtmfToneLength.setValueIndex( + Settings.System.getInt(context.getContentResolver(), + Settings.System.DTMF_TONE_WHEN_DIALING, PLAY_DTMF_TONE)); + } else { + getPreferenceScreen().removePreference(mDtmfToneLength); + mDtmfToneLength = null; + } + } + + @Override + public void onResume() { + super.onResume(); + + if (mVibrateWhenRinging != null) { + mVibrateWhenRinging.setChecked(shouldVibrateWhenRinging()); + } + + // Lookup the ringtone name asynchronously. + new Thread(mRingtoneLookupRunnable).start(); + } + + /** + * Supports onPreferenceChangeListener to look for preference changes. + * + * @param preference The preference to be changed + * @param objValue The value of the selection, NOT its localized display value. + */ + @Override + public boolean onPreferenceChange(Preference preference, Object objValue) { + if (preference == mVibrateWhenRinging) { + boolean doVibrate = (Boolean) objValue; + Settings.System.putInt(getActivity().getContentResolver(), + Settings.System.VIBRATE_WHEN_RINGING, + doVibrate ? DO_VIBRATION_FOR_CALLS : NO_VIBRATION_FOR_CALLS); + } else if (preference == mDtmfToneLength) { + int index = mDtmfToneLength.findIndexOfValue((String) objValue); + Settings.System.putInt(getActivity().getContentResolver(), + Settings.System.DTMF_TONE_TYPE_WHEN_DIALING, index); + } + return true; + } + + /** + * Click listener for toggle events. + */ + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + if (preference == mPlayDtmfTone) { + Settings.System.putInt(getActivity().getContentResolver(), + Settings.System.DTMF_TONE_WHEN_DIALING, + mPlayDtmfTone.isChecked() ? PLAY_DTMF_TONE : NO_DTMF_TONE); + } + return true; + } + + /** + * Updates the summary text on the ringtone preference with the name of the ringtone. + */ + private void updateRingtonePreferenceSummary() { + SettingsUtil.updateRingtoneName( + getActivity(), + mRingtoneLookupComplete, + RingtoneManager.TYPE_RINGTONE, + mRingtonePreference.getKey(), + MSG_UPDATE_RINGTONE_SUMMARY); + } + + /** + * Obtain the value for "vibrate when ringing" setting. The default value is false. + * + * Watch out: if the setting is missing in the device, this will try obtaining the old + * "vibrate on ring" setting from AudioManager, and save the previous setting to the new one. + */ + private boolean shouldVibrateWhenRinging() { + int vibrateWhenRingingSetting = Settings.System.getInt(getActivity().getContentResolver(), + Settings.System.VIBRATE_WHEN_RINGING, + NO_VIBRATION_FOR_CALLS); + return hasVibrator() && (vibrateWhenRingingSetting == DO_VIBRATION_FOR_CALLS); + } + + /** + * Obtains the value for dialpad/DTMF tones. The default value is true. + */ + private boolean shouldPlayDtmfTone() { + int dtmfToneSetting = Settings.System.getInt(getActivity().getContentResolver(), + Settings.System.DTMF_TONE_WHEN_DIALING, + PLAY_DTMF_TONE); + return dtmfToneSetting == PLAY_DTMF_TONE; + } + + /** + * Whether the device hardware has a vibrator. + */ + private boolean hasVibrator() { + Vibrator vibrator = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE); + return vibrator != null && vibrator.hasVibrator(); + } + + private boolean shouldHideCarrierSettings() { + int hideCarrierNetworkSetting = Settings.Global.getInt(getActivity().getContentResolver(), + Settings.Global.HIDE_CARRIER_NETWORK_SETTINGS, SHOW_CARRIER_SETTINGS); + return hideCarrierNetworkSetting == HIDE_CARRIER_SETTINGS; + } +} diff --git a/src/com/android/dialer/util/PrivilegedCallUtil.java b/src/com/android/dialer/util/PrivilegedCallUtil.java new file mode 100644 index 000000000..367f36566 --- /dev/null +++ b/src/com/android/dialer/util/PrivilegedCallUtil.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.util; + +import android.content.Intent; +import android.net.Uri; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.telecom.VideoProfile; + +import com.android.contacts.common.CallUtil; +import com.android.phone.common.PhoneConstants; + +/** + * Utilities related to calls that can only be used by system apps. These + * use {@link Intent#ACTION_CALL_PRIVILEGED}. + */ +public class PrivilegedCallUtil { + + public static final String CALL_ACTION = Intent.ACTION_CALL_PRIVILEGED; + + /** + * Return an Intent for making a phone call. Scheme (e.g. tel, sip) will be determined + * automatically. + */ + public static Intent getCallIntent(String number) { + return getCallIntent(number, null, null); + } + + /** + * Return an Intent for making a phone call. A given Uri will be used as is (without any + * sanity check). + */ + public static Intent getCallIntent(Uri uri) { + return getCallIntent(uri, null, null); + } + + /** + * A variant of {@link #getCallIntent(String)} but also accept a call origin. + * For more information about call origin, see comments in Phone package (PhoneApp). + */ + public static Intent getCallIntent(String number, String callOrigin) { + return getCallIntent(CallUtil.getCallUri(number), callOrigin, null); + } + + /** + * A variant of {@link #getCallIntent(String)} but also include {@code Account}. + */ + public static Intent getCallIntent(String number, PhoneAccountHandle accountHandle) { + return getCallIntent(number, null, accountHandle); + } + + /** + * A variant of {@link #getCallIntent(android.net.Uri)} but also include {@code Account}. + */ + public static Intent getCallIntent(Uri uri, PhoneAccountHandle accountHandle) { + return getCallIntent(uri, null, accountHandle); + } + + /** + * A variant of {@link #getCallIntent(String, String)} but also include {@code Account}. + */ + public static Intent getCallIntent( + String number, String callOrigin, PhoneAccountHandle accountHandle) { + return getCallIntent(CallUtil.getCallUri(number), callOrigin, accountHandle); + } + + /** + * A variant of {@link #getCallIntent(android.net.Uri)} but also accept a call + * origin and {@code Account}. + * For more information about call origin, see comments in Phone package (PhoneApp). + */ + public static Intent getCallIntent( + Uri uri, String callOrigin, PhoneAccountHandle accountHandle) { + return getCallIntent(uri, callOrigin, accountHandle, + VideoProfile.VideoState.AUDIO_ONLY); + } + + /** + * A variant of {@link #getCallIntent(String, String)} for starting a video call. + */ + public static Intent getVideoCallIntent(String number, String callOrigin) { + return getCallIntent(CallUtil.getCallUri(number), callOrigin, null, + VideoProfile.VideoState.BIDIRECTIONAL); + } + + /** + * A variant of {@link #getCallIntent(String, String, android.telecom.PhoneAccountHandle)} for + * starting a video call. + */ + public static Intent getVideoCallIntent( + String number, String callOrigin, PhoneAccountHandle accountHandle) { + return getCallIntent(CallUtil.getCallUri(number), callOrigin, accountHandle, + VideoProfile.VideoState.BIDIRECTIONAL); + } + + /** + * A variant of {@link #getCallIntent(String, String, android.telecom.PhoneAccountHandle)} for + * starting a video call. + */ + public static Intent getVideoCallIntent(String number, PhoneAccountHandle accountHandle) { + return getVideoCallIntent(number, null, accountHandle); + } + + /** + * A variant of {@link #getCallIntent(android.net.Uri)} for calling Voicemail. + */ + public static Intent getVoicemailIntent() { + return getCallIntent(Uri.fromParts(PhoneAccount.SCHEME_VOICEMAIL, "", null)); + } + + /** + * A variant of {@link #getCallIntent(android.net.Uri)} but also accept a call + * origin and {@code Account} and {@code VideoCallProfile} state. + * For more information about call origin, see comments in Phone package (PhoneApp). + */ + public static Intent getCallIntent( + Uri uri, String callOrigin, PhoneAccountHandle accountHandle, int videoState) { + final Intent intent = new Intent(CALL_ACTION, uri); + intent.putExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState); + if (callOrigin != null) { + intent.putExtra(PhoneConstants.EXTRA_CALL_ORIGIN, callOrigin); + } + if (accountHandle != null) { + intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle); + } + + return intent; + } +} diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java b/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java index 31db17720..0d5c3deed 100644 --- a/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java +++ b/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java @@ -120,6 +120,12 @@ public class VoicemailPlaybackFragment extends Fragment { } @Override + public void onViewStateRestored(Bundle savedInstanceState) { + mPresenter.onRestoreInstanceState(savedInstanceState); + super.onViewStateRestored(savedInstanceState); + } + + @Override public void onDestroy() { shutdownMediaPlayer(); mPresenter.onDestroy(); diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java index cb246f4c8..30fea1aee 100644 --- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java +++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java @@ -32,6 +32,7 @@ import com.android.dialer.R; import com.android.dialer.util.AsyncTaskExecutor; import com.android.ex.variablespeed.MediaPlayerProxy; import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -110,8 +111,8 @@ public class VoicemailPlaybackPresenter { * If present in the saved instance bundle, we should not resume playback on * create. */ - private static final String PAUSED_STATE_KEY = VoicemailPlaybackPresenter.class.getName() - + ".PAUSED_STATE_KEY"; + private static final String IS_PLAYING_STATE_KEY = VoicemailPlaybackPresenter.class.getName() + + ".IS_PLAYING_STATE_KEY"; /** * If present in the saved instance bundle, indicates where to set the * playback slider. @@ -169,6 +170,8 @@ public class VoicemailPlaybackPresenter { private FetchResultHandler mFetchResultHandler; private PowerManager.WakeLock mWakeLock; private AsyncTask<Void, ?, ?> mPrepareTask; + private int mPosition; + private boolean mPlaying; public VoicemailPlaybackPresenter(PlaybackView view, MediaPlayerProxy player, Uri voicemailUri, ScheduledExecutorService executorService, @@ -346,22 +349,34 @@ public class VoicemailPlaybackPresenter { mView.setSpeakerPhoneOn(mView.isSpeakerPhoneOn()); mView.setRateDecreaseButtonListener(createRateDecreaseListener()); mView.setRateIncreaseButtonListener(createRateIncreaseListener()); - mView.setClipPosition(0, mDuration.get()); - mView.playbackStopped(); - // Always disable on stop. - mView.disableProximitySensor(); - if (mStartPlayingImmediately) { - resetPrepareStartPlaying(0); + if (mPlaying) { + resetPrepareStartPlaying(mPosition); + } else { + stopPlaybackAtPosition(mPosition, mDuration.get()); + if ((mPosition == 0) && (mStartPlayingImmediately)) { + resetPrepareStartPlaying(0); + } } - // TODO: Now I'm ignoring the bundle, when previously I was checking for contains against - // the PAUSED_STATE_KEY, and CLIP_POSITION_KEY. } public void onSaveInstanceState(Bundle outState) { outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition()); - if (!mPlayer.isPlaying()) { - outState.putBoolean(PAUSED_STATE_KEY, true); + outState.putBoolean(IS_PLAYING_STATE_KEY, mPlaying); + } + + public void onRestoreInstanceState(Bundle inState) { + int position = 0; + boolean isPlaying = false; + if (inState != null) { + position = inState.getInt(CLIP_POSITION_KEY, 0); + isPlaying = inState.getBoolean(IS_PLAYING_STATE_KEY, false); } + setPositionAndPlayingStatus(position, isPlaying) ; + } + + private void setPositionAndPlayingStatus(int position, boolean isPlaying) { + mPosition = position; + mPlaying = isPlaying; } public void onDestroy() { @@ -469,6 +484,7 @@ public class VoicemailPlaybackPresenter { try { // Can throw RejectedExecutionException mPlayer.start(); + setPositionAndPlayingStatus(mPlayer.getCurrentPosition(), true); mView.playbackStarted(); if (!mWakeLock.isHeld()) { mWakeLock.acquire(); @@ -501,6 +517,7 @@ public class VoicemailPlaybackPresenter { mView.playbackError(e); mPositionUpdater.stopUpdating(); mPlayer.release(); + setPositionAndPlayingStatus(0, false); } public void handleCompletion(MediaPlayer mediaPlayer) { @@ -537,10 +554,15 @@ public class VoicemailPlaybackPresenter { @Override public void onStopTrackingTouch(SeekBar arg0) { if (mPlayer.isPlaying()) { + setPositionAndPlayingStatus(mPlayer.getCurrentPosition(), false); stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); + } else { + setPositionAndPlayingStatus(mView.getDesiredClipPosition(), + mShouldResumePlaybackAfterSeeking); } + if (mShouldResumePlaybackAfterSeeking) { - resetPrepareStartPlaying(mView.getDesiredClipPosition()); + postSuccessfullyFetchedContent(); } } @@ -575,9 +597,11 @@ public class VoicemailPlaybackPresenter { @Override public void onClick(View arg0) { if (mPlayer.isPlaying()) { + setPositionAndPlayingStatus(mPlayer.getCurrentPosition(), false); stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); } else { - resetPrepareStartPlaying(mView.getDesiredClipPosition()); + setPositionAndPlayingStatus(mPosition, true); + postSuccessfullyFetchedContent(); } } } diff --git a/src/com/android/dialer/widget/OverlappingPaneLayout.java b/src/com/android/dialer/widget/OverlappingPaneLayout.java deleted file mode 100644 index 167b849f2..000000000 --- a/src/com/android/dialer/widget/OverlappingPaneLayout.java +++ /dev/null @@ -1,1358 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.dialer.widget; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.os.Parcel; -import android.os.Parcelable; -import android.support.v4.view.AccessibilityDelegateCompat; -import android.support.v4.view.MotionEventCompat; -import android.support.v4.view.ViewCompat; -import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; -import android.util.AttributeSet; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.ViewParent; -import android.view.accessibility.AccessibilityEvent; - -/** - * A custom layout that aligns its child views vertically as two panes, and allows for the bottom - * pane to be dragged upwards to overlap and hide the top pane. This layout is adapted from - * {@link android.support.v4.widget.SlidingPaneLayout}. - */ -public class OverlappingPaneLayout extends ViewGroup { - private static final String TAG = "SlidingPaneLayout"; - private static final boolean DEBUG = false; - - /** - * Default size of the overhang for a pane in the open state. - * At least this much of a sliding pane will remain visible. - * This indicates that there is more content available and provides - * a "physical" edge to grab to pull it closed. - */ - private static final int DEFAULT_OVERHANG_SIZE = 32; // dp; - - /** - * If no fade color is given by default it will fade to 80% gray. - */ - private static final int DEFAULT_FADE_COLOR = 0xcccccccc; - - /** - * Minimum velocity that will be detected as a fling - */ - private static final int MIN_FLING_VELOCITY = 400; // dips per second - - /** - * The size of the overhang in pixels. - * This is the minimum section of the sliding panel that will - * be visible in the open state to allow for a closing drag. - */ - private final int mOverhangSize; - - /** - * True if a panel can slide with the current measurements - */ - private boolean mCanSlide; - - /** - * The child view that can slide, if any. - */ - private View mSlideableView; - - /** - * The view that can be used to start the drag with. - */ - private View mCapturableView; - - /** - * How far the panel is offset from its closed position. - * range [0, 1] where 0 = closed, 1 = open. - */ - private float mSlideOffset; - - /** - * How far the panel is offset from its closed position, in pixels. - * range [0, {@link #mSlideRange}] where 0 is completely closed. - */ - private int mSlideOffsetPx; - - /** - * How far in pixels the slideable panel may move. - */ - private int mSlideRange; - - /** - * A panel view is locked into internal scrolling or another condition that - * is preventing a drag. - */ - private boolean mIsUnableToDrag; - - /** - * Tracks whether or not a child view is in the process of a nested scroll. - */ - private boolean mIsInNestedScroll; - - /** - * Indicates that the layout is currently in the process of a nested pre-scroll operation where - * the child scrolling view is being dragged downwards. - */ - private boolean mInNestedPreScrollDownwards; - - /** - * Indicates that the layout is currently in the process of a nested pre-scroll operation where - * the child scrolling view is being dragged upwards. - */ - private boolean mInNestedPreScrollUpwards; - - /** - * Indicates that the layout is currently in the process of a fling initiated by a pre-fling - * from the child scrolling view. - */ - private boolean mIsInNestedFling; - - /** - * Indicates the direction of the pre fling. We need to store this information since - * OverScoller doesn't expose the direction of its velocity. - */ - private boolean mInUpwardsPreFling; - - /** - * Stores an offset used to represent a point somewhere in between the panel's fully closed - * state and fully opened state where the panel can be temporarily pinned or opened up to - * during scrolling. - */ - private int mIntermediateOffset = 0; - - private float mInitialMotionX; - private float mInitialMotionY; - - private PanelSlideCallbacks mPanelSlideCallbacks; - - private final ViewDragHelper mDragHelper; - - /** - * Stores whether or not the pane was open the last time it was slideable. - * If open/close operations are invoked this state is modified. Used by - * instance state save/restore. - */ - private boolean mPreservedOpenState; - private boolean mFirstLayout = true; - - private final Rect mTmpRect = new Rect(); - - /** - * How many dips we need to scroll past a position before we can snap to the next position - * on release. Using this prevents accidentally snapping to positions. - * - * This is needed since vertical nested scrolling can be passed to this class even if the - * vertical scroll is less than the the nested list's touch slop. - */ - private final int mReleaseScrollSlop; - - /** - * Callbacks for interacting with sliding panes. - */ - public interface PanelSlideCallbacks { - /** - * Called when a sliding pane's position changes. - * @param panel The child view that was moved - * @param slideOffset The new offset of this sliding pane within its range, from 0-1 - */ - public void onPanelSlide(View panel, float slideOffset); - /** - * Called when a sliding pane becomes slid completely open. The pane may or may not - * be interactive at this point depending on how much of the pane is visible. - * @param panel The child view that was slid to an open position, revealing other panes - */ - public void onPanelOpened(View panel); - - /** - * Called when a sliding pane becomes slid completely closed. The pane is now guaranteed - * to be interactive. It may now obscure other views in the layout. - * @param panel The child view that was slid to a closed position - */ - public void onPanelClosed(View panel); - - /** - * Called when a sliding pane is flung as far open/closed as it can be. - * @param velocityY Velocity of the panel once its fling goes as far as it can. - */ - public void onPanelFlingReachesEdge(int velocityY); - - /** - * Returns true if the second panel's contents haven't been scrolled at all. This value is - * used to determine whether or not we can fully expand the header on downwards scrolls. - * - * Instead of using this callback, it would be preferable to instead fully expand the header - * on a View#onNestedFlingOver() callback. The behavior would be nicer. Unfortunately, - * no such callback exists yet (b/17547693). - */ - public boolean isScrollableChildUnscrolled(); - } - - public OverlappingPaneLayout(Context context) { - this(context, null); - } - - public OverlappingPaneLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public OverlappingPaneLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - final float density = context.getResources().getDisplayMetrics().density; - mOverhangSize = (int) (DEFAULT_OVERHANG_SIZE * density + 0.5f); - - setWillNotDraw(false); - - ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate()); - - mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback()); - mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density); - - mReleaseScrollSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); - } - - /** - * Set an offset, somewhere in between the panel's fully closed state and fully opened state, - * where the panel can be temporarily pinned or opened up to. - * - * @param offset Offset in pixels - */ - public void setIntermediatePinnedOffset(int offset) { - mIntermediateOffset = offset; - } - - /** - * Set the view that can be used to start dragging the sliding pane. - */ - public void setCapturableView(View capturableView) { - mCapturableView = capturableView; - } - - public void setPanelSlideCallbacks(PanelSlideCallbacks listener) { - mPanelSlideCallbacks = listener; - } - - void dispatchOnPanelSlide(View panel) { - mPanelSlideCallbacks.onPanelSlide(panel, mSlideOffset); - } - - void dispatchOnPanelOpened(View panel) { - mPanelSlideCallbacks.onPanelOpened(panel); - sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - } - - void dispatchOnPanelClosed(View panel) { - mPanelSlideCallbacks.onPanelClosed(panel); - sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - } - - void updateObscuredViewsVisibility(View panel) { - final int startBound = getPaddingTop(); - final int endBound = getHeight() - getPaddingBottom(); - - final int leftBound = getPaddingLeft(); - final int rightBound = getWidth() - getPaddingRight(); - final int left; - final int right; - final int top; - final int bottom; - if (panel != null && viewIsOpaque(panel)) { - left = panel.getLeft(); - right = panel.getRight(); - top = panel.getTop(); - bottom = panel.getBottom(); - } else { - left = right = top = bottom = 0; - } - - for (int i = 0, childCount = getChildCount(); i < childCount; i++) { - final View child = getChildAt(i); - - if (child == panel) { - // There are still more children above the panel but they won't be affected. - break; - } - - final int clampedChildLeft = Math.max(leftBound, child.getLeft()); - final int clampedChildRight = Math.min(rightBound, child.getRight()); - final int clampedChildTop = Math.max(startBound, child.getTop()); - final int clampedChildBottom = Math.min(endBound, child.getBottom()); - - final int vis; - if (clampedChildLeft >= left && clampedChildTop >= top && - clampedChildRight <= right && clampedChildBottom <= bottom) { - vis = INVISIBLE; - } else { - vis = VISIBLE; - } - child.setVisibility(vis); - } - } - - void setAllChildrenVisible() { - for (int i = 0, childCount = getChildCount(); i < childCount; i++) { - final View child = getChildAt(i); - if (child.getVisibility() == INVISIBLE) { - child.setVisibility(VISIBLE); - } - } - } - - private static boolean viewIsOpaque(View v) { - if (ViewCompat.isOpaque(v)) return true; - - final Drawable bg = v.getBackground(); - if (bg != null) { - return bg.getOpacity() == PixelFormat.OPAQUE; - } - return false; - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - mFirstLayout = true; - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mFirstLayout = true; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - - int widthMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSize = MeasureSpec.getSize(widthMeasureSpec); - int heightMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSize = MeasureSpec.getSize(heightMeasureSpec); - - if (widthMode != MeasureSpec.EXACTLY) { - if (isInEditMode()) { - // Don't crash the layout editor. Consume all of the space if specified - // or pick a magic number from thin air otherwise. - // TODO Better communication with tools of this bogus state. - // It will crash on a real device. - if (widthMode == MeasureSpec.AT_MOST) { - widthMode = MeasureSpec.EXACTLY; - } else if (widthMode == MeasureSpec.UNSPECIFIED) { - widthMode = MeasureSpec.EXACTLY; - widthSize = 300; - } - } else { - throw new IllegalStateException("Width must have an exact value or MATCH_PARENT"); - } - } else if (heightMode == MeasureSpec.UNSPECIFIED) { - if (isInEditMode()) { - // Don't crash the layout editor. Pick a magic number from thin air instead. - // TODO Better communication with tools of this bogus state. - // It will crash on a real device. - if (heightMode == MeasureSpec.UNSPECIFIED) { - heightMode = MeasureSpec.AT_MOST; - heightSize = 300; - } - } else { - throw new IllegalStateException("Height must not be UNSPECIFIED"); - } - } - - int layoutWidth = 0; - int maxLayoutWidth = -1; - switch (widthMode) { - case MeasureSpec.EXACTLY: - layoutWidth = maxLayoutWidth = widthSize - getPaddingLeft() - getPaddingRight(); - break; - case MeasureSpec.AT_MOST: - maxLayoutWidth = widthSize - getPaddingLeft() - getPaddingRight(); - break; - } - - float weightSum = 0; - boolean canSlide = false; - final int heightAvailable = heightSize - getPaddingTop() - getPaddingBottom(); - int heightRemaining = heightAvailable; - final int childCount = getChildCount(); - - if (childCount > 2) { - Log.e(TAG, "onMeasure: More than two child views are not supported."); - } - - // We'll find the current one below. - mSlideableView = null; - - // First pass. Measure based on child LayoutParams width/height. - // Weight will incur a second pass. - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - - if (child.getVisibility() == GONE) { - continue; - } - - if (lp.weight > 0) { - weightSum += lp.weight; - - // If we have no height, weight is the only contributor to the final size. - // Measure this view on the weight pass only. - if (lp.height == 0) continue; - } - - int childHeightSpec; - final int verticalMargin = lp.topMargin + lp.bottomMargin; - if (lp.height == LayoutParams.WRAP_CONTENT) { - childHeightSpec = MeasureSpec.makeMeasureSpec(heightAvailable - verticalMargin, - MeasureSpec.AT_MOST); - } else if (lp.height == LayoutParams.MATCH_PARENT) { - childHeightSpec = MeasureSpec.makeMeasureSpec(heightAvailable - verticalMargin, - MeasureSpec.EXACTLY); - } else { - childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); - } - - int childWidthSpec; - if (lp.width == LayoutParams.WRAP_CONTENT) { - childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth, MeasureSpec.AT_MOST); - } else if (lp.width == LayoutParams.MATCH_PARENT) { - childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth, MeasureSpec.EXACTLY); - } else { - childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); - } - - child.measure(childWidthSpec, childHeightSpec); - final int childWidth = child.getMeasuredWidth(); - final int childHeight = child.getMeasuredHeight(); - - if (widthMode == MeasureSpec.AT_MOST && childWidth > layoutWidth) { - layoutWidth = Math.min(childWidth, maxLayoutWidth); - } - - heightRemaining -= childHeight; - canSlide |= lp.slideable = heightRemaining < 0; - if (lp.slideable) { - mSlideableView = child; - } - } - - // Resolve weight and make sure non-sliding panels are smaller than the full screen. - if (canSlide || weightSum > 0) { - final int fixedPanelHeightLimit = heightAvailable - mOverhangSize; - - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - - if (child.getVisibility() == GONE) { - continue; - } - - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - - if (child.getVisibility() == GONE) { - continue; - } - - final boolean skippedFirstPass = lp.height == 0 && lp.weight > 0; - final int measuredHeight = skippedFirstPass ? 0 : child.getMeasuredHeight(); - if (canSlide && child != mSlideableView) { - if (lp.height < 0 && (measuredHeight > fixedPanelHeightLimit || lp.weight > 0)) { - // Fixed panels in a sliding configuration should - // be clamped to the fixed panel limit. - final int childWidthSpec; - if (skippedFirstPass) { - // Do initial width measurement if we skipped measuring this view - // the first time around. - if (lp.width == LayoutParams.WRAP_CONTENT) { - childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth, - MeasureSpec.AT_MOST); - } else if (lp.height == LayoutParams.MATCH_PARENT) { - childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth, - MeasureSpec.EXACTLY); - } else { - childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, - MeasureSpec.EXACTLY); - } - } else { - childWidthSpec = MeasureSpec.makeMeasureSpec( - child.getMeasuredWidth(), MeasureSpec.EXACTLY); - } - final int childHeightSpec = MeasureSpec.makeMeasureSpec( - fixedPanelHeightLimit, MeasureSpec.EXACTLY); - child.measure(childWidthSpec, childHeightSpec); - } - } else if (lp.weight > 0) { - int childWidthSpec; - if (lp.height == 0) { - // This was skipped the first time; figure out a real width spec. - if (lp.width == LayoutParams.WRAP_CONTENT) { - childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth, - MeasureSpec.AT_MOST); - } else if (lp.width == LayoutParams.MATCH_PARENT) { - childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth, - MeasureSpec.EXACTLY); - } else { - childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, - MeasureSpec.EXACTLY); - } - } else { - childWidthSpec = MeasureSpec.makeMeasureSpec( - child.getMeasuredWidth(), MeasureSpec.EXACTLY); - } - - if (canSlide) { - // Consume available space - final int verticalMargin = lp.topMargin + lp.bottomMargin; - final int newHeight = heightAvailable - verticalMargin; - final int childHeightSpec = MeasureSpec.makeMeasureSpec( - newHeight, MeasureSpec.EXACTLY); - if (measuredHeight != newHeight) { - child.measure(childWidthSpec, childHeightSpec); - } - } else { - // Distribute the extra width proportionally similar to LinearLayout - final int heightToDistribute = Math.max(0, heightRemaining); - final int addedHeight = (int) (lp.weight * heightToDistribute / weightSum); - final int childHeightSpec = MeasureSpec.makeMeasureSpec( - measuredHeight + addedHeight, MeasureSpec.EXACTLY); - child.measure(childWidthSpec, childHeightSpec); - } - } - } - } - - final int measuredHeight = heightSize; - final int measuredWidth = layoutWidth + getPaddingLeft() + getPaddingRight(); - - setMeasuredDimension(measuredWidth, measuredHeight); - mCanSlide = canSlide; - - if (mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE && !canSlide) { - // Cancel scrolling in progress, it's no longer relevant. - mDragHelper.abort(); - } - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_TOP); - - final int height = b - t; - final int paddingTop = getPaddingTop(); - final int paddingBottom = getPaddingBottom(); - final int paddingLeft = getPaddingLeft(); - - final int childCount = getChildCount(); - int yStart = paddingTop; - int nextYStart = yStart; - - if (mFirstLayout) { - mSlideOffset = mCanSlide && mPreservedOpenState ? 1.f : 0.f; - } - - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - - if (child.getVisibility() == GONE) { - continue; - } - - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - - final int childHeight = child.getMeasuredHeight(); - - if (lp.slideable) { - final int margin = lp.topMargin + lp.bottomMargin; - final int range = Math.min(nextYStart, - height - paddingBottom - mOverhangSize) - yStart - margin; - mSlideRange = range; - final int lpMargin = lp.topMargin; - final int pos = (int) (range * mSlideOffset); - yStart += pos + lpMargin; - updateSlideOffset(pos); - } else { - yStart = nextYStart; - } - - final int childTop = yStart; - final int childBottom = childTop + childHeight; - final int childLeft = paddingLeft; - final int childRight = childLeft + child.getMeasuredWidth(); - - child.layout(childLeft, childTop, childRight, childBottom); - - nextYStart += child.getHeight(); - } - - if (mFirstLayout) { - updateObscuredViewsVisibility(mSlideableView); - } - - mFirstLayout = false; - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - // Recalculate sliding panes and their details - if (h != oldh) { - mFirstLayout = true; - } - } - - @Override - public void requestChildFocus(View child, View focused) { - super.requestChildFocus(child, focused); - if (!isInTouchMode() && !mCanSlide) { - mPreservedOpenState = child == mSlideableView; - } - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - final int action = MotionEventCompat.getActionMasked(ev); - - // Preserve the open state based on the last view that was touched. - if (!mCanSlide && action == MotionEvent.ACTION_DOWN && getChildCount() > 1) { - // After the first things will be slideable. - final View secondChild = getChildAt(1); - if (secondChild != null) { - mPreservedOpenState = !mDragHelper.isViewUnder(secondChild, - (int) ev.getX(), (int) ev.getY()); - } - } - - if (!mCanSlide || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) { - if (!mIsInNestedScroll) { - mDragHelper.cancel(); - } - return super.onInterceptTouchEvent(ev); - } - - if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { - if (!mIsInNestedScroll) { - mDragHelper.cancel(); - } - return false; - } - - switch (action) { - case MotionEvent.ACTION_DOWN: { - mIsUnableToDrag = false; - final float x = ev.getX(); - final float y = ev.getY(); - mInitialMotionX = x; - mInitialMotionY = y; - - break; - } - - case MotionEvent.ACTION_MOVE: { - final float x = ev.getX(); - final float y = ev.getY(); - final float adx = Math.abs(x - mInitialMotionX); - final float ady = Math.abs(y - mInitialMotionY); - final int slop = mDragHelper.getTouchSlop(); - if (ady > slop && adx > ady || !isCapturableViewUnder((int) x, (int) y)) { - if (!mIsInNestedScroll) { - mDragHelper.cancel(); - } - mIsUnableToDrag = true; - return false; - } - } - } - - final boolean interceptForDrag = mDragHelper.shouldInterceptTouchEvent(ev); - - return interceptForDrag; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (!mCanSlide) { - return super.onTouchEvent(ev); - } - - mDragHelper.processTouchEvent(ev); - - final int action = ev.getAction(); - boolean wantTouchEvents = true; - - switch (action & MotionEventCompat.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: { - final float x = ev.getX(); - final float y = ev.getY(); - mInitialMotionX = x; - mInitialMotionY = y; - break; - } - } - - return wantTouchEvents; - } - - /** - * Refreshes the {@link OverlappingPaneLayout} be attempting to re-open or re-close the pane. - * This ensures that the overlapping pane is repositioned based on any changes to the view - * which is being overlapped. - * <p> - * The {@link #openPane()} and {@link #closePane()} methods do not perform any animation if the - * pane has already been positioned appropriately. - */ - public void refresh() { - if (isOpen()) { - openPane(); - } else { - closePane(); - } - } - - private boolean closePane(View pane, int initialVelocity) { - if (mFirstLayout || smoothSlideTo(0.f, initialVelocity)) { - mPreservedOpenState = false; - return true; - } - return false; - } - - private boolean openPane(View pane, int initialVelocity) { - if (mFirstLayout || smoothSlideTo(1.f, initialVelocity)) { - mPreservedOpenState = true; - return true; - } - return false; - } - - private void updateSlideOffset(int offsetPx) { - mSlideOffsetPx = offsetPx; - mSlideOffset = (float) mSlideOffsetPx / mSlideRange; - } - - /** - * Open the sliding pane if it is currently slideable. If first layout - * has already completed this will animate. - * - * @return true if the pane was slideable and is now open/in the process of opening - */ - public boolean openPane() { - return openPane(mSlideableView, 0); - } - - /** - * Close the sliding pane if it is currently slideable. If first layout - * has already completed this will animate. - * - * @return true if the pane was slideable and is now closed/in the process of closing - */ - public boolean closePane() { - return closePane(mSlideableView, 0); - } - - /** - * Check if the layout is open. It can be open either because the slider - * itself is open revealing the left pane, or if all content fits without sliding. - * - * @return true if sliding panels are open - */ - public boolean isOpen() { - return !mCanSlide || mSlideOffset > 0; - } - - /** - * Check if the content in this layout cannot fully fit side by side and therefore - * the content pane can be slid back and forth. - * - * @return true if content in this layout can be slid open and closed - */ - public boolean isSlideable() { - return mCanSlide; - } - - private void onPanelDragged(int newTop) { - if (mSlideableView == null) { - // This can happen if we're aborting motion during layout because everything now fits. - mSlideOffset = 0; - return; - } - final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); - - final int lpMargin = lp.topMargin; - final int topBound = getPaddingTop() + lpMargin; - - updateSlideOffset(newTop - topBound); - - dispatchOnPanelSlide(mSlideableView); - } - - @Override - protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - boolean result; - final int save = canvas.save(Canvas.CLIP_SAVE_FLAG); - - if (mCanSlide && !lp.slideable && mSlideableView != null) { - // Clip against the slider; no sense drawing what will immediately be covered. - canvas.getClipBounds(mTmpRect); - - mTmpRect.bottom = Math.min(mTmpRect.bottom, mSlideableView.getTop()); - canvas.clipRect(mTmpRect); - } - - if (Build.VERSION.SDK_INT >= 11) { // HC - result = super.drawChild(canvas, child, drawingTime); - } else { - if (child.isDrawingCacheEnabled()) { - child.setDrawingCacheEnabled(false); - } - result = super.drawChild(canvas, child, drawingTime); - } - - canvas.restoreToCount(save); - - return result; - } - - /** - * Smoothly animate mDraggingPane to the target X position within its range. - * - * @param slideOffset position to animate to - * @param velocity initial velocity in case of fling, or 0. - */ - boolean smoothSlideTo(float slideOffset, int velocity) { - if (!mCanSlide) { - // Nothing to do. - return false; - } - - final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); - - int y; - int topBound = getPaddingTop() + lp.topMargin; - y = (int) (topBound + slideOffset * mSlideRange); - - if (mDragHelper.smoothSlideViewTo(mSlideableView, mSlideableView.getLeft(), y)) { - setAllChildrenVisible(); - ViewCompat.postInvalidateOnAnimation(this); - return true; - } - return false; - } - - @Override - public void computeScroll() { - if (mDragHelper.continueSettling(/* deferCallbacks = */ false)) { - if (!mCanSlide) { - mDragHelper.abort(); - return; - } - - ViewCompat.postInvalidateOnAnimation(this); - } - } - - private boolean isCapturableViewUnder(int x, int y) { - View capturableView = mCapturableView != null ? mCapturableView : mSlideableView; - if (capturableView == null) { - return false; - } - int[] viewLocation = new int[2]; - capturableView.getLocationOnScreen(viewLocation); - int[] parentLocation = new int[2]; - this.getLocationOnScreen(parentLocation); - int screenX = parentLocation[0] + x; - int screenY = parentLocation[1] + y; - return screenX >= viewLocation[0] - && screenX < viewLocation[0] + capturableView.getWidth() - && screenY >= viewLocation[1] - && screenY < viewLocation[1] + capturableView.getHeight(); - } - - @Override - protected ViewGroup.LayoutParams generateDefaultLayoutParams() { - return new LayoutParams(); - } - - @Override - protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { - return p instanceof MarginLayoutParams - ? new LayoutParams((MarginLayoutParams) p) - : new LayoutParams(p); - } - - @Override - protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { - return p instanceof LayoutParams && super.checkLayoutParams(p); - } - - @Override - public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { - return new LayoutParams(getContext(), attrs); - } - - @Override - protected Parcelable onSaveInstanceState() { - Parcelable superState = super.onSaveInstanceState(); - - SavedState ss = new SavedState(superState); - ss.isOpen = isSlideable() ? isOpen() : mPreservedOpenState; - - return ss; - } - - @Override - protected void onRestoreInstanceState(Parcelable state) { - SavedState ss = (SavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - - if (ss.isOpen) { - openPane(); - } else { - closePane(); - } - mPreservedOpenState = ss.isOpen; - } - - @Override - public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { - final boolean startNestedScroll = (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0; - if (startNestedScroll) { - mIsInNestedScroll = true; - mDragHelper.startNestedScroll(mSlideableView); - } - if (DEBUG) { - Log.d(TAG, "onStartNestedScroll: " + startNestedScroll); - } - return startNestedScroll; - } - - @Override - public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { - if (dy == 0) { - // Nothing to do - return; - } - if (DEBUG) { - Log.d(TAG, "onNestedPreScroll: " + dy); - } - - mInNestedPreScrollDownwards = dy < 0; - mInNestedPreScrollUpwards = dy > 0; - mIsInNestedFling = false; - mDragHelper.processNestedScroll(mSlideableView, 0, -dy, consumed); - } - - @Override - public boolean onNestedPreFling(View target, float velocityX, float velocityY) { - if (!(velocityY > 0 && mSlideOffsetPx != 0 - || velocityY < 0 && mSlideOffsetPx < mIntermediateOffset - || velocityY < 0 && mSlideOffsetPx < mSlideRange - && mPanelSlideCallbacks.isScrollableChildUnscrolled())) { - // No need to consume the fling if the fling won't collapse or expand the header. - // How far we are willing to expand the header depends on isScrollableChildUnscrolled(). - return false; - } - - if (DEBUG) { - Log.d(TAG, "onNestedPreFling: " + velocityY); - } - mInUpwardsPreFling = velocityY > 0; - mIsInNestedFling = true; - mIsInNestedScroll = false; - mDragHelper.processNestedFling(mSlideableView, (int) -velocityY); - return true; - } - - @Override - public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, - int dyUnconsumed) { - if (DEBUG) { - Log.d(TAG, "onNestedScroll: " + dyUnconsumed); - } - mIsInNestedFling = false; - mDragHelper.processNestedScroll(mSlideableView, 0, -dyUnconsumed, null); - } - - @Override - public void onStopNestedScroll(View child) { - if (DEBUG) { - Log.d(TAG, "onStopNestedScroll"); - } - if (mIsInNestedScroll && !mIsInNestedFling) { - mDragHelper.stopNestedScroll(mSlideableView); - mInNestedPreScrollDownwards = false; - mInNestedPreScrollUpwards = false; - mIsInNestedScroll = false; - } - } - - private class DragHelperCallback extends ViewDragHelper.Callback { - - @Override - public boolean tryCaptureView(View child, int pointerId) { - if (mIsUnableToDrag) { - return false; - } - - return ((LayoutParams) child.getLayoutParams()).slideable; - } - - @Override - public void onViewDragStateChanged(int state) { - if (DEBUG) { - Log.d(TAG, "onViewDragStateChanged: " + state); - } - - if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) { - if (mSlideOffset == 0) { - updateObscuredViewsVisibility(mSlideableView); - dispatchOnPanelClosed(mSlideableView); - mPreservedOpenState = false; - } else { - dispatchOnPanelOpened(mSlideableView); - mPreservedOpenState = true; - } - } - - if (state == ViewDragHelper.STATE_IDLE - && mDragHelper.getVelocityMagnitude() > 0 - && mIsInNestedFling) { - mIsInNestedFling = false; - final int flingVelocity = !mInUpwardsPreFling ? - -mDragHelper.getVelocityMagnitude() : mDragHelper.getVelocityMagnitude(); - mPanelSlideCallbacks.onPanelFlingReachesEdge(flingVelocity); - } - } - - @Override - public void onViewCaptured(View capturedChild, int activePointerId) { - // Make all child views visible in preparation for sliding things around - setAllChildrenVisible(); - } - - @Override - public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { - onPanelDragged(top); - invalidate(); - } - - @Override - public void onViewFling(View releasedChild, float xVelocity, float yVelocity) { - if (releasedChild == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "onViewFling: " + yVelocity); - } - - // Flings won't always fully expand or collapse the header. Instead of performing the - // fling and then waiting for the fling to end before snapping into place, we - // immediately snap into place if we predict the fling won't fully expand or collapse - // the header. - int yOffsetPx = mDragHelper.predictFlingYOffset((int) yVelocity); - if (yVelocity < 0) { - // Only perform a fling if we know the fling will fully compress the header. - if (-yOffsetPx > mSlideOffsetPx) { - mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0, - mSlideRange, Integer.MAX_VALUE, (int) yVelocity); - } else { - mIsInNestedFling = false; - onViewReleased(releasedChild, xVelocity, yVelocity); - } - } else { - // Only perform a fling if we know the fling will expand the header as far - // as it can possible be expanded, given the isScrollableChildUnscrolled() value. - if (yOffsetPx + mSlideOffsetPx >= mSlideRange - && mPanelSlideCallbacks.isScrollableChildUnscrolled()) { - mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0, - Integer.MAX_VALUE, mSlideRange, (int) yVelocity); - } else if (yOffsetPx + mSlideOffsetPx >= mIntermediateOffset - && mSlideOffsetPx <= mIntermediateOffset - && !mPanelSlideCallbacks.isScrollableChildUnscrolled()) { - mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0, - Integer.MAX_VALUE, mIntermediateOffset, (int) yVelocity); - } else { - mIsInNestedFling = false; - onViewReleased(releasedChild, xVelocity, yVelocity); - } - } - - mInNestedPreScrollDownwards = false; - mInNestedPreScrollUpwards = false; - - // Without this invalidate, some calls to flingCapturedView can have no affect. - invalidate(); - } - - @Override - public void onViewReleased(View releasedChild, float xvel, float yvel) { - if (DEBUG) { - Log.d(TAG, "onViewReleased: " - + " mIsInNestedFling=" + mIsInNestedFling - + " unscrolled=" + mPanelSlideCallbacks.isScrollableChildUnscrolled() - + ", mInNestedPreScrollDownwards = " + mInNestedPreScrollDownwards - + ", mInNestedPreScrollUpwards = " + mInNestedPreScrollUpwards - + ", yvel=" + yvel); - } - if (releasedChild == null) { - return; - } - - final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams(); - int top = getPaddingTop() + lp.topMargin; - - // Decide where to snap to according to the current direction of motion and the current - // position. The velocity's magnitude has no bearing on this. - if (mInNestedPreScrollDownwards || yvel > 0) { - // Scrolling downwards - if (mSlideOffsetPx > mIntermediateOffset + mReleaseScrollSlop) { - top += mSlideRange; - } else if (mSlideOffsetPx > mReleaseScrollSlop) { - top += mIntermediateOffset; - } else { - // Offset is very close to 0 - } - } else if (mInNestedPreScrollUpwards || yvel < 0) { - // Scrolling upwards - if (mSlideOffsetPx > mSlideRange - mReleaseScrollSlop) { - // Offset is very close to mSlideRange - top += mSlideRange; - } else if (mSlideOffsetPx > mIntermediateOffset - mReleaseScrollSlop) { - // Offset is between mIntermediateOffset and mSlideRange. - top += mIntermediateOffset; - } else { - // Offset is between 0 and mIntermediateOffset. - } - } else { - // Not moving upwards or downwards. This case can only be triggered when directly - // dragging the tabs. We don't bother to remember previous scroll direction - // when directly dragging the tabs. - if (0 <= mSlideOffsetPx && mSlideOffsetPx <= mIntermediateOffset / 2) { - // Offset is between 0 and mIntermediateOffset, but closer to 0 - // Leave top unchanged - } else if (mIntermediateOffset / 2 <= mSlideOffsetPx - && mSlideOffsetPx <= (mIntermediateOffset + mSlideRange) / 2) { - // Offset is closest to mIntermediateOffset - top += mIntermediateOffset; - } else { - // Offset is between mIntermediateOffset and mSlideRange, but closer to - // mSlideRange - top += mSlideRange; - } - } - - mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top); - invalidate(); - } - - @Override - public int getViewVerticalDragRange(View child) { - return mSlideRange; - } - - @Override - public int clampViewPositionHorizontal(View child, int left, int dx) { - // Make sure we never move views horizontally. - return child.getLeft(); - } - - @Override - public int clampViewPositionVertical(View child, int top, int dy) { - final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); - - final int newTop; - int previousTop = top - dy; - int topBound = getPaddingTop() + lp.topMargin; - int bottomBound = topBound + (mPanelSlideCallbacks.isScrollableChildUnscrolled() - || !mIsInNestedScroll ? mSlideRange : mIntermediateOffset); - if (previousTop > bottomBound) { - // We were previously below the bottomBound, so loosen the bottomBound so that this - // makes sense. This can occur after the view was directly dragged by the tabs. - bottomBound = Math.max(bottomBound, mSlideRange); - } - newTop = Math.min(Math.max(top, topBound), bottomBound); - - return newTop; - } - - @Override - public void onEdgeDragStarted(int edgeFlags, int pointerId) { - mDragHelper.captureChildView(mSlideableView, pointerId); - } - } - - public static class LayoutParams extends ViewGroup.MarginLayoutParams { - private static final int[] ATTRS = new int[] { - android.R.attr.layout_weight - }; - - /** - * The weighted proportion of how much of the leftover space - * this child should consume after measurement. - */ - public float weight = 0; - - /** - * True if this pane is the slideable pane in the layout. - */ - boolean slideable; - - public LayoutParams() { - super(FILL_PARENT, FILL_PARENT); - } - - public LayoutParams(int width, int height) { - super(width, height); - } - - public LayoutParams(android.view.ViewGroup.LayoutParams source) { - super(source); - } - - public LayoutParams(MarginLayoutParams source) { - super(source); - } - - public LayoutParams(LayoutParams source) { - super(source); - this.weight = source.weight; - } - - public LayoutParams(Context c, AttributeSet attrs) { - super(c, attrs); - - final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS); - this.weight = a.getFloat(0, 0); - a.recycle(); - } - - } - - static class SavedState extends BaseSavedState { - boolean isOpen; - - SavedState(Parcelable superState) { - super(superState); - } - - private SavedState(Parcel in) { - super(in); - isOpen = in.readInt() != 0; - } - - @Override - public void writeToParcel(Parcel out, int flags) { - super.writeToParcel(out, flags); - out.writeInt(isOpen ? 1 : 0); - } - - public static final Parcelable.Creator<SavedState> CREATOR = - new Parcelable.Creator<SavedState>() { - public SavedState createFromParcel(Parcel in) { - return new SavedState(in); - } - - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - } - - class AccessibilityDelegate extends AccessibilityDelegateCompat { - private final Rect mTmpRect = new Rect(); - - @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { - final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info); - super.onInitializeAccessibilityNodeInfo(host, superNode); - copyNodeInfoNoChildren(info, superNode); - superNode.recycle(); - - info.setClassName(OverlappingPaneLayout.class.getName()); - info.setSource(host); - - final ViewParent parent = ViewCompat.getParentForAccessibility(host); - if (parent instanceof View) { - info.setParent((View) parent); - } - - // This is a best-approximation of addChildrenForAccessibility() - // that accounts for filtering. - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - if (child.getVisibility() == View.VISIBLE) { - // Force importance to "yes" since we can't read the value. - ViewCompat.setImportantForAccessibility( - child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); - info.addChild(child); - } - } - } - - @Override - public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(host, event); - - event.setClassName(OverlappingPaneLayout.class.getName()); - } - - /** - * This should really be in AccessibilityNodeInfoCompat, but there unfortunately - * seem to be a few elements that are not easily cloneable using the underlying API. - * Leave it private here as it's not general-purpose useful. - */ - private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, - AccessibilityNodeInfoCompat src) { - final Rect rect = mTmpRect; - - src.getBoundsInParent(rect); - dest.setBoundsInParent(rect); - - src.getBoundsInScreen(rect); - dest.setBoundsInScreen(rect); - - dest.setVisibleToUser(src.isVisibleToUser()); - dest.setPackageName(src.getPackageName()); - dest.setClassName(src.getClassName()); - dest.setContentDescription(src.getContentDescription()); - - dest.setEnabled(src.isEnabled()); - dest.setClickable(src.isClickable()); - dest.setFocusable(src.isFocusable()); - dest.setFocused(src.isFocused()); - dest.setAccessibilityFocused(src.isAccessibilityFocused()); - dest.setSelected(src.isSelected()); - dest.setLongClickable(src.isLongClickable()); - - dest.addAction(src.getActions()); - - dest.setMovementGranularities(src.getMovementGranularities()); - } - } -} diff --git a/src/com/android/dialerbind/ObjectFactory.java b/src/com/android/dialerbind/ObjectFactory.java index 54a5821df..dfacd3f6d 100644 --- a/src/com/android/dialerbind/ObjectFactory.java +++ b/src/com/android/dialerbind/ObjectFactory.java @@ -42,15 +42,15 @@ public class ObjectFactory { * @param context The context to use. * @param callFetcher Instance of call fetcher to use. * @param contactInfoHelper Instance of contact info helper class to use. - * @param isCallLog Is this call log adapter being used on the call log? * @return Instance of CallLogAdapter. */ - public static CallLogAdapter newCallLogAdapter(Context context, - CallFetcher callFetcher, ContactInfoHelper contactInfoHelper, - CallItemExpandedListener callItemExpandedListener, - OnReportButtonClickListener onReportButtonClickListener, boolean isCallLog) { - return new CallLogAdapter(context, callFetcher, contactInfoHelper, - callItemExpandedListener, onReportButtonClickListener, isCallLog); + public static CallLogAdapter newCallLogAdapter( + Context context, + CallFetcher callFetcher, + ContactInfoHelper contactInfoHelper, + OnReportButtonClickListener onReportButtonClickListener) { + return new CallLogAdapter( + context, callFetcher, contactInfoHelper, onReportButtonClickListener); } public static DialogFragment getReportDialogFragment(String number) { |