346 lines
14 KiB
Java
346 lines
14 KiB
Java
|
/*
|
||
|
* 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.app.Activity;
|
||
|
import android.content.res.ColorStateList;
|
||
|
import android.provider.CallLog.Calls;
|
||
|
import android.support.annotation.ColorInt;
|
||
|
import android.support.annotation.DrawableRes;
|
||
|
import android.support.v7.widget.RecyclerView;
|
||
|
import android.telecom.PhoneAccount;
|
||
|
import android.telecom.PhoneAccountHandle;
|
||
|
import android.text.TextUtils;
|
||
|
import android.view.View;
|
||
|
import android.view.View.AccessibilityDelegate;
|
||
|
import android.view.accessibility.AccessibilityNodeInfo;
|
||
|
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
|
||
|
import android.widget.ImageView;
|
||
|
import android.widget.TextView;
|
||
|
import com.android.dialer.calllog.model.CoalescedRow;
|
||
|
import com.android.dialer.calllog.ui.NewCallLogAdapter.PopCounts;
|
||
|
import com.android.dialer.calllog.ui.menu.NewCallLogMenu;
|
||
|
import com.android.dialer.calllogutils.CallLogEntryDescriptions;
|
||
|
import com.android.dialer.calllogutils.CallLogEntryText;
|
||
|
import com.android.dialer.calllogutils.CallLogRowActions;
|
||
|
import com.android.dialer.calllogutils.PhoneAccountUtils;
|
||
|
import com.android.dialer.calllogutils.PhotoInfoBuilder;
|
||
|
import com.android.dialer.common.concurrent.DialerExecutorComponent;
|
||
|
import com.android.dialer.compat.telephony.TelephonyManagerCompat;
|
||
|
import com.android.dialer.oem.MotorolaUtils;
|
||
|
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
|
||
|
import com.android.dialer.telecom.TelecomUtil;
|
||
|
import com.android.dialer.time.Clock;
|
||
|
import com.android.dialer.widget.ContactPhotoView;
|
||
|
import com.google.common.util.concurrent.FutureCallback;
|
||
|
import com.google.common.util.concurrent.Futures;
|
||
|
import java.util.Locale;
|
||
|
import java.util.concurrent.ExecutorService;
|
||
|
|
||
|
/** {@link RecyclerView.ViewHolder} for the new call log. */
|
||
|
final class NewCallLogViewHolder extends RecyclerView.ViewHolder {
|
||
|
|
||
|
private final Activity activity;
|
||
|
private final ContactPhotoView contactPhotoView;
|
||
|
private final TextView primaryTextView;
|
||
|
private final TextView callCountTextView;
|
||
|
private final TextView secondaryTextView;
|
||
|
private final ImageView callTypeIcon;
|
||
|
private final ImageView hdIcon;
|
||
|
private final ImageView wifiIcon;
|
||
|
private final ImageView assistedDialIcon;
|
||
|
private final TextView phoneAccountView;
|
||
|
private final ImageView callButton;
|
||
|
private final View callLogEntryRootView;
|
||
|
|
||
|
private final Clock clock;
|
||
|
private final RealtimeRowProcessor realtimeRowProcessor;
|
||
|
private final ExecutorService uiExecutorService;
|
||
|
private final PopCounts popCounts;
|
||
|
|
||
|
private long currentRowId;
|
||
|
|
||
|
NewCallLogViewHolder(
|
||
|
Activity activity,
|
||
|
View view,
|
||
|
Clock clock,
|
||
|
RealtimeRowProcessor realtimeRowProcessor,
|
||
|
PopCounts popCounts) {
|
||
|
super(view);
|
||
|
this.activity = activity;
|
||
|
callLogEntryRootView = view;
|
||
|
contactPhotoView = view.findViewById(R.id.contact_photo_view);
|
||
|
primaryTextView = view.findViewById(R.id.primary_text);
|
||
|
callCountTextView = view.findViewById(R.id.call_count);
|
||
|
secondaryTextView = view.findViewById(R.id.secondary_text);
|
||
|
callTypeIcon = view.findViewById(R.id.call_type_icon);
|
||
|
hdIcon = view.findViewById(R.id.hd_icon);
|
||
|
wifiIcon = view.findViewById(R.id.wifi_icon);
|
||
|
assistedDialIcon = view.findViewById(R.id.assisted_dial_icon);
|
||
|
phoneAccountView = view.findViewById(R.id.phone_account);
|
||
|
callButton = view.findViewById(R.id.call_button);
|
||
|
|
||
|
this.clock = clock;
|
||
|
this.realtimeRowProcessor = realtimeRowProcessor;
|
||
|
this.popCounts = popCounts;
|
||
|
uiExecutorService = DialerExecutorComponent.get(activity).uiExecutor();
|
||
|
}
|
||
|
|
||
|
void bind(CoalescedRow coalescedRow) {
|
||
|
// The row ID is used to make sure async updates are applied to the correct views.
|
||
|
currentRowId = coalescedRow.getId();
|
||
|
|
||
|
// Even if there is additional real time processing necessary, we still want to immediately show
|
||
|
// what information we have, rather than an empty card. For example, if CP2 information needs to
|
||
|
// be queried on the fly, we can still show the phone number until the contact name loads.
|
||
|
displayRow(coalescedRow);
|
||
|
configA11yForRow(coalescedRow);
|
||
|
|
||
|
// Note: This leaks the view holder via the callback (which is an inner class), but this is OK
|
||
|
// because we only create ~10 of them (and they'll be collected assuming all jobs finish).
|
||
|
Futures.addCallback(
|
||
|
realtimeRowProcessor.applyRealtimeProcessing(coalescedRow),
|
||
|
new RealtimeRowFutureCallback(coalescedRow),
|
||
|
uiExecutorService);
|
||
|
}
|
||
|
|
||
|
private void displayRow(CoalescedRow row) {
|
||
|
// TODO(zachh): Handle RTL properly.
|
||
|
primaryTextView.setText(CallLogEntryText.buildPrimaryText(activity, row));
|
||
|
secondaryTextView.setText(CallLogEntryText.buildSecondaryTextForEntries(activity, clock, row));
|
||
|
|
||
|
if (isUnreadMissedCall(row)) {
|
||
|
primaryTextView.setTextAppearance(R.style.primary_textview_unread_call);
|
||
|
callCountTextView.setTextAppearance(R.style.primary_textview_unread_call);
|
||
|
secondaryTextView.setTextAppearance(R.style.secondary_textview_unread_call);
|
||
|
phoneAccountView.setTextAppearance(R.style.phoneaccount_textview_unread_call);
|
||
|
} else {
|
||
|
primaryTextView.setTextAppearance(R.style.primary_textview);
|
||
|
callCountTextView.setTextAppearance(R.style.primary_textview);
|
||
|
secondaryTextView.setTextAppearance(R.style.secondary_textview);
|
||
|
phoneAccountView.setTextAppearance(R.style.phoneaccount_textview);
|
||
|
}
|
||
|
|
||
|
setNumberCalls(row);
|
||
|
setPhoto(row);
|
||
|
setFeatureIcons(row);
|
||
|
setCallTypeIcon(row);
|
||
|
setPhoneAccounts(row);
|
||
|
setCallButon(row);
|
||
|
|
||
|
itemView.setOnClickListener(NewCallLogMenu.createOnClickListener(activity, row));
|
||
|
}
|
||
|
|
||
|
private void configA11yForRow(CoalescedRow row) {
|
||
|
callLogEntryRootView.setContentDescription(
|
||
|
CallLogEntryDescriptions.buildDescriptionForEntry(activity, clock, row));
|
||
|
|
||
|
// Inform a11y users that double tapping an entry now makes a call.
|
||
|
// This will instruct TalkBack to say "double tap to call" instead of
|
||
|
// "double tap to activate".
|
||
|
callLogEntryRootView.setAccessibilityDelegate(
|
||
|
new AccessibilityDelegate() {
|
||
|
@Override
|
||
|
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
|
||
|
super.onInitializeAccessibilityNodeInfo(host, info);
|
||
|
info.addAction(
|
||
|
new AccessibilityAction(
|
||
|
AccessibilityNodeInfo.ACTION_CLICK,
|
||
|
activity
|
||
|
.getResources()
|
||
|
.getString(R.string.a11y_new_call_log_entry_tap_action)));
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
private void setNumberCalls(CoalescedRow row) {
|
||
|
int numberCalls = row.getCoalescedIds().getCoalescedIdCount();
|
||
|
if (numberCalls > 1) {
|
||
|
callCountTextView.setText(String.format(Locale.getDefault(), "(%d)", numberCalls));
|
||
|
callCountTextView.setVisibility(View.VISIBLE);
|
||
|
} else {
|
||
|
callCountTextView.setVisibility(View.GONE);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private boolean isUnreadMissedCall(CoalescedRow row) {
|
||
|
// Show missed call styling if the most recent call in the group was missed and it is still
|
||
|
// marked as not read. The "NEW" column is presumably used for notifications and voicemails
|
||
|
// only.
|
||
|
return row.getCallType() == Calls.MISSED_TYPE && !row.getIsRead();
|
||
|
}
|
||
|
|
||
|
private void setPhoto(CoalescedRow row) {
|
||
|
contactPhotoView.setPhoto(PhotoInfoBuilder.fromCoalescedRow(activity, row).build());
|
||
|
}
|
||
|
|
||
|
private void setFeatureIcons(CoalescedRow row) {
|
||
|
ColorStateList colorStateList =
|
||
|
ColorStateList.valueOf(
|
||
|
activity.getColor(
|
||
|
isUnreadMissedCall(row)
|
||
|
? R.color.feature_icon_unread_color
|
||
|
: R.color.feature_icon_read_color));
|
||
|
|
||
|
// Handle HD Icon
|
||
|
if ((row.getFeatures() & Calls.FEATURES_HD_CALL) == Calls.FEATURES_HD_CALL) {
|
||
|
hdIcon.setVisibility(View.VISIBLE);
|
||
|
hdIcon.setImageTintList(colorStateList);
|
||
|
} else {
|
||
|
hdIcon.setVisibility(View.GONE);
|
||
|
}
|
||
|
|
||
|
// Handle Wifi Icon
|
||
|
if (MotorolaUtils.shouldShowWifiIconInCallLog(activity, row.getFeatures())) {
|
||
|
wifiIcon.setVisibility(View.VISIBLE);
|
||
|
wifiIcon.setImageTintList(colorStateList);
|
||
|
} else {
|
||
|
wifiIcon.setVisibility(View.GONE);
|
||
|
}
|
||
|
|
||
|
// Handle Assisted Dialing Icon
|
||
|
if ((row.getFeatures() & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING)
|
||
|
== TelephonyManagerCompat.FEATURES_ASSISTED_DIALING) {
|
||
|
assistedDialIcon.setVisibility(View.VISIBLE);
|
||
|
assistedDialIcon.setImageTintList(colorStateList);
|
||
|
} else {
|
||
|
assistedDialIcon.setVisibility(View.GONE);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void setCallTypeIcon(CoalescedRow row) {
|
||
|
@DrawableRes int resId;
|
||
|
switch (row.getCallType()) {
|
||
|
case Calls.INCOMING_TYPE:
|
||
|
case Calls.ANSWERED_EXTERNALLY_TYPE:
|
||
|
resId = R.drawable.quantum_ic_call_received_vd_theme_24;
|
||
|
break;
|
||
|
case Calls.OUTGOING_TYPE:
|
||
|
resId = R.drawable.quantum_ic_call_made_vd_theme_24;
|
||
|
break;
|
||
|
case Calls.MISSED_TYPE:
|
||
|
resId = R.drawable.quantum_ic_call_missed_vd_theme_24;
|
||
|
break;
|
||
|
case Calls.VOICEMAIL_TYPE:
|
||
|
throw new IllegalStateException("Voicemails not expected in call log");
|
||
|
case Calls.BLOCKED_TYPE:
|
||
|
resId = R.drawable.quantum_ic_block_vd_theme_24;
|
||
|
break;
|
||
|
default:
|
||
|
// It is possible for users to end up with calls with unknown call types in their
|
||
|
// call history, possibly due to 3rd party call log implementations (e.g. to
|
||
|
// distinguish between rejected and missed calls). Instead of crashing, just
|
||
|
// assume that all unknown call types are missed calls.
|
||
|
resId = R.drawable.quantum_ic_call_missed_vd_theme_24;
|
||
|
break;
|
||
|
}
|
||
|
callTypeIcon.setImageResource(resId);
|
||
|
|
||
|
if (isUnreadMissedCall(row)) {
|
||
|
callTypeIcon.setImageTintList(
|
||
|
ColorStateList.valueOf(activity.getColor(R.color.call_type_icon_unread_color)));
|
||
|
} else {
|
||
|
callTypeIcon.setImageTintList(
|
||
|
ColorStateList.valueOf(activity.getColor(R.color.call_type_icon_read_color)));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void setPhoneAccounts(CoalescedRow row) {
|
||
|
PhoneAccountHandle phoneAccountHandle =
|
||
|
TelecomUtil.composePhoneAccountHandle(
|
||
|
row.getPhoneAccountComponentName(), row.getPhoneAccountId());
|
||
|
if (phoneAccountHandle == null) {
|
||
|
phoneAccountView.setVisibility(View.GONE);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
String phoneAccountLabel = PhoneAccountUtils.getAccountLabel(activity, phoneAccountHandle);
|
||
|
if (TextUtils.isEmpty(phoneAccountLabel)) {
|
||
|
phoneAccountView.setVisibility(View.GONE);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
@ColorInt
|
||
|
int phoneAccountColor = PhoneAccountUtils.getAccountColor(activity, phoneAccountHandle);
|
||
|
if (phoneAccountColor == PhoneAccount.NO_HIGHLIGHT_COLOR) {
|
||
|
phoneAccountColor =
|
||
|
activity
|
||
|
.getResources()
|
||
|
.getColor(R.color.dialer_secondary_text_color, activity.getTheme());
|
||
|
}
|
||
|
|
||
|
phoneAccountView.setText(phoneAccountLabel);
|
||
|
phoneAccountView.setTextColor(phoneAccountColor);
|
||
|
phoneAccountView.setVisibility(View.VISIBLE);
|
||
|
}
|
||
|
|
||
|
private void setCallButon(CoalescedRow row) {
|
||
|
if (!PhoneNumberHelper.canPlaceCallsTo(
|
||
|
row.getNumber().getNormalizedNumber(), row.getNumberPresentation())) {
|
||
|
callButton.setVisibility(View.GONE);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
callButton.setVisibility(View.VISIBLE);
|
||
|
if ((row.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) {
|
||
|
callButton.setImageResource(R.drawable.quantum_ic_videocam_vd_theme_24);
|
||
|
callButton.setContentDescription(
|
||
|
TextUtils.expandTemplate(
|
||
|
activity.getResources().getText(R.string.a11y_new_call_log_entry_video_call),
|
||
|
CallLogEntryText.buildPrimaryText(activity, row)));
|
||
|
} else {
|
||
|
callButton.setImageResource(R.drawable.quantum_ic_call_vd_theme_24);
|
||
|
callButton.setContentDescription(
|
||
|
TextUtils.expandTemplate(
|
||
|
activity.getResources().getText(R.string.a11y_new_call_log_entry_voice_call),
|
||
|
CallLogEntryText.buildPrimaryText(activity, row)));
|
||
|
}
|
||
|
|
||
|
callButton.setOnClickListener(view -> CallLogRowActions.startCallForRow(activity, row));
|
||
|
}
|
||
|
|
||
|
private class RealtimeRowFutureCallback implements FutureCallback<CoalescedRow> {
|
||
|
private final CoalescedRow originalRow;
|
||
|
|
||
|
RealtimeRowFutureCallback(CoalescedRow originalRow) {
|
||
|
this.originalRow = originalRow;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onSuccess(CoalescedRow updatedRow) {
|
||
|
// If the user scrolled then this ViewHolder may not correspond to the completed task and
|
||
|
// there's nothing to do.
|
||
|
if (originalRow.getId() != currentRowId) {
|
||
|
popCounts.didNotPop++;
|
||
|
return;
|
||
|
}
|
||
|
// Only update the UI if the updated row differs from the original row (which has already
|
||
|
// been displayed).
|
||
|
if (!updatedRow.equals(originalRow)) {
|
||
|
displayRow(updatedRow);
|
||
|
popCounts.popped++;
|
||
|
return;
|
||
|
}
|
||
|
popCounts.didNotPop++;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onFailure(Throwable throwable) {
|
||
|
throw new RuntimeException("realtime processing failed", throwable);
|
||
|
}
|
||
|
}
|
||
|
}
|