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 --- .../database/AnnotatedCallLogDatabaseHelper.java | 4 +- .../android/dialer/calllog/database/Coalescer.java | 12 +- .../calllog/database/annotated_call_log.proto | 3 +- .../contract/AnnotatedCallLogContract.java | 29 +++-- .../datasources/contacts/ContactsDataSource.java | 4 +- .../systemcalllog/SystemCallLogDataSource.java | 69 ++++++++--- .../calllog/datasources/util/RowCombiner.java | 13 ++- .../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 +++++ .../android/dialer/calllogutils/CallLogDates.java | 3 +- java/com/android/dialer/function/Supplier.java | 23 ++++ .../dialer/strictmode/DialerStrictMode.java | 14 +-- java/com/android/dialer/time/Clock.java | 23 ++++ 21 files changed, 592 insertions(+), 82 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 create mode 100644 java/com/android/dialer/function/Supplier.java create mode 100644 java/com/android/dialer/time/Clock.java (limited to 'java') diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java index ebfd3c79b..507a51af6 100644 --- a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java +++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java @@ -38,7 +38,9 @@ class AnnotatedCallLogDatabaseHelper extends SQLiteOpenHelper { // Common columns. .append(AnnotatedCallLog._ID + " integer primary key, ") .append(AnnotatedCallLog.TIMESTAMP + " integer, ") - .append(AnnotatedCallLog.PRIMARY_TEXT + " string, ") + .append(AnnotatedCallLog.NAME + " string, ") + .append(AnnotatedCallLog.NEW + " integer, ") + .append(AnnotatedCallLog.TYPE + " integer, ") .append(AnnotatedCallLog.CONTACT_PHOTO_URI + " string, ") .append(AnnotatedCallLog.NUMBER_TYPE_LABEL + " string, ") .append(AnnotatedCallLog.IS_READ + " integer, ") diff --git a/java/com/android/dialer/calllog/database/Coalescer.java b/java/com/android/dialer/calllog/database/Coalescer.java index 23ddc9c21..55bed3e8c 100644 --- a/java/com/android/dialer/calllog/database/Coalescer.java +++ b/java/com/android/dialer/calllog/database/Coalescer.java @@ -133,8 +133,16 @@ public class Coalescer { DialerPhoneNumber number1; DialerPhoneNumber number2; try { - number1 = DialerPhoneNumber.parseFrom(row1.getAsByteArray(AnnotatedCallLog.NUMBER)); - number2 = DialerPhoneNumber.parseFrom(row2.getAsByteArray(AnnotatedCallLog.NUMBER)); + byte[] number1Bytes = row1.getAsByteArray(AnnotatedCallLog.NUMBER); + byte[] number2Bytes = row2.getAsByteArray(AnnotatedCallLog.NUMBER); + + if (number1Bytes == null || number2Bytes == null) { + // Empty numbers should not be combined. + return false; + } + + number1 = DialerPhoneNumber.parseFrom(number1Bytes); + number2 = DialerPhoneNumber.parseFrom(number2Bytes); } catch (InvalidProtocolBufferException e) { throw Assert.createAssertionFailException("error parsing DialerPhoneNumber proto", e); } diff --git a/java/com/android/dialer/calllog/database/annotated_call_log.proto b/java/com/android/dialer/calllog/database/annotated_call_log.proto index eb0ee52ce..de2bc5f14 100644 --- a/java/com/android/dialer/calllog/database/annotated_call_log.proto +++ b/java/com/android/dialer/calllog/database/annotated_call_log.proto @@ -8,7 +8,8 @@ option optimize_for = LITE_RUNTIME; package com.android.dialer; -// A list of android.provider.CallLog.Calls.TYPE values. +// A list of android.provider.CallLog.Calls.TYPE values ordered from newest to +// oldest. message CallTypes { repeated int32 type = 1; } diff --git a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java index 172006878..c669bdae5 100644 --- a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java +++ b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java @@ -42,15 +42,15 @@ public class AnnotatedCallLogContract { String TIMESTAMP = "timestamp"; /** - * Primary text to display for the entry. This could be a name from a local contact or caller ID - * data source, or it could just be a phone number, for example. + * Name of the caller if available. This could be a name from a local contact or caller ID data + * source, for example. * *

This is exactly how it should appear to the user. If the user's locale or name display * preferences change, this column should be rewritten. * *

Type: TEXT */ - String PRIMARY_TEXT = "primary_text"; + String NAME = "name"; /** * Local photo URI for the contact associated with the phone number, if it exists. @@ -74,14 +74,21 @@ public class AnnotatedCallLogContract { String NUMBER_TYPE_LABEL = "number_type_label"; /** - * See CallLog.Calls.IS_READ. + * See {@link android.provider.CallLog.Calls#IS_READ}. * *

TYPE: INTEGER (boolean) */ String IS_READ = "is_read"; /** - * See CallLog.Calls.GEOCODED_LOCATION. + * See {@link android.provider.CallLog.Calls#NEW}. + * + *

Type: INTEGER (boolean) + */ + String NEW = "new"; + + /** + * See {@link android.provider.CallLog.Calls#GEOCODED_LOCATION}. * *

TYPE: TEXT */ @@ -102,7 +109,7 @@ public class AnnotatedCallLogContract { String PHONE_ACCOUNT_COLOR = "phone_account_color"; /** - * See CallLog.Calls.FEATURES. + * See {@link android.provider.CallLog.Calls#FEATURES}. * *

TYPE: INTEGER (int) */ @@ -128,10 +135,11 @@ public class AnnotatedCallLogContract { new String[] { _ID, TIMESTAMP, - PRIMARY_TEXT, + NAME, CONTACT_PHOTO_URI, NUMBER_TYPE_LABEL, IS_READ, + NEW, GEOCODED_LOCATION, PHONE_ACCOUNT_LABEL, PHONE_ACCOUNT_COLOR, @@ -168,6 +176,13 @@ public class AnnotatedCallLogContract { *

Type: BLOB */ public static final String NUMBER = "number"; + + /** + * Copied from {@link android.provider.CallLog.Calls#TYPE}. + * + *

Type: INTEGER (int) + */ + public static final String TYPE = "type"; } /** diff --git a/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java b/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java index ad824274e..8090cbc62 100644 --- a/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java +++ b/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java @@ -51,7 +51,7 @@ public final class ContactsDataSource implements CallLogDataSource { Assert.isWorkerThread(); // TODO(zachh): Implementation. for (ContentValues contentValues : mutations.getInserts().values()) { - contentValues.put(AnnotatedCallLog.PRIMARY_TEXT, "Placeholder name"); + contentValues.put(AnnotatedCallLog.NAME, "Placeholder name"); } } @@ -64,7 +64,7 @@ public final class ContactsDataSource implements CallLogDataSource { public ContentValues coalesce(List individualRowsSortedByTimestampDesc) { // TODO(zachh): Implementation. return new RowCombiner(individualRowsSortedByTimestampDesc) - .useSingleValueString(AnnotatedCallLog.PRIMARY_TEXT) + .useSingleValueString(AnnotatedCallLog.NAME) .combine(); } diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java index 94b908f8f..7bf2972c5 100644 --- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java @@ -37,6 +37,7 @@ import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.text.TextUtils; import android.util.ArraySet; +import com.android.dialer.CallTypes; import com.android.dialer.DialerPhoneNumber; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog; @@ -84,6 +85,7 @@ public class SystemCallLogDataSource implements CallLogDataSource { LogUtil.i("SystemCallLogDataSource.registerContentObservers", "no call log permissions"); return; } + // TODO(zachh): Need to somehow register observers if user enables permission after launch? appContext .getContentResolver() @@ -104,8 +106,11 @@ public class SystemCallLogDataSource implements CallLogDataSource { * column is unused). This means that we can't detect deletes without scanning the entire table, * which would be too slow. So, we just rely on content observers to trigger rebuilds when any * change is made to the system call log. + * + * Just return false unless the table has never been written to. */ - return false; + return !PreferenceManager.getDefaultSharedPreferences(appContext) + .contains(PREF_LAST_TIMESTAMP_PROCESSED); } @WorkerThread @@ -152,23 +157,41 @@ public class SystemCallLogDataSource implements CallLogDataSource { ContentValues coalescedValues = new RowCombiner(individualRowsSortedByTimestampDesc) .useMostRecentLong(AnnotatedCallLog.TIMESTAMP) + .useMostRecentLong(AnnotatedCallLog.NEW) + .useMostRecentString(AnnotatedCallLog.NUMBER_TYPE_LABEL) + .useMostRecentString(AnnotatedCallLog.GEOCODED_LOCATION) .combine(); // All phone numbers in the provided group should be equivalent (but could be formatted // differently). Arbitrarily show the raw phone number of the most recent call. DialerPhoneNumber mostRecentPhoneNumber = getMostRecentPhoneNumber(individualRowsSortedByTimestampDesc); - coalescedValues.put( - CoalescedAnnotatedCallLog.FORMATTED_NUMBER, - mostRecentPhoneNumber.getRawInput().getNumber()); + if (mostRecentPhoneNumber != null) { + coalescedValues.put( + CoalescedAnnotatedCallLog.FORMATTED_NUMBER, + mostRecentPhoneNumber.getRawInput().getNumber()); + } + + CallTypes.Builder callTypes = CallTypes.newBuilder(); + // Store a maximum of 3 call types since that's all we show to users via icons. + for (int i = 0; i < 3 && i < individualRowsSortedByTimestampDesc.size(); i++) { + callTypes.addType( + individualRowsSortedByTimestampDesc.get(i).getAsInteger(AnnotatedCallLog.TYPE)); + } + coalescedValues.put(CoalescedAnnotatedCallLog.CALL_TYPES, callTypes.build().toByteArray()); + return coalescedValues; } + @Nullable private static DialerPhoneNumber getMostRecentPhoneNumber( List individualRowsSortedByTimestampDesc) { - DialerPhoneNumber dialerPhoneNumber; byte[] protoBytes = individualRowsSortedByTimestampDesc.get(0).getAsByteArray(AnnotatedCallLog.NUMBER); + if (protoBytes == null) { + return null; + } + DialerPhoneNumber dialerPhoneNumber; try { dialerPhoneNumber = DialerPhoneNumber.parseFrom(protoBytes); } catch (InvalidProtocolBufferException e) { @@ -198,10 +221,12 @@ public class SystemCallLogDataSource implements CallLogDataSource { Calls.DATE, Calls.LAST_MODIFIED, Calls.NUMBER, + Calls.TYPE, Calls.COUNTRY_ISO, Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_LABEL, Calls.IS_READ, + Calls.NEW, Calls.GEOCODED_LOCATION, Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_ID, @@ -226,10 +251,12 @@ public class SystemCallLogDataSource implements CallLogDataSource { int dateColumn = cursor.getColumnIndexOrThrow(Calls.DATE); int lastModifiedColumn = cursor.getColumnIndexOrThrow(Calls.LAST_MODIFIED); int numberColumn = cursor.getColumnIndexOrThrow(Calls.NUMBER); + int typeColumn = cursor.getColumnIndexOrThrow(Calls.TYPE); int countryIsoColumn = cursor.getColumnIndexOrThrow(Calls.COUNTRY_ISO); int cachedNumberTypeColumn = cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_TYPE); int cachedNumberLabelColumn = cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_LABEL); int isReadColumn = cursor.getColumnIndexOrThrow(Calls.IS_READ); + int newColumn = cursor.getColumnIndexOrThrow(Calls.NEW); int geocodedLocationColumn = cursor.getColumnIndexOrThrow(Calls.GEOCODED_LOCATION); int phoneAccountComponentColumn = cursor.getColumnIndexOrThrow(Calls.PHONE_ACCOUNT_COMPONENT_NAME); @@ -243,30 +270,40 @@ public class SystemCallLogDataSource implements CallLogDataSource { long id = cursor.getLong(idColumn); long date = cursor.getLong(dateColumn); String numberAsStr = cursor.getString(numberColumn); + long type = cursor.getType(typeColumn); String countryIso = cursor.getString(countryIsoColumn); // TODO(zachh): Decide if should use "cached" columns from call log or recompute. int cachedNumberType = cursor.getInt(cachedNumberTypeColumn); String cachedNumberLabel = cursor.getString(cachedNumberLabelColumn); int isRead = cursor.getInt(isReadColumn); + int isNew = cursor.getInt(newColumn); String geocodedLocation = cursor.getString(geocodedLocationColumn); String phoneAccountComponentName = cursor.getString(phoneAccountComponentColumn); String phoneAccountId = cursor.getString(phoneAccountIdColumn); int features = cursor.getInt(featuresColumn); - byte[] numberAsProtoBytes = - dialerPhoneNumberUtil.parse(numberAsStr, countryIso).toByteArray(); - ContentValues contentValues = new ContentValues(); contentValues.put(AnnotatedCallLog.TIMESTAMP, date); - // TODO(zachh): Need to handle post-dial digits; different on N and M. - contentValues.put(AnnotatedCallLog.NUMBER, numberAsProtoBytes); - - // TODO(zachh): Test this for locales. - contentValues.put( - AnnotatedCallLog.NUMBER_TYPE_LABEL, - Phone.getTypeLabel(appContext.getResources(), cachedNumberType, cachedNumberLabel) - .toString()); + + if (!TextUtils.isEmpty(numberAsStr)) { + byte[] numberAsProtoBytes = + dialerPhoneNumberUtil.parse(numberAsStr, countryIso).toByteArray(); + // TODO(zachh): Need to handle post-dial digits; different on N and M. + contentValues.put(AnnotatedCallLog.NUMBER, numberAsProtoBytes); + } + + contentValues.put(AnnotatedCallLog.TYPE, type); + + // Phone.getTypeLabel returns "Custom" if given (0, null) which is not of any use. Just + // omit setting the label if there's no information for it. + if (cachedNumberType != 0 || cachedNumberLabel != null) { + contentValues.put( + AnnotatedCallLog.NUMBER_TYPE_LABEL, + Phone.getTypeLabel(appContext.getResources(), cachedNumberType, cachedNumberLabel) + .toString()); + } contentValues.put(AnnotatedCallLog.IS_READ, isRead); + contentValues.put(AnnotatedCallLog.NEW, isNew); contentValues.put(AnnotatedCallLog.GEOCODED_LOCATION, geocodedLocation); populatePhoneAccountLabelAndColor( appContext, contentValues, phoneAccountComponentName, phoneAccountId); diff --git a/java/com/android/dialer/calllog/datasources/util/RowCombiner.java b/java/com/android/dialer/calllog/datasources/util/RowCombiner.java index 0c7be1e27..3595a055d 100644 --- a/java/com/android/dialer/calllog/datasources/util/RowCombiner.java +++ b/java/com/android/dialer/calllog/datasources/util/RowCombiner.java @@ -36,12 +36,23 @@ public class RowCombiner { return this; } + /** Use the most recent value for the specified column. */ + public RowCombiner useMostRecentString(String columnName) { + combinedRow.put(columnName, individualRowsSortedByTimestampDesc.get(0).getAsString(columnName)); + return this; + } + /** Asserts that all column values for the given column name are the same, and uses it. */ public RowCombiner useSingleValueString(String columnName) { Iterator iterator = individualRowsSortedByTimestampDesc.iterator(); String singleValue = iterator.next().getAsString(columnName); while (iterator.hasNext()) { - Assert.checkState(iterator.next().getAsString(columnName).equals(singleValue)); + String current = iterator.next().getAsString(columnName); + if (current == null) { + Assert.checkState(singleValue == null); + } else { + Assert.checkState(current.equals(singleValue)); + } } combinedRow.put(columnName, singleValue); return this; 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 diff --git a/java/com/android/dialer/calllogutils/CallLogDates.java b/java/com/android/dialer/calllogutils/CallLogDates.java index 2d4bd8bf5..82e8e404e 100644 --- a/java/com/android/dialer/calllogutils/CallLogDates.java +++ b/java/com/android/dialer/calllogutils/CallLogDates.java @@ -151,7 +151,8 @@ public final class CallLogDates { return then.equals(threeDaysAgoStartOfDay) || then.after(threeDaysAgoStartOfDay); } - private static boolean isSameDay(long firstMillis, long secondMillis) { + /** Returns true if the provided timestamps are from the same day in the default time zone. */ + public static boolean isSameDay(long firstMillis, long secondMillis) { Calendar first = Calendar.getInstance(); first.setTimeInMillis(firstMillis); diff --git a/java/com/android/dialer/function/Supplier.java b/java/com/android/dialer/function/Supplier.java new file mode 100644 index 000000000..1a16183f2 --- /dev/null +++ b/java/com/android/dialer/function/Supplier.java @@ -0,0 +1,23 @@ +/* + * 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.function; + +/** Functional interface for supplying generic values. */ +public interface Supplier { + + /** Supplies a value. */ + T get(); +} diff --git a/java/com/android/dialer/strictmode/DialerStrictMode.java b/java/com/android/dialer/strictmode/DialerStrictMode.java index 4a0336e53..c7d0b3f57 100644 --- a/java/com/android/dialer/strictmode/DialerStrictMode.java +++ b/java/com/android/dialer/strictmode/DialerStrictMode.java @@ -28,6 +28,7 @@ import android.support.annotation.AnyThread; import android.support.annotation.MainThread; import android.support.v4.os.UserManagerCompat; import com.android.dialer.buildtype.BuildType; +import com.android.dialer.function.Supplier; import com.android.dialer.util.DialerUtils; /** Enables strict mode for the application, and provides means of temporarily disabling it. */ @@ -94,11 +95,6 @@ public final class DialerStrictMode { return Looper.getMainLooper().equals(Looper.myLooper()); } - /** Functional interface intended to be used with {@link #bypass(Provider)}. */ - public interface Provider { - T get(); - } - /** * Convenience method for disabling and enabling the thread policy death penalty using lambdas. * @@ -111,17 +107,17 @@ public final class DialerStrictMode { *

The thread policy is only mutated if this is called from the main thread. */ @AnyThread - public static T bypass(Provider provider) { + public static T bypass(Supplier supplier) { if (isStrictModeAllowed() && onMainThread()) { ThreadPolicy originalPolicy = StrictMode.getThreadPolicy(); StrictModeUtils.setRecommendedMainThreadPolicy(THREAD_LOG_PENALTY); try { - return provider.get(); + return supplier.get(); } finally { StrictMode.setThreadPolicy(originalPolicy); } } - return provider.get(); + return supplier.get(); } /** @@ -149,3 +145,5 @@ public final class DialerStrictMode { runnable.run(); } } + + diff --git a/java/com/android/dialer/time/Clock.java b/java/com/android/dialer/time/Clock.java new file mode 100644 index 000000000..4b7edc662 --- /dev/null +++ b/java/com/android/dialer/time/Clock.java @@ -0,0 +1,23 @@ +/* + * 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.time; + +/** Functional interface for providing time since epoch. */ +public interface Clock { + /** Returns milliseconds since epoch. */ + long currentTimeMillis(); +} -- cgit v1.2.3