From a0df9f7f52b4d7f926581f30bd0a7774a6abac43 Mon Sep 17 00:00:00 2001 From: Zachary Heidepriem Date: Wed, 11 Oct 2017 16:03:06 -0700 Subject: Added basic bottom sheet to new call log. Also added ability to click on row to call. Required plumbing through the original phone number and phone account info through AnnotatedCallLog and CoalescedAnnotatedCallLog, so that clicking to dial doesn't require an additional lookup. Required some refactoring: -created autovalue for CoalescedRow. -created autovalue for ContactPrimaryActionInfo and use it in ContactActionBottomSheet -moved logic for building primary and secondary text into CallLogUtils so it can be shared between call log list and bottom sheets -moved clipboard logic to own package for copying numbers Bug: 34672501 Test: unit PiperOrigin-RevId: 171760252 Change-Id: I645d89974460b611c1d9668c3ca3e50a716c7f8f --- .../database/AnnotatedCallLogDatabaseHelper.java | 5 +- .../android/dialer/calllog/database/Coalescer.java | 23 +- .../contract/AnnotatedCallLogContract.java | 38 ++-- .../systemcalllog/SystemCallLogDataSource.java | 14 ++ .../calllog/datasources/util/RowCombiner.java | 6 + .../android/dialer/calllog/model/CoalescedRow.java | 147 +++++++++++++ .../ui/CoalescedAnnotatedCallLogCursorLoader.java | 160 ++++++-------- .../dialer/calllog/ui/NewCallLogAdapter.java | 11 +- .../dialer/calllog/ui/NewCallLogViewHolder.java | 104 ++++----- .../dialer/calllog/ui/menu/AndroidManifest.xml | 16 ++ .../android/dialer/calllog/ui/menu/Modules.java | 234 +++++++++++++++++++++ .../dialer/calllog/ui/menu/NewCallLogMenu.java | 33 +++ .../dialer/calllog/ui/menu/PrimaryAction.java | 48 +++++ .../dialer/calllog/ui/menu/res/values/strings.xml | 35 +++ .../dialer/calllog/ui/res/values/strings.xml | 6 - 15 files changed, 679 insertions(+), 201 deletions(-) create mode 100644 java/com/android/dialer/calllog/model/CoalescedRow.java create mode 100644 java/com/android/dialer/calllog/ui/menu/AndroidManifest.xml create mode 100644 java/com/android/dialer/calllog/ui/menu/Modules.java create mode 100644 java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java create mode 100644 java/com/android/dialer/calllog/ui/menu/PrimaryAction.java create mode 100644 java/com/android/dialer/calllog/ui/menu/res/values/strings.xml (limited to 'java/com/android/dialer/calllog') diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java index 40d922f41..a5f1425f8 100644 --- a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java +++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java @@ -39,6 +39,7 @@ class AnnotatedCallLogDatabaseHelper extends SQLiteOpenHelper { .append(AnnotatedCallLog._ID + " integer primary key, ") .append(AnnotatedCallLog.TIMESTAMP + " integer, ") .append(AnnotatedCallLog.NAME + " string, ") + .append(AnnotatedCallLog.NUMBER + " blob, ") .append(AnnotatedCallLog.FORMATTED_NUMBER + " string, ") .append(AnnotatedCallLog.PHOTO_URI + " string, ") .append(AnnotatedCallLog.PHOTO_ID + " integer, ") @@ -47,13 +48,13 @@ class AnnotatedCallLogDatabaseHelper extends SQLiteOpenHelper { .append(AnnotatedCallLog.IS_READ + " integer, ") .append(AnnotatedCallLog.NEW + " integer, ") .append(AnnotatedCallLog.GEOCODED_LOCATION + " string, ") + .append(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME + " string, ") + .append(AnnotatedCallLog.PHONE_ACCOUNT_ID + " string, ") .append(AnnotatedCallLog.PHONE_ACCOUNT_LABEL + " string, ") .append(AnnotatedCallLog.PHONE_ACCOUNT_COLOR + " integer, ") .append(AnnotatedCallLog.FEATURES + " integer, ") .append(AnnotatedCallLog.IS_BUSINESS + " integer, ") .append(AnnotatedCallLog.IS_VOICEMAIL + " integer, ") - // Columns only in AnnotatedCallLog - .append(AnnotatedCallLog.NUMBER + " blob, ") .append(AnnotatedCallLog.CALL_TYPE + " integer") .append(");") .toString(); diff --git a/java/com/android/dialer/calllog/database/Coalescer.java b/java/com/android/dialer/calllog/database/Coalescer.java index 63fa9f828..a8a8f2f1d 100644 --- a/java/com/android/dialer/calllog/database/Coalescer.java +++ b/java/com/android/dialer/calllog/database/Coalescer.java @@ -20,11 +20,13 @@ import android.database.Cursor; import android.database.MatrixCursor; import android.support.annotation.NonNull; import android.support.annotation.WorkerThread; +import android.telecom.PhoneAccountHandle; import com.android.dialer.DialerPhoneNumber; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog; import com.android.dialer.calllog.datasources.CallLogDataSource; import com.android.dialer.calllog.datasources.DataSources; +import com.android.dialer.calllogutils.PhoneAccountUtils; import com.android.dialer.common.Assert; import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.google.i18n.phonenumbers.PhoneNumberUtil; @@ -131,11 +133,19 @@ public class Coalescer { private static boolean rowsShouldBeCombined( DialerPhoneNumberUtil dialerPhoneNumberUtil, ContentValues row1, ContentValues row2) { // Don't combine rows which don't use the same phone account. - if (!Objects.equals( - row1.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_LABEL), - row2.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_LABEL))) { + PhoneAccountHandle phoneAccount1 = + PhoneAccountUtils.getAccount( + row1.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME), + row1.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_ID)); + PhoneAccountHandle phoneAccount2 = + PhoneAccountUtils.getAccount( + row2.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME), + row2.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_ID)); + + if (!Objects.equals(phoneAccount1, phoneAccount2)) { return false; } + DialerPhoneNumber number1; DialerPhoneNumber number2; try { @@ -153,13 +163,8 @@ public class Coalescer { throw Assert.createAssertionFailException("error parsing DialerPhoneNumber proto", e); } - if (!number1.hasDialerInternalPhoneNumber() && !number2.hasDialerInternalPhoneNumber()) { - // Empty numbers should not be combined. - return false; - } - if (!number1.hasDialerInternalPhoneNumber() || !number2.hasDialerInternalPhoneNumber()) { - // An empty number should not be combined with a non-empty number. + // An empty number should not be combined with any other number. return false; } return dialerPhoneNumberUtil.isExactMatch(number1, number2); diff --git a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java index d466da9ae..e79ffd090 100644 --- a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java +++ b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java @@ -51,6 +51,15 @@ public class AnnotatedCallLogContract { */ String NAME = "name"; + /** + * The phone number called or number the call came from, encoded as a {@link + * com.android.dialer.DialerPhoneNumber} proto. The number may be empty if it was an incoming + * call and the number was unknown. + * + *

Type: BLOB + */ + String NUMBER = "number"; + /** * Copied from {@link android.provider.CallLog.Calls#CACHED_FORMATTED_NUMBER}. * @@ -111,6 +120,20 @@ public class AnnotatedCallLogContract { */ String GEOCODED_LOCATION = "geocoded_location"; + /** + * See {@link android.provider.CallLog.Calls#PHONE_ACCOUNT_COMPONENT_NAME}. + * + *

TYPE: TEXT + */ + String PHONE_ACCOUNT_COMPONENT_NAME = "phone_account_component_name"; + + /** + * See {@link android.provider.CallLog.Calls#PHONE_ACCOUNT_ID}. + * + *

TYPE: TEXT + */ + String PHONE_ACCOUNT_ID = "phone_account_id"; + /** * String suitable for display which indicates the phone account used to make the call. * @@ -160,6 +183,7 @@ public class AnnotatedCallLogContract { _ID, TIMESTAMP, NAME, + NUMBER, FORMATTED_NUMBER, PHOTO_URI, PHOTO_ID, @@ -168,6 +192,8 @@ public class AnnotatedCallLogContract { IS_READ, NEW, GEOCODED_LOCATION, + PHONE_ACCOUNT_COMPONENT_NAME, + PHONE_ACCOUNT_ID, PHONE_ACCOUNT_LABEL, PHONE_ACCOUNT_COLOR, FEATURES, @@ -192,18 +218,6 @@ public class AnnotatedCallLogContract { /** The MIME type of a {@link android.content.ContentProvider#getType(Uri)} single entry. */ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/annotated_call_log"; - - /** - * The phone number called or number the call came from, encoded as a {@link - * com.android.dialer.DialerPhoneNumber} proto. The number may be empty if it was an incoming - * call and the number was unknown. - * - *

This column is only present in the annotated call log, and not the coalesced annotated - * call log. The coalesced version uses a formatted number string rather than proto bytes. - * - *

Type: BLOB - */ - public static final String NUMBER = "number"; } /** diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java index d6ad618b3..0a965f63e 100644 --- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java @@ -38,6 +38,7 @@ import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.text.TextUtils; import android.util.ArraySet; +import com.android.dialer.DialerPhoneNumber; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; import com.android.dialer.calllog.datasources.CallLogDataSource; import com.android.dialer.calllog.datasources.CallLogMutations; @@ -156,11 +157,17 @@ public class SystemCallLogDataSource implements CallLogDataSource { .useMostRecentLong(AnnotatedCallLog.NEW) .useMostRecentString(AnnotatedCallLog.NUMBER_TYPE_LABEL) .useMostRecentString(AnnotatedCallLog.NAME) + // Two different DialerPhoneNumbers could be combined if they are different but considered + // to be an "exact match" by libphonenumber; in this case we arbitrarily select the most + // recent one. + .useMostRecentBlob(AnnotatedCallLog.NUMBER) .useMostRecentString(AnnotatedCallLog.FORMATTED_NUMBER) .useMostRecentString(AnnotatedCallLog.PHOTO_URI) .useMostRecentLong(AnnotatedCallLog.PHOTO_ID) .useMostRecentString(AnnotatedCallLog.LOOKUP_URI) .useMostRecentString(AnnotatedCallLog.GEOCODED_LOCATION) + .useSingleValueString(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME) + .useSingleValueString(AnnotatedCallLog.PHONE_ACCOUNT_ID) .useSingleValueString(AnnotatedCallLog.PHONE_ACCOUNT_LABEL) .useSingleValueLong(AnnotatedCallLog.PHONE_ACCOUNT_COLOR) .useMostRecentLong(AnnotatedCallLog.CALL_TYPE) @@ -272,10 +279,14 @@ public class SystemCallLogDataSource implements CallLogDataSource { dialerPhoneNumberUtil.parse(numberAsStr, countryIso).toByteArray(); // TODO(zachh): Need to handle post-dial digits; different on N and M. contentValues.put(AnnotatedCallLog.NUMBER, numberAsProtoBytes); + } else { + contentValues.put( + AnnotatedCallLog.NUMBER, DialerPhoneNumber.getDefaultInstance().toByteArray()); } contentValues.put(AnnotatedCallLog.CALL_TYPE, type); contentValues.put(AnnotatedCallLog.NAME, cachedName); + // TODO(zachh): Format the number using DialerPhoneNumberUtil here. contentValues.put(AnnotatedCallLog.FORMATTED_NUMBER, formattedNumber); contentValues.put(AnnotatedCallLog.PHOTO_URI, cachedPhotoUri); contentValues.put(AnnotatedCallLog.PHOTO_ID, cachedPhotoId); @@ -292,6 +303,9 @@ public class SystemCallLogDataSource implements CallLogDataSource { contentValues.put(AnnotatedCallLog.IS_READ, isRead); contentValues.put(AnnotatedCallLog.NEW, isNew); contentValues.put(AnnotatedCallLog.GEOCODED_LOCATION, geocodedLocation); + contentValues.put( + AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME, phoneAccountComponentName); + contentValues.put(AnnotatedCallLog.PHONE_ACCOUNT_ID, phoneAccountId); populatePhoneAccountLabelAndColor( appContext, contentValues, phoneAccountComponentName, phoneAccountId); contentValues.put(AnnotatedCallLog.FEATURES, features); diff --git a/java/com/android/dialer/calllog/datasources/util/RowCombiner.java b/java/com/android/dialer/calllog/datasources/util/RowCombiner.java index adb7a0742..8e9e9c659 100644 --- a/java/com/android/dialer/calllog/datasources/util/RowCombiner.java +++ b/java/com/android/dialer/calllog/datasources/util/RowCombiner.java @@ -43,6 +43,12 @@ public class RowCombiner { return this; } + public RowCombiner useMostRecentBlob(String columnName) { + combinedRow.put( + columnName, individualRowsSortedByTimestampDesc.get(0).getAsByteArray(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(); diff --git a/java/com/android/dialer/calllog/model/CoalescedRow.java b/java/com/android/dialer/calllog/model/CoalescedRow.java new file mode 100644 index 000000000..091467430 --- /dev/null +++ b/java/com/android/dialer/calllog/model/CoalescedRow.java @@ -0,0 +1,147 @@ +/* + * 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.model; + +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.android.dialer.DialerPhoneNumber; +import com.google.auto.value.AutoValue; + +/** Data class containing the contents of a row from the CoalescedAnnotatedCallLog. */ +@AutoValue +public abstract class CoalescedRow { + + public static Builder builder() { + return new AutoValue_CoalescedRow.Builder() + .setId(0) + .setTimestamp(0) + .setNumber(DialerPhoneNumber.getDefaultInstance()) + .setPhotoId(0) + .setIsRead(false) + .setIsNew(false) + .setPhoneAccountColor(0) + .setFeatures(0) + .setIsBusiness(false) + .setIsVoicemail(false) + .setNumberCalls(0) + .setCallType(0); + } + + public abstract int id(); + + public abstract long timestamp(); + + @NonNull + public abstract DialerPhoneNumber number(); + + @Nullable + public abstract String name(); + + @Nullable + public abstract String formattedNumber(); + + @Nullable + public abstract String photoUri(); + + public abstract long photoId(); + + @Nullable + public abstract String lookupUri(); + + @Nullable + public abstract String numberTypeLabel(); + + public abstract boolean isRead(); + + public abstract boolean isNew(); + + @Nullable + public abstract String geocodedLocation(); + + @Nullable + public abstract String phoneAccountComponentName(); + + @Nullable + public abstract String phoneAccountId(); + + @Nullable + public abstract String phoneAccountLabel(); + + @ColorInt + public abstract int phoneAccountColor(); + + public abstract int features(); + + public abstract boolean isBusiness(); + + public abstract boolean isVoicemail(); + + public abstract int callType(); + + public abstract int numberCalls(); + + /** Builder for {@link CoalescedRow}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setId(int id); + + public abstract Builder setTimestamp(long timestamp); + + public abstract Builder setNumber(@NonNull DialerPhoneNumber number); + + public abstract Builder setName(@Nullable String name); + + public abstract Builder setFormattedNumber(@Nullable String formattedNumber); + + public abstract Builder setPhotoUri(@Nullable String photoUri); + + public abstract Builder setPhotoId(long photoId); + + public abstract Builder setLookupUri(@Nullable String lookupUri); + + public abstract Builder setNumberTypeLabel(@Nullable String numberTypeLabel); + + public abstract Builder setIsRead(boolean isRead); + + public abstract Builder setIsNew(boolean isNew); + + public abstract Builder setGeocodedLocation(@Nullable String geocodedLocation); + + public abstract Builder setPhoneAccountComponentName( + @Nullable String phoneAccountComponentName); + + public abstract Builder setPhoneAccountId(@Nullable String phoneAccountId); + + public abstract Builder setPhoneAccountLabel(@Nullable String phoneAccountLabel); + + public abstract Builder setPhoneAccountColor(@ColorInt int phoneAccountColor); + + public abstract Builder setFeatures(int features); + + public abstract Builder setIsBusiness(boolean isBusiness); + + public abstract Builder setIsVoicemail(boolean isVoicemail); + + public abstract Builder setCallType(int callType); + + public abstract Builder setNumberCalls(int numberCalls); + + public abstract CoalescedRow build(); + } +} diff --git a/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java b/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java index 13a801ac8..9f635439a 100644 --- a/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java +++ b/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java @@ -18,115 +18,37 @@ package com.android.dialer.calllog.ui; import android.content.Context; import android.database.Cursor; -import android.support.annotation.ColorInt; import android.support.v4.content.CursorLoader; +import com.android.dialer.DialerPhoneNumber; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog; +import com.android.dialer.calllog.model.CoalescedRow; +import com.google.protobuf.InvalidProtocolBufferException; /** CursorLoader for the coalesced annotated call log. */ final class CoalescedAnnotatedCallLogCursorLoader extends CursorLoader { - /** Indexes for CoalescedAnnotatedCallLog.ALL_COLUMNS */ + // Indexes for CoalescedAnnotatedCallLog.ALL_COLUMNS private static final int ID = 0; - private static final int TIMESTAMP = 1; private static final int NAME = 2; - private static final int FORMATTED_NUMBER = 3; - private static final int PHOTO_URI = 4; - private static final int PHOTO_ID = 5; - private static final int LOOKUP_URI = 6; - private static final int NUMBER_TYPE_LABEL = 7; - private static final int IS_READ = 8; - private static final int NEW = 9; - private static final int GEOCODED_LOCATION = 10; - private static final int PHONE_ACCOUNT_LABEL = 11; - private static final int PHONE_ACCOUNT_COLOR = 12; - private static final int FEATURES = 13; - private static final int IS_BUSINESS = 14; - private static final int IS_VOICEMAIL = 15; - private static final int TYPE = 16; - private static final int NUMBER_CALLS = 17; - - /** Convenience class for accessing values using an abbreviated syntax. */ - static final class Row { - private final Cursor cursor; - - Row(Cursor cursor) { - this.cursor = cursor; - } - - long id() { - return cursor.getInt(ID); - } - - long timestamp() { - return cursor.getLong(TIMESTAMP); - } - - String name() { - return cursor.getString(NAME); - } - - String formattedNumber() { - return cursor.getString(FORMATTED_NUMBER); - } - - String photoUri() { - return cursor.getString(PHOTO_URI); - } - - long photoId() { - return cursor.getLong(PHOTO_ID); - } - - String lookupUri() { - return cursor.getString(LOOKUP_URI); - } - - String numberTypeLabel() { - return cursor.getString(NUMBER_TYPE_LABEL); - } - - boolean isRead() { - return cursor.getInt(IS_READ) == 1; - } - - boolean isNew() { - return cursor.getInt(NEW) == 1; - } - - String geocodedLocation() { - return cursor.getString(GEOCODED_LOCATION); - } - - String phoneAccountLabel() { - return cursor.getString(PHONE_ACCOUNT_LABEL); - } - - @ColorInt - int phoneAccountColor() { - return cursor.getInt(PHONE_ACCOUNT_COLOR); - } - - int features() { - return cursor.getInt(FEATURES); - } - - boolean isBusiness() { - return cursor.getInt(IS_BUSINESS) == 1; - } - - boolean isVoicemail() { - return cursor.getInt(IS_VOICEMAIL) == 1; - } - - int numberCalls() { - return cursor.getInt(NUMBER_CALLS); - } - - int callType() { - return cursor.getInt(TYPE); - } - } + private static final int NUMBER = 3; + private static final int FORMATTED_NUMBER = 4; + private static final int PHOTO_URI = 5; + private static final int PHOTO_ID = 6; + private static final int LOOKUP_URI = 7; + private static final int NUMBER_TYPE_LABEL = 8; + private static final int IS_READ = 9; + private static final int NEW = 10; + private static final int GEOCODED_LOCATION = 11; + private static final int PHONE_ACCOUNT_COMPONENT_NAME = 12; + private static final int PHONE_ACCOUNT_ID = 13; + private static final int PHONE_ACCOUNT_LABEL = 14; + private static final int PHONE_ACCOUNT_COLOR = 15; + private static final int FEATURES = 16; + private static final int IS_BUSINESS = 17; + private static final int IS_VOICEMAIL = 18; + private static final int CALL_TYPE = 19; + private static final int NUMBER_CALLS = 20; CoalescedAnnotatedCallLogCursorLoader(Context context) { // CoalescedAnnotatedCallLog requires that PROJECTION be ALL_COLUMNS and the following params be @@ -139,4 +61,42 @@ final class CoalescedAnnotatedCallLogCursorLoader extends CursorLoader { null, null); } + + /** Creates a new {@link CoalescedRow} from the provided cursor using the current position. */ + static CoalescedRow toRow(Cursor cursor) { + DialerPhoneNumber number; + try { + number = DialerPhoneNumber.parseFrom(cursor.getBlob(NUMBER)); + } catch (InvalidProtocolBufferException e) { + throw new IllegalStateException("Couldn't parse DialerPhoneNumber bytes"); + } + + return CoalescedRow.builder() + .setId(cursor.getInt(ID)) + .setTimestamp(cursor.getLong(TIMESTAMP)) + .setName(cursor.getString(NAME)) + .setNumber(number) + .setFormattedNumber(cursor.getString(FORMATTED_NUMBER)) + .setPhotoUri(cursor.getString(PHOTO_URI)) + .setPhotoId(cursor.getLong(PHOTO_ID)) + .setLookupUri(cursor.getString(LOOKUP_URI)) + .setNumberTypeLabel(cursor.getString(NUMBER_TYPE_LABEL)) + .setIsRead(cursor.getInt(IS_READ) == 1) + .setIsNew(cursor.getInt(NEW) == 1) + .setGeocodedLocation(cursor.getString(GEOCODED_LOCATION)) + .setPhoneAccountComponentName(cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME)) + .setPhoneAccountId(cursor.getString(PHONE_ACCOUNT_ID)) + .setPhoneAccountLabel(cursor.getString(PHONE_ACCOUNT_LABEL)) + .setPhoneAccountColor(cursor.getInt(PHONE_ACCOUNT_COLOR)) + .setFeatures(cursor.getInt(FEATURES)) + .setIsBusiness(cursor.getInt(IS_BUSINESS) == 1) + .setIsVoicemail(cursor.getInt(IS_VOICEMAIL) == 1) + .setCallType(cursor.getInt(CALL_TYPE)) + .setNumberCalls(cursor.getInt(NUMBER_CALLS)) + .build(); + } + + static long getTimestamp(Cursor cursor) { + return cursor.getLong(TIMESTAMP); + } } diff --git a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java index b922a6e3b..d5cfb7e24 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java @@ -58,15 +58,14 @@ final class NewCallLogAdapter extends RecyclerView.Adapter { // 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())) { + long firstTimestamp = CoalescedAnnotatedCallLogCursorLoader.getTimestamp(cursor); + if (CallLogDates.isSameDay(currentTimeMillis, firstTimestamp)) { 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())) { + long timestamp = CoalescedAnnotatedCallLogCursorLoader.getTimestamp(cursor); + + if (CallLogDates.isSameDay(currentTimeMillis, timestamp)) { adapterPosition++; } else { this.olderHeaderPosition = adapterPosition; diff --git a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java index 8ac419e56..4e59301ce 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java @@ -16,16 +16,19 @@ package com.android.dialer.calllog.ui; import android.content.Context; +import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.provider.CallLog.Calls; import android.support.v7.widget.RecyclerView; -import android.text.TextUtils; import android.view.View; +import android.widget.ImageView; import android.widget.QuickContactBadge; import android.widget.TextView; -import com.android.dialer.calllog.ui.CoalescedAnnotatedCallLogCursorLoader.Row; -import com.android.dialer.calllogutils.CallLogDates; +import com.android.dialer.calllog.model.CoalescedRow; +import com.android.dialer.calllog.ui.menu.NewCallLogMenu; +import com.android.dialer.calllogutils.CallLogEntryText; +import com.android.dialer.calllogutils.CallLogIntents; import com.android.dialer.calllogutils.CallTypeIconsView; import com.android.dialer.contactphoto.ContactPhotoManager; import com.android.dialer.lettertile.LetterTileDrawable; @@ -43,6 +46,8 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { private final CallTypeIconsView primaryCallTypeIconsView; // Used for Wifi, HD icons private final CallTypeIconsView secondaryCallTypeIconsView; // Used for call types private final TextView phoneAccountView; + private final ImageView menuButton; + private final Clock clock; NewCallLogViewHolder(View view, Clock clock) { @@ -54,17 +59,18 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { primaryCallTypeIconsView = view.findViewById(R.id.primary_call_type_icons); secondaryCallTypeIconsView = view.findViewById(R.id.secondary_call_type_icons); phoneAccountView = view.findViewById(R.id.phone_account); + menuButton = view.findViewById(R.id.menu_button); + this.clock = clock; } /** @param cursor a cursor from {@link CoalescedAnnotatedCallLogCursorLoader}. */ void bind(Cursor cursor) { - CoalescedAnnotatedCallLogCursorLoader.Row row = - new CoalescedAnnotatedCallLogCursorLoader.Row(cursor); + CoalescedRow row = CoalescedAnnotatedCallLogCursorLoader.toRow(cursor); // TODO(zachh): Handle RTL properly. - primaryTextView.setText(buildPrimaryText(row)); - secondaryTextView.setText(buildSecondaryText(row)); + primaryTextView.setText(CallLogEntryText.buildPrimaryText(context, row)); + secondaryTextView.setText(CallLogEntryText.buildSecondaryTextForEntries(context, clock, row)); if (isNewMissedCall(row)) { primaryTextView.setTextAppearance(R.style.primary_textview_new_call); @@ -72,77 +78,29 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { secondaryTextView.setTextAppearance(R.style.secondary_textview_new_call); } + setNumberCalls(row); setPhoto(row); setPrimaryCallTypes(row); setSecondaryCallTypes(row); setPhoneAccounts(row); + setOnClickListenerForRow(row); + setOnClickListenerForMenuButon(row); } - private String buildPrimaryText(CoalescedAnnotatedCallLogCursorLoader.Row row) { - StringBuilder primaryText = new StringBuilder(); - if (!TextUtils.isEmpty(row.name())) { - primaryText.append(row.name()); - } else if (!TextUtils.isEmpty(row.formattedNumber())) { - primaryText.append(row.formattedNumber()); - } else { - // TODO(zachh): Handle CallLog.Calls.PRESENTATION_*, including Verizon restricted numbers. - primaryText.append(context.getText(R.string.new_call_log_unknown)); - } + private void setNumberCalls(CoalescedRow row) { + // TODO(zachh): Number of calls shouldn't be text, but a circle with a number inside. if (row.numberCalls() > 1) { - primaryText.append(String.format(Locale.getDefault(), " (%d)", row.numberCalls())); + primaryTextView.append(String.format(Locale.getDefault(), " (%d)", row.numberCalls())); } - return primaryText.toString(); } - private boolean isNewMissedCall(CoalescedAnnotatedCallLogCursorLoader.Row row) { + private boolean isNewMissedCall(CoalescedRow row) { // Show missed call styling if the most recent call in the group was missed and it is still // marked as NEW. It is not clear what IS_READ should be used for and it is currently not used. return row.callType() == Calls.MISSED_TYPE && row.isNew(); } - 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(Row row) { + private void setPhoto(CoalescedRow row) { // TODO(zachh): Set the contact type. ContactPhotoManager.getInstance(context) .loadDialerThumbnailOrPhoto( @@ -154,7 +112,7 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { LetterTileDrawable.TYPE_DEFAULT); } - private void setPrimaryCallTypes(Row row) { + private void setPrimaryCallTypes(CoalescedRow row) { // Only HD and Wifi icons are shown following the primary text. primaryCallTypeIconsView.setShowHd( MotorolaUtils.shouldShowHdIconInCallLog(context, row.features())); @@ -162,18 +120,32 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { MotorolaUtils.shouldShowWifiIconInCallLog(context, row.features())); } - private void setSecondaryCallTypes(Row row) { + private void setSecondaryCallTypes(CoalescedRow row) { // Only call type icon is shown before the secondary text. secondaryCallTypeIconsView.add(row.callType()); // TODO(zachh): Per new mocks, may need to add method to CallTypeIconsView to disable coloring. } - private void setPhoneAccounts(Row row) { + private void setPhoneAccounts(CoalescedRow row) { if (row.phoneAccountLabel() != null) { phoneAccountView.setText(row.phoneAccountLabel()); phoneAccountView.setTextColor(row.phoneAccountColor()); phoneAccountView.setVisibility(View.VISIBLE); } } + + private void setOnClickListenerForRow(CoalescedRow row) { + itemView.setOnClickListener( + (view) -> { + Intent callbackIntent = CallLogIntents.getCallBackIntent(row); + if (callbackIntent != null) { + context.startActivity(callbackIntent); + } + }); + } + + private void setOnClickListenerForMenuButon(CoalescedRow row) { + menuButton.setOnClickListener(NewCallLogMenu.createOnClickListener(context, row)); + } } diff --git a/java/com/android/dialer/calllog/ui/menu/AndroidManifest.xml b/java/com/android/dialer/calllog/ui/menu/AndroidManifest.xml new file mode 100644 index 000000000..0d8274dff --- /dev/null +++ b/java/com/android/dialer/calllog/ui/menu/AndroidManifest.xml @@ -0,0 +1,16 @@ + + diff --git a/java/com/android/dialer/calllog/ui/menu/Modules.java b/java/com/android/dialer/calllog/ui/menu/Modules.java new file mode 100644 index 000000000..8de63518c --- /dev/null +++ b/java/com/android/dialer/calllog/ui/menu/Modules.java @@ -0,0 +1,234 @@ +/* + * 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.menu; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import com.android.dialer.calldetails.CallDetailsActivity; +import com.android.dialer.calldetails.CallDetailsEntries; +import com.android.dialer.callintent.CallInitiationType; +import com.android.dialer.calllog.model.CoalescedRow; +import com.android.dialer.calllogutils.PhoneAccountUtils; +import com.android.dialer.clipboard.ClipboardUtils; +import com.android.dialer.contactactions.ContactActionModule; +import com.android.dialer.contactactions.DividerModule; +import com.android.dialer.contactactions.IntentModule; +import com.android.dialer.dialercontact.DialerContact; +import com.android.dialer.lettertile.LetterTileDrawable; +import com.android.dialer.util.IntentUtil; +import com.android.dialer.util.UriUtils; +import java.util.ArrayList; +import java.util.List; + +/** + * Configures the modules for the bottom sheet; these are the rows below the top row (primary + * action) in the bottom sheet. + */ +final class Modules { + + static List fromRow(Context context, CoalescedRow row) { + // Conditionally add each module, which are items in the bottom sheet's menu. + List modules = new ArrayList<>(); + + maybeAddModuleForVideoOrAudioCall(context, row, modules); + maybeAddModuleForAddingToContacts(context, row, modules); + + String originalNumber = row.number().getRawInput().getNumber(); + maybeAddModuleForSendingTextMessage(context, originalNumber, modules); + + if (!modules.isEmpty()) { + modules.add(new DividerModule()); + } + + // TODO(zachh): Module for blocking/unblocking spam. + // TODO(zachh): Module for CallComposer. + maybeAddModuleForCopyingNumber(context, originalNumber, modules); + + // TODO(zachh): Revisit if DialerContact is the best thing to pass to CallDetails; could + // it use a ContactPrimaryActionInfo instead? + addModuleForAccessingCallDetails(context, createDialerContactFromRow(row), modules); + + return modules; + } + + private static void maybeAddModuleForVideoOrAudioCall( + Context context, CoalescedRow row, List modules) { + String originalNumber = row.number().getRawInput().getNumber(); + if (TextUtils.isEmpty(originalNumber)) { + // Skip adding the menu item if the phone number is unknown. + return; + } + + PhoneAccountHandle phoneAccountHandle = + PhoneAccountUtils.getAccount(row.phoneAccountComponentName(), row.phoneAccountId()); + + if ((row.features() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) { + // Add an audio call item for video calls. Clicking the top entry on the bottom sheet will + // trigger a video call. + modules.add( + IntentModule.newCallModule( + context, originalNumber, phoneAccountHandle, CallInitiationType.Type.CALL_LOG)); + } else { + // Add a video call item for audio calls. Click the top entry on the bottom sheet will + // trigger an audio call. + // TODO(zachh): Only show video option if video capabilities present? + modules.add( + IntentModule.newVideoCallModule( + context, originalNumber, phoneAccountHandle, CallInitiationType.Type.CALL_LOG)); + } + } + + private static void maybeAddModuleForAddingToContacts( + Context context, CoalescedRow row, List modules) { + // TODO(zachh): Only show this for non-spam/blocked numbers. + + // Skip showing the menu item for existing contacts. + if (isExistingContact(row)) { + return; + } + + // Skip showing the menu item if there is no number. + String originalNumber = row.number().getRawInput().getNumber(); + if (TextUtils.isEmpty(originalNumber)) { + return; + } + + Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); + intent.putExtra(ContactsContract.Intents.Insert.PHONE, originalNumber); + + if (!TextUtils.isEmpty(row.name())) { + intent.putExtra(ContactsContract.Intents.Insert.NAME, row.name()); + } + modules.add( + new IntentModule( + context, + intent, + R.string.add_to_contacts, + R.drawable.quantum_ic_person_add_vd_theme_24)); + } + + /** + * Lookup URIs are currently fetched from the cached column of the system call log. This URI + * contains encoded information for non-contacts for the purposes of populating contact cards. + * + *

We infer whether a contact is existing or not by checking if the lookup URI is "encoded" or + * not. + * + *

TODO(zachh): We should revisit this once the contact URI is no longer being read from the + * cached column in the system database, in case we decide not to overload the column. + */ + private static boolean isExistingContact(CoalescedRow row) { + return !TextUtils.isEmpty(row.lookupUri()) + && !UriUtils.isEncodedContactUri(Uri.parse(row.lookupUri())); + } + + private static void maybeAddModuleForSendingTextMessage( + Context context, String originalNumber, List modules) { + // TODO(zachh): There are some conditions where this module should not be shown; consider + // voicemail, business numbers, blocked numbers, spam numbers, etc. + if (!TextUtils.isEmpty(originalNumber)) { + modules.add( + new IntentModule( + context, + IntentUtil.getSendSmsIntent(originalNumber), + R.string.send_a_message, + R.drawable.quantum_ic_message_vd_theme_24)); + } + } + + private static void maybeAddModuleForCopyingNumber( + Context context, String originalNumber, List modules) { + if (TextUtils.isEmpty(originalNumber)) { + return; + } + modules.add( + new ContactActionModule() { + @Override + public int getStringId() { + return R.string.copy_number; + } + + @Override + public int getDrawableId() { + return R.drawable.quantum_ic_content_copy_vd_theme_24; + } + + @Override + public boolean onClick() { + ClipboardUtils.copyText(context, null, originalNumber, true); + return false; + } + }); + } + + private static void addModuleForAccessingCallDetails( + Context context, DialerContact dialerContact, List modules) { + // TODO(zachh): Load CallDetailsEntries and canReportInaccurateNumber in + // CallDetailsActivity (see also isPeopleApiSource(sourceType)). + CallDetailsEntries callDetailsEntries = CallDetailsEntries.getDefaultInstance(); + boolean canReportInaccurateNumber = false; + boolean canSupportAssistedDialing = false; // TODO(zachh): Properly set value. + + modules.add( + new IntentModule( + context, + CallDetailsActivity.newInstance( + context, + callDetailsEntries, + dialerContact, + canReportInaccurateNumber, + canSupportAssistedDialing), + R.string.call_details, + R.drawable.quantum_ic_info_outline_vd_theme_24)); + } + + private static DialerContact createDialerContactFromRow(CoalescedRow row) { + // TODO(zachh): Do something with parsed values to make more dialable? + String originalNumber = row.number().getRawInput().getNumber(); + + DialerContact.Builder dialerContactBuilder = + DialerContact.newBuilder() + .setNumber(originalNumber) + .setContactType(LetterTileDrawable.TYPE_DEFAULT) // TODO(zachh): Use proper type. + .setPhotoId(row.photoId()); + + if (!TextUtils.isEmpty(row.name())) { + dialerContactBuilder.setNameOrNumber(row.name()); + } else if (!TextUtils.isEmpty(originalNumber)) { + dialerContactBuilder.setNameOrNumber(originalNumber); + } + if (row.numberTypeLabel() != null) { + dialerContactBuilder.setNumberLabel(row.numberTypeLabel()); + } + if (row.photoUri() != null) { + dialerContactBuilder.setPhotoUri(row.photoUri()); + } + if (row.lookupUri() != null) { + dialerContactBuilder.setContactUri(row.lookupUri()); + } + if (row.formattedNumber() != null) { + dialerContactBuilder.setDisplayNumber(row.formattedNumber()); + } + return dialerContactBuilder.build(); + } +} diff --git a/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java b/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java new file mode 100644 index 000000000..2ae823e7f --- /dev/null +++ b/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java @@ -0,0 +1,33 @@ +/* + * 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.menu; + +import android.content.Context; +import android.view.View; +import com.android.dialer.calllog.model.CoalescedRow; +import com.android.dialer.contactactions.ContactActionBottomSheet; + +/** Handles configuration of the bottom sheet menus for call log entries. */ +public final class NewCallLogMenu { + + /** Creates and returns the OnClickListener which opens the menu for the provided row. */ + public static View.OnClickListener createOnClickListener(Context context, CoalescedRow row) { + return (view) -> + ContactActionBottomSheet.show( + context, PrimaryAction.fromRow(context, row), Modules.fromRow(context, row)); + } +} diff --git a/java/com/android/dialer/calllog/ui/menu/PrimaryAction.java b/java/com/android/dialer/calllog/ui/menu/PrimaryAction.java new file mode 100644 index 000000000..7077d0231 --- /dev/null +++ b/java/com/android/dialer/calllog/ui/menu/PrimaryAction.java @@ -0,0 +1,48 @@ +/* + * 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.menu; + +import android.content.Context; +import android.provider.CallLog.Calls; +import com.android.dialer.calllog.model.CoalescedRow; +import com.android.dialer.calllogutils.CallLogEntryText; +import com.android.dialer.calllogutils.CallLogIntents; +import com.android.dialer.contactactions.ContactPrimaryActionInfo; +import com.android.dialer.contactactions.ContactPrimaryActionInfo.PhotoInfo; +import com.android.dialer.lettertile.LetterTileDrawable; + +/** Configures the primary action row (top row) for the bottom sheet. */ +final class PrimaryAction { + + static ContactPrimaryActionInfo fromRow(Context context, CoalescedRow row) { + return ContactPrimaryActionInfo.builder() + .setNumber(row.number()) + .setPhotoInfo( + PhotoInfo.builder() + .setPhotoId(row.photoId()) + .setPhotoUri(row.photoUri()) + .setLookupUri(row.lookupUri()) + .setIsVideo((row.features() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) + .setContactType(LetterTileDrawable.TYPE_DEFAULT) // TODO(zachh): Use proper type. + .setDisplayName(row.name()) + .build()) + .setPrimaryText(CallLogEntryText.buildPrimaryText(context, row)) + .setSecondaryText(CallLogEntryText.buildSecondaryTextForBottomSheet(context, row)) + .setIntent(CallLogIntents.getCallBackIntent(row)) + .build(); + } +} diff --git a/java/com/android/dialer/calllog/ui/menu/res/values/strings.xml b/java/com/android/dialer/calllog/ui/menu/res/values/strings.xml new file mode 100644 index 000000000..aaa7da04a --- /dev/null +++ b/java/com/android/dialer/calllog/ui/menu/res/values/strings.xml @@ -0,0 +1,35 @@ + + + + + + + Add to contacts + + + Copy number + + + Send a message + + + Call details + + \ No newline at end of file diff --git a/java/com/android/dialer/calllog/ui/res/values/strings.xml b/java/com/android/dialer/calllog/ui/res/values/strings.xml index 9b044ca08..0ef0eaf26 100644 --- a/java/com/android/dialer/calllog/ui/res/values/strings.xml +++ b/java/com/android/dialer/calllog/ui/res/values/strings.xml @@ -17,12 +17,6 @@ - - Video - - - Unknown - Today -- cgit v1.2.3