diff options
21 files changed, 312 insertions, 45 deletions
diff --git a/InCallUI/src/com/android/incallui/StatusBarNotifier.java b/InCallUI/src/com/android/incallui/StatusBarNotifier.java index 1a4ec0935..315c0d0ae 100644 --- a/InCallUI/src/com/android/incallui/StatusBarNotifier.java +++ b/InCallUI/src/com/android/incallui/StatusBarNotifier.java @@ -315,7 +315,9 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener, audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC); audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE); notification.audioAttributes = audioAttributes.build(); - notification.vibrate = VIBRATE_PATTERN; + if (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) { + notification.vibrate = VIBRATE_PATTERN; + } } if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) { Log.v(this, "Playing call waiting tone"); diff --git a/InCallUI/src/com/android/incallui/ringtone/DialerRingtoneManager.java b/InCallUI/src/com/android/incallui/ringtone/DialerRingtoneManager.java index 29d3d9de3..39844e5a2 100644 --- a/InCallUI/src/com/android/incallui/ringtone/DialerRingtoneManager.java +++ b/InCallUI/src/com/android/incallui/ringtone/DialerRingtoneManager.java @@ -18,7 +18,9 @@ package com.android.incallui.ringtone; import com.google.common.base.Preconditions; +import android.content.ContentResolver; import android.net.Uri; +import android.provider.Settings; import android.support.annotation.Nullable; import com.android.contacts.common.compat.CompatUtils; @@ -70,6 +72,17 @@ public class DialerRingtoneManager { } /** + * Determines if an incoming call should vibrate as well as ring. + * + * @param resolver {@link ContentResolver} used to look up the + * {@link Settings.System#VIBRATE_WHEN_RINGING} setting. + * @return {@code true} if the call should vibrate, {@code false} otherwise. + */ + public boolean shouldVibrate(ContentResolver resolver) { + return Settings.System.getInt(resolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0; + } + + /** * The incoming callState is never set as {@link State#CALL_WAITING} because * {@link Call#translateState(int)} doesn't account for that case, check for it here */ diff --git a/res/drawable-hdpi/ic_archive_white_24dp.png b/res/drawable-hdpi/ic_archive_white_24dp.png Binary files differnew file mode 100644 index 000000000..bb72e890f --- /dev/null +++ b/res/drawable-hdpi/ic_archive_white_24dp.png diff --git a/res/drawable-mdpi/ic_archive_white_24dp.png b/res/drawable-mdpi/ic_archive_white_24dp.png Binary files differnew file mode 100644 index 000000000..f6aa3f966 --- /dev/null +++ b/res/drawable-mdpi/ic_archive_white_24dp.png diff --git a/res/drawable-xhdpi/ic_archive_white_24dp.png b/res/drawable-xhdpi/ic_archive_white_24dp.png Binary files differnew file mode 100644 index 000000000..3513bd9fe --- /dev/null +++ b/res/drawable-xhdpi/ic_archive_white_24dp.png diff --git a/res/drawable-xxhdpi/ic_archive_white_24dp.png b/res/drawable-xxhdpi/ic_archive_white_24dp.png Binary files differnew file mode 100644 index 000000000..00e04e42b --- /dev/null +++ b/res/drawable-xxhdpi/ic_archive_white_24dp.png diff --git a/res/drawable-xxxhdpi/ic_archive_white_24dp.png b/res/drawable-xxxhdpi/ic_archive_white_24dp.png Binary files differnew file mode 100644 index 000000000..34cd3fd80 --- /dev/null +++ b/res/drawable-xxxhdpi/ic_archive_white_24dp.png diff --git a/res/layout/phone_favorite_tile_view.xml b/res/layout/phone_favorite_tile_view.xml index 8b00fba61..aa82ca0dc 100644 --- a/res/layout/phone_favorite_tile_view.xml +++ b/res/layout/phone_favorite_tile_view.xml @@ -24,7 +24,8 @@ android:id="@+id/contact_favorite_card" android:layout_width="match_parent" android:layout_height="match_parent" - android:focusable="true" > + android:focusable="true" + android:nextFocusRight="@+id/contact_tile_secondary_button"> <com.android.contacts.common.widget.LayoutSuppressingImageView android:id="@+id/contact_tile_image" @@ -102,8 +103,7 @@ android:id="@+id/contact_tile_push_state" android:layout_width="match_parent" android:layout_height="match_parent" - android:focusable="true" - android:nextFocusRight="@+id/contact_tile_secondary_button" + android:importantForAccessibility="no" android:background="@drawable/item_background_material_dark" /> <ImageButton diff --git a/res/layout/voicemail_playback_layout.xml b/res/layout/voicemail_playback_layout.xml index bb7fe342a..64a68bdd2 100644 --- a/res/layout/voicemail_playback_layout.xml +++ b/res/layout/voicemail_playback_layout.xml @@ -97,6 +97,17 @@ android:tint="@color/voicemail_icon_tint" android:contentDescription="@string/call_log_trash_voicemail" /> + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1" /> + + <ImageButton android:id="@+id/archive_voicemail" + style="@style/VoicemailPlaybackLayoutButtonStyle" + android:src="@drawable/ic_archive_white_24dp" + android:tint="@color/voicemail_icon_tint" + android:contentDescription="@string/call_log_archive_voicemail" /> + </LinearLayout> </LinearLayout> diff --git a/res/menu/dialtacts_options.xml b/res/menu/dialtacts_options.xml index 8a9e25f7f..8e31026a7 100644 --- a/res/menu/dialtacts_options.xml +++ b/res/menu/dialtacts_options.xml @@ -20,6 +20,9 @@ android:icon="@drawable/ic_menu_history_lt" android:title="@string/action_menu_call_history_description" /> <item + android:id="@+id/menu_archive" + android:title="@string/voicemail_archive_activity_title" /> + <item android:id="@+id/menu_import_export" android:title="@string/menu_import_export" /> <item diff --git a/res/values/strings.xml b/res/values/strings.xml index 6d2bd5ef5..734dde899 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -84,12 +84,21 @@ <!-- Menu item used to delete a voicemail. [CHAR LIMIT=30] --> <string name="call_log_trash_voicemail">Delete voicemail</string> + <!-- Menu item used to archive a voicemail. [CHAR LIMIT=30] --> + <string name="call_log_archive_voicemail">Archive voicemail</string> + <!-- Text for snackbar to undo a voicemail delete. [CHAR LIMIT=30] --> <string name="snackbar_voicemail_deleted">Voicemail deleted</string> + <!-- Text for snackbar to undo a voicemail archive. [CHAR LIMIT=30] --> + <string name="snackbar_voicemail_archived">Voicemail archived</string> + <!-- Text for undo button in snackbar for voicemail deletion. [CHAR LIMIT=10] --> <string name="snackbar_voicemail_deleted_undo">UNDO</string> + <!-- Text for going to archive button in snackbar for voicemail archive. [CHAR LIMIT=10] --> + <string name="snackbar_voicemail_archived_goto">GOTO ARCHIVE</string> + <!-- Title of the confirmation dialog for clearing the call log. [CHAR LIMIT=37] --> <string name="clearCallLogConfirmation_title">Clear call history?</string> @@ -158,6 +167,9 @@ <!-- Message to display whilst we are waiting for the content to be fetched. [CHAR LIMIT=40] --> <string name="voicemail_fetching_content">Loading voicemail\u2026</string> + <!-- Message to display whilst we are waiting for the content to be archived. [CHAR LIMIT=40] --> + <string name="voicemail_archiving_content">Archiving voicemail\u2026</string> + <!-- Message to display if we fail to get content within a suitable time period. [CHAR LIMIT=40] --> <string name="voicemail_fetching_timout">Couldn\'t load voicemail</string> @@ -455,6 +467,12 @@ The date will be replaced by 'Today' for voicemails created on the current day. For example: Today at 2:49 PM --> <string name="voicemailCallLogDateTimeFormat"><xliff:g id="date" example="Jul 25, 2014">%1$s</xliff:g> at <xliff:g id="time" example="2:49 PM">%2$s</xliff:g></string> + <!-- Format for duration of voicemails which are displayed when viewing voicemail logs. For example "01:22" --> + <string name="voicemailDurationFormat"><xliff:g id="minutes" example="10">%1$02d</xliff:g>:<xliff:g id="seconds" example="20">%2$02d</xliff:g></string> + + <!-- A format string used for displaying the date, time and duration for a voicemail call log. For example: Jul 25, 2014 at 2:49 PM • 00:34 --> + <string name="voicemailCallLogDateTimeFormatWithDuration"><xliff:g id="dateAndTime" example="Jul 25, 2014 at 2:49PM">%1$s</xliff:g> \u2022 <xliff:g id="duration" example="01:22">%2$s</xliff:g></string> + <!-- Dialog message which is shown when the user tries to make a phone call to prohibited phone numbers [CHAR LIMIT=NONE] --> <string name="dialog_phone_call_prohibited_message" msgid="4313552620858880999">Can\'t call this number</string> @@ -525,12 +543,18 @@ <!-- Text displayed when the list of voicemails is empty --> <string name="call_log_voicemail_empty">Your voicemail inbox is empty.</string> + <!-- Text displayed when the list of voicemail archives is empty --> + <string name="voicemail_archive_empty">Your voicemail archive is empty.</string> + <!-- Menu option to show favorite contacts only --> <string name="show_favorites_only">Show favorites only</string> <!-- Title of activity that displays a list of all calls --> <string name="call_log_activity_title">Call History</string> + <!-- Title of activity that displays a list of all archived voicemails --> + <string name="voicemail_archive_activity_title">Voicemail Archive</string> + <!-- Title for the call log tab containing the list of all voicemails and calls [CHAR LIMIT=30] --> <string name="call_log_all_title">All</string> @@ -911,6 +935,9 @@ <!-- Error toast message for when send to voicemail import fails. [CHAR LIMIT=40] --> <string name="send_to_voicemail_import_failed">Import failed</string> + <!-- Error toast message for when voicemail archive fails. [CHAR LIMIT=40] --> + <string name="voicemail_archive_failed">Failed to archive voicemail.</string> + <!-- String describing the delete icon on a blocked number list item. When tapped, it will show a dialog confirming the unblocking of the number. [CHAR LIMIT=NONE]--> diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java index d50748303..d12cf24df 100644 --- a/src/com/android/dialer/DialtactsActivity.java +++ b/src/com/android/dialer/DialtactsActivity.java @@ -240,6 +240,7 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O private FloatingActionButtonController mFloatingActionButtonController; private int mActionBarHeight; + private int mPreviouslySelectedTabIndex; /** * The text returned from a voice search query. Set in {@link #onActivityResult} and used in @@ -423,7 +424,7 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O mIsLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; - + mPreviouslySelectedTabIndex = ListsFragment.TAB_INDEX_SPEED_DIAL; final View floatingActionButtonContainer = findViewById( R.id.floating_action_button_container); ImageButton floatingActionButton = (ImageButton) findViewById(R.id.floating_action_button); @@ -567,6 +568,11 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O @Override protected void onPause() { + // Only clear missed calls if the pause was not triggered by an orientation change + // (or any other confirguration change) + if (!isChangingConfigurations()) { + updateMissedCalls(); + } if (mClearSearchOnPause) { hideDialpadAndSearchUi(); mClearSearchOnPause = false; @@ -1328,7 +1334,9 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O @Override public void onPageSelected(int position) { + updateMissedCalls(); int tabIndex = mListsFragment.getCurrentTabIndex(); + mPreviouslySelectedTabIndex = tabIndex; if (tabIndex == ListsFragment.TAB_INDEX_ALL_CONTACTS) { mFloatingActionButtonController.changeIcon( getResources().getDrawable(R.drawable.ic_person_add_24dp), @@ -1389,4 +1397,10 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O } return FloatingActionButtonController.ALIGN_END; } + + private void updateMissedCalls() { + if (mPreviouslySelectedTabIndex == ListsFragment.TAB_INDEX_HISTORY) { + mListsFragment.markMissedCallsAsReadAndRemoveNotifications(); + } + } } diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java index 372c7bd9b..6f96ee529 100644 --- a/src/com/android/dialer/calllog/CallLogAdapter.java +++ b/src/com/android/dialer/calllog/CallLogAdapter.java @@ -16,6 +16,7 @@ package com.android.dialer.calllog; +import com.android.dialer.DialtactsActivity; import com.google.common.annotations.VisibleForTesting; import android.content.Context; @@ -169,6 +170,12 @@ public class CallLogAdapter extends GroupingListAdapter mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; } else { + if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) { + CallLogAsyncTaskUtil.markCallAsRead(mContext, viewHolder.callIds); + if (!mIsCallLogActivity) { + ((DialtactsActivity) v.getContext()).updateTabUnreadCounts(); + } + } expandViewHolderActions(viewHolder); } @@ -506,7 +513,8 @@ public class CallLogAdapter extends GroupingListAdapter details.features = getCallFeatures(c, count); details.geocode = c.getString(CallLogQuery.GEOCODED_LOCATION); details.transcription = c.getString(CallLogQuery.TRANSCRIPTION); - if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE) { + if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE || + details.callTypes[0] == CallLog.Calls.MISSED_TYPE) { details.isRead = c.getInt(CallLogQuery.IS_READ) == 1; } diff --git a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java index bb7bdbd0a..982591814 100644 --- a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java +++ b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java @@ -29,6 +29,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.util.PermissionsUtil; import com.android.dialer.DialtactsActivity; import com.android.dialer.PhoneCallDetails; import com.android.dialer.util.AppCompatConstants; @@ -51,7 +52,9 @@ public class CallLogAsyncTaskUtil { DELETE_CALL, DELETE_BLOCKED_CALL, MARK_VOICEMAIL_READ, + MARK_CALL_READ, GET_CALL_DETAILS, + UPDATE_DURATION } private static final class CallDetailQuery { @@ -348,8 +351,6 @@ public class CallLogAsyncTaskUtil { Intent intent = new Intent(context, CallLogNotificationsService.class); intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); context.startService(intent); - - ((DialtactsActivity) context).updateTabUnreadCounts(); return null; } }); @@ -379,6 +380,64 @@ public class CallLogAsyncTaskUtil { }); } + public static void markCallAsRead(final Context context, final long[] callIds) { + if (!PermissionsUtil.hasPhonePermissions(context)) { + return; + } + if (sAsyncTaskExecutor == null) { + initTaskExecutor(); + } + + sAsyncTaskExecutor.submit(Tasks.MARK_CALL_READ, new AsyncTask<Void, Void, Void>() { + @Override + public Void doInBackground(Void... params) { + + StringBuilder where = new StringBuilder(); + where.append(CallLog.Calls.TYPE).append(" = ").append(CallLog.Calls.MISSED_TYPE); + where.append(" AND "); + + Long[] callIdLongs = new Long[callIds.length]; + for (int i = 0; i < callIds.length; i++) { + callIdLongs[i] = callIds[i]; + } + where.append(CallLog.Calls._ID).append( + " IN (" + TextUtils.join(",", callIdLongs) + ")"); + + ContentValues values = new ContentValues(1); + values.put(CallLog.Calls.IS_READ, "1"); + context.getContentResolver().update( + CallLog.Calls.CONTENT_URI, values, where.toString(), null); + return null; + } + }); + } + + /** + * Updates the duration of a voicemail call log entry. + */ + public static void updateVoicemailDuration( + final Context context, + final Uri voicemailUri, + final int duration) { + if (!PermissionsUtil.hasPhonePermissions(context)) { + return; + } + + if (sAsyncTaskExecutor == null) { + initTaskExecutor(); + } + + sAsyncTaskExecutor.submit(Tasks.UPDATE_DURATION, new AsyncTask<Void, Void, Void>() { + @Override + public Void doInBackground(Void... params) { + ContentValues values = new ContentValues(1); + values.put(CallLog.Calls.DURATION, duration); + context.getContentResolver().update(voicemailUri, values, null, null); + return null; + } + }); + } + @VisibleForTesting public static void resetForTest() { sAsyncTaskExecutor = null; diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java index 41ff7d311..9cd1359c7 100644 --- a/src/com/android/dialer/calllog/CallLogFragment.java +++ b/src/com/android/dialer/calllog/CallLogFragment.java @@ -41,6 +41,7 @@ import android.view.ViewGroup; import com.android.contacts.common.GeoUtil; import com.android.contacts.common.util.PermissionsUtil; import com.android.dialer.R; +import com.android.dialer.list.ListsFragment; import com.android.dialer.util.EmptyLoader; import com.android.dialer.voicemail.VoicemailPlaybackPresenter; import com.android.dialer.widget.EmptyContentView; @@ -66,6 +67,7 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis 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_IS_CALL_LOG_ACTIVITY = "is_call_log_activity"; // No limit specified for the number of logs to show; use the CallLogQueryHandler's default. private static final int NO_LOG_LIMIT = -1; @@ -193,6 +195,7 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter); mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit); mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit); + mIsCallLogActivity = state.getBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity); } final Activity activity = getActivity(); @@ -278,6 +281,9 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis public void onVoicemailUnreadCountFetched(Cursor cursor) {} @Override + public void onMissedCallsUnreadCountFetched(Cursor cursor) {} + + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { View view = inflater.inflate(R.layout.call_log_fragment, container, false); setupView(view, null); @@ -351,7 +357,7 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis @Override public void onStop() { - updateOnTransition(false /* onEntry */); + updateOnTransition(); super.onStop(); } @@ -371,6 +377,7 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter); outState.putInt(KEY_LOG_LIMIT, mLogLimit); outState.putLong(KEY_DATE_LIMIT, mDateLimit); + outState.putBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity); mAdapter.onSaveInstanceState(outState); } @@ -378,6 +385,9 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis @Override public void fetchCalls() { mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit); + if (!mIsCallLogActivity) { + ((ListsFragment) getParentFragment()).updateTabUnreadCounts(); + } } private void updateEmptyMessage(int filterType) { @@ -425,7 +435,7 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis if (mMenuVisible != menuVisible) { mMenuVisible = menuVisible; if (!menuVisible) { - updateOnTransition(false /* onEntry */); + updateOnTransition(); } else if (isResumed()) { refreshData(); } @@ -443,8 +453,8 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis fetchCalls(); mCallLogQueryHandler.fetchVoicemailStatus(); - - updateOnTransition(true /* onEntry */); + mCallLogQueryHandler.fetchMissedCallsUnreadCount(); + updateOnTransition(); mRefreshDataRequired = false; } else { // Refresh the display of the existing data to update the timestamp text descriptions. @@ -453,28 +463,17 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis } /** - * 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. + * Updates the voicemail notification state. * * TODO: Move to CallLogActivity */ - private void updateOnTransition(boolean onEntry) { + private void updateOnTransition() { // We don't want to update any call data when keyguard is on because the user has likely not // seen the new calls yet. // This might be called before onCreate() and thus we need to check null explicitly. - if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) { - // On either of the transitions we update the missed call and voicemail notifications. - // While exiting we additionally consume all missed calls (by marking them as read). - mCallLogQueryHandler.markNewCallsAsOld(); - if (!onEntry) { - mCallLogQueryHandler.markMissedCallsAsRead(); - } - if (mCallTypeFilter == Calls.VOICEMAIL_TYPE) { - CallLogNotificationsHelper.updateVoicemailNotifications(getActivity()); - } else { - CallLogNotificationsHelper.removeMissedCallNotifications(getActivity()); - } + if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode() + && mCallTypeFilter == Calls.VOICEMAIL_TYPE) { + CallLogNotificationsHelper.updateVoicemailNotifications(getActivity()); } } diff --git a/src/com/android/dialer/calllog/CallLogQueryHandler.java b/src/com/android/dialer/calllog/CallLogQueryHandler.java index 4cb835bda..9ff74cfbc 100644 --- a/src/com/android/dialer/calllog/CallLogQueryHandler.java +++ b/src/com/android/dialer/calllog/CallLogQueryHandler.java @@ -62,6 +62,8 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler { private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 57; /** The token for the query to fetch the number of unread voicemails. */ private static final int QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN = 58; + /** The token for the query to fetch the number of missed calls. */ + private static final int QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN = 59; private final int mLogLimit; @@ -225,19 +227,25 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler { if (!PermissionsUtil.hasPhonePermissions(mContext)) { return; } - // Mark all "new" calls as not new anymore. - StringBuilder where = new StringBuilder(); - where.append(Calls.IS_READ).append(" = 0"); - where.append(" AND "); - where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE); ContentValues values = new ContentValues(1); values.put(Calls.IS_READ, "1"); startUpdate(UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN, null, Calls.CONTENT_URI, values, - where.toString(), null); + getUnreadMissedCallsQuery(), null); } + /** Fetch all missed calls received since last time the tab was opened. */ + public void fetchMissedCallsUnreadCount() { + if (!PermissionsUtil.hasPhonePermissions(mContext)) { + return; + } + + startQuery(QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN, null, Calls.CONTENT_URI, + new String[]{Calls._ID}, getUnreadMissedCallsQuery(), null, null); + } + + @Override protected synchronized void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor) { @@ -253,6 +261,8 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler { updateVoicemailStatus(cursor); } else if (token == QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN) { updateVoicemailUnreadCount(cursor); + } else if (token == QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN) { + updateMissedCallsUnreadCount(cursor); } else { Log.w(TAG, "Unknown query completed: ignoring: " + token); } @@ -276,6 +286,17 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler { } + /** + * @return Query string to get all unread missed calls. + */ + private String getUnreadMissedCallsQuery() { + StringBuilder where = new StringBuilder(); + where.append(Calls.IS_READ).append(" = 0"); + where.append(" AND "); + where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE); + return where.toString(); + } + private void updateVoicemailStatus(Cursor statusCursor) { final Listener listener = mListener.get(); if (listener != null) { @@ -290,6 +311,13 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler { } } + private void updateMissedCallsUnreadCount(Cursor statusCursor) { + final Listener listener = mListener.get(); + if (listener != null) { + listener.onMissedCallsUnreadCountFetched(statusCursor); + } + } + /** Listener to completion of various queries. */ public interface Listener { /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */ @@ -298,6 +326,9 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler { /** Called when {@link CallLogQueryHandler#fetchVoicemailUnreadCount()} completes. */ void onVoicemailUnreadCountFetched(Cursor cursor); + /** Called when {@link CallLogQueryHandler#fetchMissedCallsUnreadCount()} completes. */ + void onMissedCallsUnreadCountFetched(Cursor cursor); + /** * Called when {@link CallLogQueryHandler#fetchCalls(int)} complete. * Returns true if takes ownership of cursor. diff --git a/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java b/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java index e6b850872..7b149e24e 100644 --- a/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java +++ b/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java @@ -40,6 +40,7 @@ import com.android.dialer.util.DialerUtils; import java.util.ArrayList; import java.util.Calendar; +import java.util.concurrent.TimeUnit; /** * Helper class to fill in the views in {@link PhoneCallDetailsViews}. @@ -110,10 +111,8 @@ public class PhoneCallDetailsHelper { callCount = null; } - CharSequence callLocationAndDate = getCallLocationAndDate(details); - - // Set the call count, location and date. - setCallCountAndDate(views, callCount, callLocationAndDate); + // Set the call count, location, date and if voicemail, set the duration. + setDetailText(views, callCount, details); // Set the account label if it exists. String accountLabel = mCallLogCache.getAccountLabel(details.accountHandle); @@ -314,10 +313,11 @@ public class PhoneCallDetailsHelper { } } - /** Sets the call count and date. */ - private void setCallCountAndDate(PhoneCallDetailsViews views, Integer callCount, - CharSequence dateText) { + /** Sets the call count, date, and if it is a voicemail, sets the duration. */ + private void setDetailText(PhoneCallDetailsViews views, Integer callCount, + PhoneCallDetails details) { // Combine the count (if present) and the date. + CharSequence dateText = getCallLocationAndDate(details); final CharSequence text; if (callCount != null) { text = mResources.getString( @@ -326,6 +326,22 @@ public class PhoneCallDetailsHelper { text = dateText; } - views.callLocationAndDate.setText(text); + if (details.callTypes[0] == Calls.VOICEMAIL_TYPE && details.duration > 0) { + views.callLocationAndDate.setText(mResources.getString( + R.string.voicemailCallLogDateTimeFormatWithDuration, text, + getVoicemailDuration(details))); + } else { + views.callLocationAndDate.setText(text); + } + + } + + private String getVoicemailDuration(PhoneCallDetails details) { + long minutes = TimeUnit.SECONDS.toMinutes(details.duration); + long seconds = details.duration - TimeUnit.MINUTES.toSeconds(minutes); + if (minutes > 99) { + minutes = 99; + } + return mResources.getString(R.string.voicemailDurationFormat, minutes, seconds); } } diff --git a/src/com/android/dialer/list/ListsFragment.java b/src/com/android/dialer/list/ListsFragment.java index d5caa1a2e..5b7c950bf 100644 --- a/src/com/android/dialer/list/ListsFragment.java +++ b/src/com/android/dialer/list/ListsFragment.java @@ -36,6 +36,7 @@ import com.android.contacts.common.list.ViewPagerTabs; import com.android.dialer.DialtactsActivity; import com.android.dialer.R; import com.android.dialer.calllog.CallLogFragment; +import com.android.dialer.calllog.CallLogNotificationsHelper; import com.android.dialer.calllog.CallLogQueryHandler; import com.android.dialer.calllog.VisualVoicemailCallLogFragment; import com.android.dialer.logging.Logger; @@ -192,6 +193,7 @@ public class ListsFragment extends Fragment mCallLogQueryHandler = new CallLogQueryHandler(getActivity(), getActivity().getContentResolver(), this); mCallLogQueryHandler.fetchVoicemailStatus(); + mCallLogQueryHandler.fetchMissedCallsUnreadCount(); Trace.endSection(); } @@ -347,6 +349,23 @@ public class ListsFragment extends Fragment } @Override + public void onMissedCallsUnreadCountFetched(Cursor cursor) { + if (getActivity() == null || getActivity().isFinishing() || cursor == null) { + return; + } + + int count = 0; + try { + count = cursor.getCount(); + } finally { + cursor.close(); + } + + mViewPagerTabs.setUnreadCount(count, TAB_INDEX_HISTORY); + mViewPagerTabs.updateTab(TAB_INDEX_HISTORY); + } + + @Override public boolean onCallsFetched(Cursor statusCursor) { // Return false; did not take ownership of cursor return false; @@ -358,14 +377,29 @@ public class ListsFragment extends Fragment /** * External method to update unread count because the unread count changes when the user - * expands a voicemail in the call log. + * expands a voicemail in the call log or when the user expands an unread call in the call + * history tab. */ public void updateTabUnreadCounts() { - if (mHasActiveVoicemailProvider && mCallLogQueryHandler != null) { - mCallLogQueryHandler.fetchVoicemailUnreadCount(); + if (mCallLogQueryHandler != null) { + mCallLogQueryHandler.fetchMissedCallsUnreadCount(); + if (mHasActiveVoicemailProvider) { + mCallLogQueryHandler.fetchVoicemailUnreadCount(); + } } } + /** + * External method to mark all missed calls as read. + */ + public void markMissedCallsAsReadAndRemoveNotifications() { + if (mCallLogQueryHandler != null) { + mCallLogQueryHandler.markMissedCallsAsRead(); + CallLogNotificationsHelper.removeMissedCallNotifications(getActivity()); + } + } + + public void showRemoveView(boolean show) { mRemoveViewContent.setVisibility(show ? View.VISIBLE : View.GONE); mRemoveView.setAlpha(show ? 0 : 1); diff --git a/src/com/android/dialer/voicemail/VisualVoicemailEnabledChecker.java b/src/com/android/dialer/voicemail/VisualVoicemailEnabledChecker.java index 3134b1486..80a0368bd 100644 --- a/src/com/android/dialer/voicemail/VisualVoicemailEnabledChecker.java +++ b/src/com/android/dialer/voicemail/VisualVoicemailEnabledChecker.java @@ -91,6 +91,11 @@ public class VisualVoicemailEnabledChecker implements CallLogQueryHandler.Listen } @Override + public void onMissedCallsUnreadCountFetched(Cursor cursor) { + // Do nothing + } + + @Override public boolean onCallsFetched(Cursor combinedCursor) { // Do nothing return false; diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java index 93ff00270..2bb4de932 100644 --- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java +++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java @@ -36,6 +36,7 @@ import android.view.WindowManager.LayoutParams; import android.widget.SeekBar; import com.android.dialer.R; +import com.android.dialer.calllog.CallLogAsyncTaskUtil; import com.android.dialer.util.AsyncTaskExecutor; import com.android.dialer.util.AsyncTaskExecutors; import com.android.common.io.MoreCloseables; @@ -557,6 +558,12 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene Log.d(TAG, "onPrepared"); mIsPrepared = true; + // Update the duration in the database if it was not previously retrieved + if (mDuration.get() == 0) { + CallLogAsyncTaskUtil.updateVoicemailDuration(mContext, mVoicemailUri, + mMediaPlayer.getDuration() / 1000); + } + mDuration.set(mMediaPlayer.getDuration()); Log.d(TAG, "onPrepared: mPosition=" + mPosition); diff --git a/tests/src/com/android/dialer/calllog/PhoneCallDetailsHelperTest.java b/tests/src/com/android/dialer/calllog/PhoneCallDetailsHelperTest.java index b6202b904..c0d1203d9 100644 --- a/tests/src/com/android/dialer/calllog/PhoneCallDetailsHelperTest.java +++ b/tests/src/com/android/dialer/calllog/PhoneCallDetailsHelperTest.java @@ -34,6 +34,8 @@ import com.android.dialer.util.LocaleTestUtils; import java.util.GregorianCalendar; import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Unit tests for {@link PhoneCallDetailsHelper}. @@ -141,9 +143,29 @@ public class PhoneCallDetailsHelperTest extends AndroidTestCase { public void testVoicemailLocationNotShownWithDate() { setVoicemailPhoneCallDetailsWithDate(TEST_DATE); + assertLocationAndDateExactEquals("Jun 3 at 1:00 PM • 99:20"); + } + + public void testVoicemailDuration() { + setVoicemailPhoneCallDetailsWithDuration(100); + assertDurationExactEquals("01:40"); + } + + public void testVoicemailDuration_Capped() { + setVoicemailPhoneCallDetailsWithDuration(TEST_DURATION); + assertDurationExactEquals("99:20"); + } + + public void testVoicemailDuration_Zero() { + setVoicemailPhoneCallDetailsWithDuration(0); assertLocationAndDateExactEquals("Jun 3 at 1:00 PM"); } + public void testVoicemailDuration_EvenMinute() { + setVoicemailPhoneCallDetailsWithDuration(60); + assertDurationExactEquals("01:00"); + } + /** Asserts that a char sequence is actually a Spanned corresponding to the expected HTML. */ private void assertEqualsHtml(String expectedHtml, CharSequence actualText) { // In order to contain HTML, the text should actually be a Spanned. @@ -346,6 +368,14 @@ public class PhoneCallDetailsHelperTest extends AndroidTestCase { assertEquals(text, mViews.callLocationAndDate.getText()); } + /** Asserts that the duration is exactly as included in the location and date text field. */ + private void assertDurationExactEquals(String text) { + Matcher matcher = Pattern.compile("(.*) (\\u2022) (\\d{2}:\\d{2})").matcher( + mViews.callLocationAndDate.getText()); + assertEquals(true, matcher.matches()); + assertEquals(text, matcher.group(3)); + } + /** Asserts that the video icon is shown. */ private void assertIsVideoCall(boolean isVideoCall) { assertEquals(isVideoCall, mViews.callTypeIcons.isVideoShown()); @@ -407,6 +437,14 @@ public class PhoneCallDetailsHelperTest extends AndroidTestCase { mHelper.setPhoneCallDetails(mViews, details); } + /** Sets the voice mail details with default values and the given duration. */ + private void setVoicemailPhoneCallDetailsWithDuration(long duration) { + PhoneCallDetails details = getPhoneCallDetails(); + details.duration = duration; + details.callTypes = new int[] {Calls.VOICEMAIL_TYPE}; + mHelper.setPhoneCallDetails(mViews, details); + } + /** Sets the phone call details with default values and the given call types using icons. */ private void setPhoneCallDetailsWithCallTypeIcons(int... callTypes) { PhoneCallDetails details = getPhoneCallDetails(); |