From 51f2b28ae6a45f57f94e5c9a66081a10aebc8349 Mon Sep 17 00:00:00 2001 From: zachh Date: Fri, 25 Aug 2017 12:19:07 -0700 Subject: Added partial UI to new call log. UI notes: -Updated view holder to show number and basic secondary text -Updated adapter to divide "Today" and "Older" entries -Photo is just an anonymous avatar for now -Clicking anywhere is a no-op Other things done in this CL: -Plumbed a few more columns through the call log framework -Tweaked some column names in the data model (contract) -Cleaned up some existing tests and added some new ones Screenshot: https://screenshot.googleplex.com/DiMscW47AYb This is the complete spec I am working from: https://screenshot.googleplex.com/XLquTek1oHk Bug: 34672501 Test: existing and new Change-Id: Ice0e538e23e59b7d752f47125a5f9da96bf91430 PiperOrigin-RevId: 166508997 --- .../ui/CoalescedAnnotatedCallLogCursorLoader.java | 29 +++-- .../dialer/calllog/ui/HeaderViewHolder.java | 36 ++++++ .../dialer/calllog/ui/NewCallLogAdapter.java | 126 +++++++++++++++++++-- .../dialer/calllog/ui/NewCallLogFragment.java | 4 +- .../dialer/calllog/ui/NewCallLogViewHolder.java | 100 ++++++++++++++-- .../calllog/ui/res/layout/new_call_log_entry.xml | 65 +++++++++-- .../calllog/ui/res/layout/new_call_log_header.xml | 29 +++++ .../dialer/calllog/ui/res/values/dimens.xml | 28 +++++ .../dialer/calllog/ui/res/values/strings.xml | 32 ++++++ .../dialer/calllog/ui/res/values/styles.xml | 28 +++++ 10 files changed, 434 insertions(+), 43 deletions(-) create mode 100644 java/com/android/dialer/calllog/ui/HeaderViewHolder.java create mode 100644 java/com/android/dialer/calllog/ui/res/layout/new_call_log_header.xml create mode 100644 java/com/android/dialer/calllog/ui/res/values/dimens.xml create mode 100644 java/com/android/dialer/calllog/ui/res/values/strings.xml create mode 100644 java/com/android/dialer/calllog/ui/res/values/styles.xml (limited to 'java/com/android/dialer/calllog/ui') diff --git a/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java b/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java index e993816bf..654591688 100644 --- a/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java +++ b/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java @@ -30,19 +30,20 @@ final class CoalescedAnnotatedCallLogCursorLoader extends CursorLoader { private static final int ID = 0; private static final int TIMESTAMP = 1; - private static final int PRIMARY_TEXT = 2; + private static final int NAME = 2; private static final int CONTACT_PHOTO_URI = 3; private static final int NUMBER_TYPE_LABEL = 4; private static final int IS_READ = 5; - private static final int GEOCODED_LOCATION = 6; - private static final int PHONE_ACCOUNT_LABEL = 7; - private static final int PHONE_ACCOUNT_COLOR = 8; - private static final int FEATURES = 9; - private static final int IS_BUSINESS = 10; - private static final int IS_VOICEMAIL = 11; - private static final int NUMBER_CALLS = 12; - private static final int FORMATTED_NUMBER = 13; - private static final int CALL_TYPES = 14; + private static final int NEW = 6; + private static final int GEOCODED_LOCATION = 7; + private static final int PHONE_ACCOUNT_LABEL = 8; + private static final int PHONE_ACCOUNT_COLOR = 9; + private static final int FEATURES = 10; + private static final int IS_BUSINESS = 11; + private static final int IS_VOICEMAIL = 12; + private static final int NUMBER_CALLS = 13; + private static final int FORMATTED_NUMBER = 14; + private static final int CALL_TYPES = 15; /** Convenience class for accessing values using an abbreviated syntax. */ static final class Row { @@ -60,8 +61,8 @@ final class CoalescedAnnotatedCallLogCursorLoader extends CursorLoader { return cursor.getLong(TIMESTAMP); } - String primaryText() { - return cursor.getString(PRIMARY_TEXT); + String name() { + return cursor.getString(NAME); } String contactPhotoUri() { @@ -76,6 +77,10 @@ final class CoalescedAnnotatedCallLogCursorLoader extends CursorLoader { return cursor.getInt(IS_READ) == 1; } + boolean isNew() { + return cursor.getInt(NEW) == 1; + } + String geocodedLocation() { return cursor.getString(GEOCODED_LOCATION); } diff --git a/java/com/android/dialer/calllog/ui/HeaderViewHolder.java b/java/com/android/dialer/calllog/ui/HeaderViewHolder.java new file mode 100644 index 000000000..e4fe029fa --- /dev/null +++ b/java/com/android/dialer/calllog/ui/HeaderViewHolder.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 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.ui; + +import android.support.annotation.StringRes; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.view.View; +import android.widget.TextView; + +/** ViewHolder for {@link NewCallLogAdapter} to display "Today" or "Older" divider row. */ +final class HeaderViewHolder extends ViewHolder { + + private TextView headerTextView; + + HeaderViewHolder(View view) { + super(view); + headerTextView = view.findViewById(R.id.new_call_log_header_text); + } + + void setHeader(@StringRes int header) { + headerTextView.setText(header); + } +} diff --git a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java index 4655b0982..b922a6e3b 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java @@ -16,34 +16,140 @@ package com.android.dialer.calllog.ui; import android.database.Cursor; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; import android.view.LayoutInflater; import android.view.ViewGroup; +import com.android.dialer.calllogutils.CallLogDates; +import com.android.dialer.common.Assert; +import com.android.dialer.time.Clock; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** {@link RecyclerView.Adapter} for the new call log fragment. */ -final class NewCallLogAdapter extends RecyclerView.Adapter { +final class NewCallLogAdapter extends RecyclerView.Adapter { + + /** IntDef for the different types of rows that can be shown in the call log. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({RowType.HEADER_TODAY, RowType.HEADER_OLDER, RowType.CALL_LOG_ENTRY}) + @interface RowType { + /** Header that displays "Today". */ + int HEADER_TODAY = 1; + /** Header that displays "Older". */ + int HEADER_OLDER = 2; + /** A row representing a call log entry (which could represent one or more calls). */ + int CALL_LOG_ENTRY = 3; + } private final Cursor cursor; + private final Clock clock; - NewCallLogAdapter(Cursor cursor) { + /** Null when the "Today" header should not be displayed. */ + @Nullable private final Integer todayHeaderPosition; + /** Null when the "Older" header should not be displayed. */ + @Nullable private final Integer olderHeaderPosition; + + NewCallLogAdapter(Cursor cursor, Clock clock) { this.cursor = cursor; + this.clock = clock; + + // Calculate header adapter positions by reading cursor. + long currentTimeMillis = clock.currentTimeMillis(); + if (cursor.moveToNext()) { + CoalescedAnnotatedCallLogCursorLoader.Row firstRow = + new CoalescedAnnotatedCallLogCursorLoader.Row(cursor); + if (CallLogDates.isSameDay(currentTimeMillis, firstRow.timestamp())) { + this.todayHeaderPosition = 0; + int adapterPosition = 2; // Accounted for "Today" header and first row. + while (cursor.moveToNext()) { + CoalescedAnnotatedCallLogCursorLoader.Row row = + new CoalescedAnnotatedCallLogCursorLoader.Row(cursor); + if (CallLogDates.isSameDay(currentTimeMillis, row.timestamp())) { + adapterPosition++; + } else { + this.olderHeaderPosition = adapterPosition; + return; + } + } + this.olderHeaderPosition = null; // Didn't find any "Older" rows. + } else { + this.todayHeaderPosition = null; // Didn't find any "Today" rows. + this.olderHeaderPosition = 0; + } + } else { // There are no rows, just need to set these because they are final. + this.todayHeaderPosition = null; + this.olderHeaderPosition = null; + } + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup viewGroup, @RowType int viewType) { + switch (viewType) { + case RowType.HEADER_TODAY: + case RowType.HEADER_OLDER: + return new HeaderViewHolder( + LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.new_call_log_header, viewGroup, false)); + case RowType.CALL_LOG_ENTRY: + return new NewCallLogViewHolder( + LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.new_call_log_entry, viewGroup, false), + clock); + default: + throw Assert.createUnsupportedOperationFailException("Unsupported view type: " + viewType); + } } @Override - public NewCallLogViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { - return new NewCallLogViewHolder( - LayoutInflater.from(viewGroup.getContext()) - .inflate(R.layout.new_call_log_entry, viewGroup, false)); + public void onBindViewHolder(ViewHolder viewHolder, int position) { + if (viewHolder instanceof HeaderViewHolder) { + HeaderViewHolder headerViewHolder = (HeaderViewHolder) viewHolder; + @RowType int viewType = getItemViewType(position); + if (viewType == RowType.HEADER_OLDER) { + headerViewHolder.setHeader(R.string.new_call_log_header_older); + } else if (viewType == RowType.HEADER_TODAY) { + headerViewHolder.setHeader(R.string.new_call_log_header_today); + } else { + throw Assert.createIllegalStateFailException( + "Unexpected view type " + viewType + " at position: " + position); + } + return; + } + NewCallLogViewHolder newCallLogViewHolder = (NewCallLogViewHolder) viewHolder; + int previousHeaders = 0; + if (todayHeaderPosition != null && position > todayHeaderPosition) { + previousHeaders++; + } + if (olderHeaderPosition != null && position > olderHeaderPosition) { + previousHeaders++; + } + cursor.moveToPosition(position - previousHeaders); + newCallLogViewHolder.bind(cursor); } @Override - public void onBindViewHolder(NewCallLogViewHolder viewHolder, int position) { - cursor.moveToPosition(position); - viewHolder.bind(cursor); + @RowType + public int getItemViewType(int position) { + if (todayHeaderPosition != null && position == todayHeaderPosition) { + return RowType.HEADER_TODAY; + } + if (olderHeaderPosition != null && position == olderHeaderPosition) { + return RowType.HEADER_OLDER; + } + return RowType.CALL_LOG_ENTRY; } @Override public int getItemCount() { - return cursor.getCount(); + int numberOfHeaders = 0; + if (todayHeaderPosition != null) { + numberOfHeaders++; + } + if (olderHeaderPosition != null) { + numberOfHeaders++; + } + return cursor.getCount() + numberOfHeaders; } } diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java index c4bcb766b..92276786e 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java @@ -68,7 +68,7 @@ public final class NewCallLogFragment extends Fragment refreshAnnotatedCallLogTask = dialerExecutorFactory .createUiTaskBuilder( - getFragmentManager(), + getActivity().getFragmentManager(), "NewCallLogFragment.refreshAnnotatedCallLog", component.getRefreshAnnotatedCallLogWorker()) .build(); @@ -140,7 +140,7 @@ public final class NewCallLogFragment extends Fragment // TODO(zachh): Handle empty cursor by showing empty view. recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(new NewCallLogAdapter(newCursor)); + recyclerView.setAdapter(new NewCallLogAdapter(newCursor, System::currentTimeMillis)); } @Override diff --git a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java index 72ea17b03..b6b658fe6 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java @@ -15,33 +15,119 @@ */ package com.android.dialer.calllog.ui; +import android.content.Context; import android.database.Cursor; +import android.provider.CallLog.Calls; import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; import android.view.View; +import android.widget.QuickContactBadge; import android.widget.TextView; -import java.text.SimpleDateFormat; +import com.android.dialer.calllogutils.CallLogDates; +import com.android.dialer.contactphoto.ContactPhotoManager; +import com.android.dialer.lettertile.LetterTileDrawable; +import com.android.dialer.time.Clock; import java.util.Locale; /** {@link RecyclerView.ViewHolder} for the new call log. */ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { - // TODO(zachh): Format correctly using current locale. - private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US); - + private final Context context; private final TextView primaryTextView; private final TextView secondaryTextView; + private final QuickContactBadge quickContactBadge; + private final Clock clock; - NewCallLogViewHolder(View view) { + NewCallLogViewHolder(View view, Clock clock) { super(view); + this.context = view.getContext(); primaryTextView = view.findViewById(R.id.primary_text); secondaryTextView = view.findViewById(R.id.secondary_text); + quickContactBadge = view.findViewById(R.id.quick_contact_photo); + this.clock = clock; } /** @param cursor a cursor from {@link CoalescedAnnotatedCallLogCursorLoader}. */ void bind(Cursor cursor) { CoalescedAnnotatedCallLogCursorLoader.Row row = new CoalescedAnnotatedCallLogCursorLoader.Row(cursor); - primaryTextView.setText(row.primaryText()); - secondaryTextView.setText(dateFormat.format(row.timestamp())); + + // TODO(zachh): Add HD icon and Wifi icon after primary text. + // TODO(zachh): Call type icons for last 3 calls. + // TODO(zachh): Use name for primary text if available. + // TODO(zachh): Handle CallLog.Calls.PRESENTATION_*, including Verizon restricted numbers. + // TODO(zachh): Handle RTL properly. + primaryTextView.setText(buildPrimaryText(row)); + secondaryTextView.setText(buildSecondaryText(row)); + + if (row.isNew()) { + // TODO(zachh): Figure out correct styling for new/missed/unread calls. + primaryTextView.setTextAppearance(R.style.primary_textview_new_call); + // TODO(zachh): Styling for call type icons when the call is new. + secondaryTextView.setTextAppearance(R.style.secondary_textview_new_call); + } + + setPhoto(); + } + + private String buildPrimaryText(CoalescedAnnotatedCallLogCursorLoader.Row row) { + StringBuilder primaryText = + new StringBuilder( + TextUtils.isEmpty(row.formattedNumber()) + ? context.getText(R.string.new_call_log_unknown) + : row.formattedNumber()); + if (row.numberCalls() > 1) { + primaryText.append(String.format(Locale.getDefault(), " (%d)", row.numberCalls())); + } + return primaryText.toString(); + } + + private String buildSecondaryText(CoalescedAnnotatedCallLogCursorLoader.Row row) { + /* + * Rules: (Duo video, )?$Label|$Location • Date + * + * Examples: + * Duo Video, Mobile • Now + * Duo Video • 11:45pm + * Mobile • 11:45pm + * Mobile • Sunday + * Brooklyn, NJ • Jan 15 + * + * Date rules: + * if < 1 minute ago: "Now"; else if today: HH:MM(am|pm); else if < 3 days: day; else: MON D + */ + StringBuilder secondaryText = new StringBuilder(); + if ((row.features() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) { + // TODO(zachh): Add "Duo" prefix? + secondaryText.append(context.getText(R.string.new_call_log_video)); + } + String numberTypeLabel = row.numberTypeLabel(); + if (!TextUtils.isEmpty(numberTypeLabel)) { + if (secondaryText.length() > 0) { + secondaryText.append(", "); + } + secondaryText.append(numberTypeLabel); + } else { // If there's a number type label, don't show the location. + String location = row.geocodedLocation(); + if (!TextUtils.isEmpty(location)) { + if (secondaryText.length() > 0) { + secondaryText.append(", "); + } + secondaryText.append(location); + } + } + if (secondaryText.length() > 0) { + secondaryText.append(" • "); + } + secondaryText.append( + CallLogDates.newCallLogTimestampLabel(context, clock.currentTimeMillis(), row.timestamp())); + return secondaryText.toString(); + } + + private void setPhoto() { + // TODO(zachh): Set photo/icon appropriately. (This just uses the anonymous avatar.) + ContactPhotoManager.getInstance(context) + .loadDialerThumbnailOrPhoto( + quickContactBadge, null, 0, null, null, LetterTileDrawable.TYPE_DEFAULT); } } diff --git a/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml b/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml index 38e07becf..568b3512e 100644 --- a/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml +++ b/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml @@ -15,23 +15,64 @@ ~ limitations under the License --> - + android:layout_marginTop="@dimen/call_log_entry_top_margin" + android:paddingTop="@dimen/call_log_entry_padding_top_start" + android:paddingBottom="@dimen/call_log_entry_padding_bottom_end" + android:paddingStart="@dimen/call_log_entry_padding_top_start" + android:paddingEnd="@dimen/call_log_entry_padding_bottom_end" + android:gravity="center_vertical"> - + - + android:layout_centerVertical="true" + android:layout_toEndOf="@+id/quick_contact_photo" + android:layout_toStartOf="@+id/menu_button" + android:orientation="vertical"> + + + + + + + - \ No newline at end of file + + diff --git a/java/com/android/dialer/calllog/ui/res/layout/new_call_log_header.xml b/java/com/android/dialer/calllog/ui/res/layout/new_call_log_header.xml new file mode 100644 index 000000000..13575db55 --- /dev/null +++ b/java/com/android/dialer/calllog/ui/res/layout/new_call_log_header.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/java/com/android/dialer/calllog/ui/res/values/dimens.xml b/java/com/android/dialer/calllog/ui/res/values/dimens.xml new file mode 100644 index 000000000..bfb4c99d7 --- /dev/null +++ b/java/com/android/dialer/calllog/ui/res/values/dimens.xml @@ -0,0 +1,28 @@ + + + + + + 6dp + 16dp + 12dp + 48dp + 4dp + 8dp + 48dp + + diff --git a/java/com/android/dialer/calllog/ui/res/values/strings.xml b/java/com/android/dialer/calllog/ui/res/values/strings.xml new file mode 100644 index 000000000..9b044ca08 --- /dev/null +++ b/java/com/android/dialer/calllog/ui/res/values/strings.xml @@ -0,0 +1,32 @@ + + + + + + + Video + + + Unknown + + + Today + + + Older + + \ No newline at end of file diff --git a/java/com/android/dialer/calllog/ui/res/values/styles.xml b/java/com/android/dialer/calllog/ui/res/values/styles.xml new file mode 100644 index 000000000..23cb93e1a --- /dev/null +++ b/java/com/android/dialer/calllog/ui/res/values/styles.xml @@ -0,0 +1,28 @@ + + + + + + + + + \ No newline at end of file -- cgit v1.2.3