1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 package com.android.dialer.calllog.ui;
17 
18 import android.app.Activity;
19 import android.content.res.ColorStateList;
20 import android.provider.CallLog.Calls;
21 import android.support.annotation.ColorInt;
22 import android.support.annotation.DrawableRes;
23 import android.support.v7.widget.RecyclerView;
24 import android.telecom.PhoneAccount;
25 import android.telecom.PhoneAccountHandle;
26 import android.text.TextUtils;
27 import android.view.View;
28 import android.view.View.AccessibilityDelegate;
29 import android.view.accessibility.AccessibilityNodeInfo;
30 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
31 import android.widget.ImageView;
32 import android.widget.TextView;
33 import com.android.dialer.calllog.model.CoalescedRow;
34 import com.android.dialer.calllog.ui.NewCallLogAdapter.PopCounts;
35 import com.android.dialer.calllog.ui.menu.NewCallLogMenu;
36 import com.android.dialer.calllogutils.CallLogEntryDescriptions;
37 import com.android.dialer.calllogutils.CallLogEntryText;
38 import com.android.dialer.calllogutils.CallLogRowActions;
39 import com.android.dialer.calllogutils.PhoneAccountUtils;
40 import com.android.dialer.calllogutils.PhotoInfoBuilder;
41 import com.android.dialer.common.concurrent.DialerExecutorComponent;
42 import com.android.dialer.compat.telephony.TelephonyManagerCompat;
43 import com.android.dialer.oem.MotorolaUtils;
44 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
45 import com.android.dialer.telecom.TelecomUtil;
46 import com.android.dialer.time.Clock;
47 import com.android.dialer.widget.ContactPhotoView;
48 import com.google.common.util.concurrent.FutureCallback;
49 import com.google.common.util.concurrent.Futures;
50 import java.util.Locale;
51 import java.util.concurrent.ExecutorService;
52 
53 /** {@link RecyclerView.ViewHolder} for the new call log. */
54 final class NewCallLogViewHolder extends RecyclerView.ViewHolder {
55 
56   private final Activity activity;
57   private final ContactPhotoView contactPhotoView;
58   private final TextView primaryTextView;
59   private final TextView callCountTextView;
60   private final TextView secondaryTextView;
61   private final ImageView callTypeIcon;
62   private final ImageView hdIcon;
63   private final ImageView wifiIcon;
64   private final ImageView assistedDialIcon;
65   private final TextView phoneAccountView;
66   private final ImageView callButton;
67   private final View callLogEntryRootView;
68 
69   private final Clock clock;
70   private final RealtimeRowProcessor realtimeRowProcessor;
71   private final ExecutorService uiExecutorService;
72   private final PopCounts popCounts;
73 
74   private long currentRowId;
75 
NewCallLogViewHolder( Activity activity, View view, Clock clock, RealtimeRowProcessor realtimeRowProcessor, PopCounts popCounts)76   NewCallLogViewHolder(
77       Activity activity,
78       View view,
79       Clock clock,
80       RealtimeRowProcessor realtimeRowProcessor,
81       PopCounts popCounts) {
82     super(view);
83     this.activity = activity;
84     callLogEntryRootView = view;
85     contactPhotoView = view.findViewById(R.id.contact_photo_view);
86     primaryTextView = view.findViewById(R.id.primary_text);
87     callCountTextView = view.findViewById(R.id.call_count);
88     secondaryTextView = view.findViewById(R.id.secondary_text);
89     callTypeIcon = view.findViewById(R.id.call_type_icon);
90     hdIcon = view.findViewById(R.id.hd_icon);
91     wifiIcon = view.findViewById(R.id.wifi_icon);
92     assistedDialIcon = view.findViewById(R.id.assisted_dial_icon);
93     phoneAccountView = view.findViewById(R.id.phone_account);
94     callButton = view.findViewById(R.id.call_button);
95 
96     this.clock = clock;
97     this.realtimeRowProcessor = realtimeRowProcessor;
98     this.popCounts = popCounts;
99     uiExecutorService = DialerExecutorComponent.get(activity).uiExecutor();
100   }
101 
bind(CoalescedRow coalescedRow)102   void bind(CoalescedRow coalescedRow) {
103     // The row ID is used to make sure async updates are applied to the correct views.
104     currentRowId = coalescedRow.getId();
105 
106     // Even if there is additional real time processing necessary, we still want to immediately show
107     // what information we have, rather than an empty card. For example, if CP2 information needs to
108     // be queried on the fly, we can still show the phone number until the contact name loads.
109     displayRow(coalescedRow);
110     configA11yForRow(coalescedRow);
111 
112     // Note: This leaks the view holder via the callback (which is an inner class), but this is OK
113     // because we only create ~10 of them (and they'll be collected assuming all jobs finish).
114     Futures.addCallback(
115         realtimeRowProcessor.applyRealtimeProcessing(coalescedRow),
116         new RealtimeRowFutureCallback(coalescedRow),
117         uiExecutorService);
118   }
119 
displayRow(CoalescedRow row)120   private void displayRow(CoalescedRow row) {
121     // TODO(zachh): Handle RTL properly.
122     primaryTextView.setText(CallLogEntryText.buildPrimaryText(activity, row));
123     secondaryTextView.setText(CallLogEntryText.buildSecondaryTextForEntries(activity, clock, row));
124 
125     if (isUnreadMissedCall(row)) {
126       primaryTextView.setTextAppearance(R.style.primary_textview_unread_call);
127       callCountTextView.setTextAppearance(R.style.primary_textview_unread_call);
128       secondaryTextView.setTextAppearance(R.style.secondary_textview_unread_call);
129       phoneAccountView.setTextAppearance(R.style.phoneaccount_textview_unread_call);
130     } else {
131       primaryTextView.setTextAppearance(R.style.primary_textview);
132       callCountTextView.setTextAppearance(R.style.primary_textview);
133       secondaryTextView.setTextAppearance(R.style.secondary_textview);
134       phoneAccountView.setTextAppearance(R.style.phoneaccount_textview);
135     }
136 
137     setNumberCalls(row);
138     setPhoto(row);
139     setFeatureIcons(row);
140     setCallTypeIcon(row);
141     setPhoneAccounts(row);
142     setCallButon(row);
143 
144     itemView.setOnClickListener(NewCallLogMenu.createOnClickListener(activity, row));
145   }
146 
configA11yForRow(CoalescedRow row)147   private void configA11yForRow(CoalescedRow row) {
148     callLogEntryRootView.setContentDescription(
149         CallLogEntryDescriptions.buildDescriptionForEntry(activity, clock, row));
150 
151     // Inform a11y users that double tapping an entry now makes a call.
152     // This will instruct TalkBack to say "double tap to call" instead of
153     // "double tap to activate".
154     callLogEntryRootView.setAccessibilityDelegate(
155         new AccessibilityDelegate() {
156           @Override
157           public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
158             super.onInitializeAccessibilityNodeInfo(host, info);
159             info.addAction(
160                 new AccessibilityAction(
161                     AccessibilityNodeInfo.ACTION_CLICK,
162                     activity
163                         .getResources()
164                         .getString(R.string.a11y_new_call_log_entry_tap_action)));
165           }
166         });
167   }
168 
setNumberCalls(CoalescedRow row)169   private void setNumberCalls(CoalescedRow row) {
170     int numberCalls = row.getCoalescedIds().getCoalescedIdCount();
171     if (numberCalls > 1) {
172       callCountTextView.setText(String.format(Locale.getDefault(), "(%d)", numberCalls));
173       callCountTextView.setVisibility(View.VISIBLE);
174     } else {
175       callCountTextView.setVisibility(View.GONE);
176     }
177   }
178 
isUnreadMissedCall(CoalescedRow row)179   private boolean isUnreadMissedCall(CoalescedRow row) {
180     // Show missed call styling if the most recent call in the group was missed and it is still
181     // marked as not read. The "NEW" column is presumably used for notifications and voicemails
182     // only.
183     return row.getCallType() == Calls.MISSED_TYPE && !row.getIsRead();
184   }
185 
setPhoto(CoalescedRow row)186   private void setPhoto(CoalescedRow row) {
187     contactPhotoView.setPhoto(PhotoInfoBuilder.fromCoalescedRow(activity, row).build());
188   }
189 
setFeatureIcons(CoalescedRow row)190   private void setFeatureIcons(CoalescedRow row) {
191     ColorStateList colorStateList =
192         ColorStateList.valueOf(
193             activity.getColor(
194                 isUnreadMissedCall(row)
195                     ? R.color.feature_icon_unread_color
196                     : R.color.feature_icon_read_color));
197 
198     // Handle HD Icon
199     if ((row.getFeatures() & Calls.FEATURES_HD_CALL) == Calls.FEATURES_HD_CALL) {
200       hdIcon.setVisibility(View.VISIBLE);
201       hdIcon.setImageTintList(colorStateList);
202     } else {
203       hdIcon.setVisibility(View.GONE);
204     }
205 
206     // Handle Wifi Icon
207     if (MotorolaUtils.shouldShowWifiIconInCallLog(activity, row.getFeatures())) {
208       wifiIcon.setVisibility(View.VISIBLE);
209       wifiIcon.setImageTintList(colorStateList);
210     } else {
211       wifiIcon.setVisibility(View.GONE);
212     }
213 
214     // Handle Assisted Dialing Icon
215     if ((row.getFeatures() & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING)
216         == TelephonyManagerCompat.FEATURES_ASSISTED_DIALING) {
217       assistedDialIcon.setVisibility(View.VISIBLE);
218       assistedDialIcon.setImageTintList(colorStateList);
219     } else {
220       assistedDialIcon.setVisibility(View.GONE);
221     }
222   }
223 
setCallTypeIcon(CoalescedRow row)224   private void setCallTypeIcon(CoalescedRow row) {
225     @DrawableRes int resId;
226     switch (row.getCallType()) {
227       case Calls.INCOMING_TYPE:
228       case Calls.ANSWERED_EXTERNALLY_TYPE:
229         resId = R.drawable.quantum_ic_call_received_vd_theme_24;
230         break;
231       case Calls.OUTGOING_TYPE:
232         resId = R.drawable.quantum_ic_call_made_vd_theme_24;
233         break;
234       case Calls.MISSED_TYPE:
235         resId = R.drawable.quantum_ic_call_missed_vd_theme_24;
236         break;
237       case Calls.VOICEMAIL_TYPE:
238         throw new IllegalStateException("Voicemails not expected in call log");
239       case Calls.BLOCKED_TYPE:
240         resId = R.drawable.quantum_ic_block_vd_theme_24;
241         break;
242       default:
243         // It is possible for users to end up with calls with unknown call types in their
244         // call history, possibly due to 3rd party call log implementations (e.g. to
245         // distinguish between rejected and missed calls). Instead of crashing, just
246         // assume that all unknown call types are missed calls.
247         resId = R.drawable.quantum_ic_call_missed_vd_theme_24;
248         break;
249     }
250     callTypeIcon.setImageResource(resId);
251 
252     if (isUnreadMissedCall(row)) {
253       callTypeIcon.setImageTintList(
254           ColorStateList.valueOf(activity.getColor(R.color.call_type_icon_unread_color)));
255     } else {
256       callTypeIcon.setImageTintList(
257           ColorStateList.valueOf(activity.getColor(R.color.call_type_icon_read_color)));
258     }
259   }
260 
setPhoneAccounts(CoalescedRow row)261   private void setPhoneAccounts(CoalescedRow row) {
262     PhoneAccountHandle phoneAccountHandle =
263         TelecomUtil.composePhoneAccountHandle(
264             row.getPhoneAccountComponentName(), row.getPhoneAccountId());
265     if (phoneAccountHandle == null) {
266       phoneAccountView.setVisibility(View.GONE);
267       return;
268     }
269 
270     String phoneAccountLabel = PhoneAccountUtils.getAccountLabel(activity, phoneAccountHandle);
271     if (TextUtils.isEmpty(phoneAccountLabel)) {
272       phoneAccountView.setVisibility(View.GONE);
273       return;
274     }
275 
276     @ColorInt
277     int phoneAccountColor = PhoneAccountUtils.getAccountColor(activity, phoneAccountHandle);
278     if (phoneAccountColor == PhoneAccount.NO_HIGHLIGHT_COLOR) {
279       phoneAccountColor =
280           activity
281               .getResources()
282               .getColor(R.color.dialer_secondary_text_color, activity.getTheme());
283     }
284 
285     phoneAccountView.setText(phoneAccountLabel);
286     phoneAccountView.setTextColor(phoneAccountColor);
287     phoneAccountView.setVisibility(View.VISIBLE);
288   }
289 
setCallButon(CoalescedRow row)290   private void setCallButon(CoalescedRow row) {
291     if (!PhoneNumberHelper.canPlaceCallsTo(
292         row.getNumber().getNormalizedNumber(), row.getNumberPresentation())) {
293       callButton.setVisibility(View.GONE);
294       return;
295     }
296 
297     callButton.setVisibility(View.VISIBLE);
298     if ((row.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) {
299       callButton.setImageResource(R.drawable.quantum_ic_videocam_vd_theme_24);
300       callButton.setContentDescription(
301           TextUtils.expandTemplate(
302               activity.getResources().getText(R.string.a11y_new_call_log_entry_video_call),
303               CallLogEntryText.buildPrimaryText(activity, row)));
304     } else {
305       callButton.setImageResource(R.drawable.quantum_ic_call_vd_theme_24);
306       callButton.setContentDescription(
307           TextUtils.expandTemplate(
308               activity.getResources().getText(R.string.a11y_new_call_log_entry_voice_call),
309               CallLogEntryText.buildPrimaryText(activity, row)));
310     }
311 
312     callButton.setOnClickListener(view -> CallLogRowActions.startCallForRow(activity, row));
313   }
314 
315   private class RealtimeRowFutureCallback implements FutureCallback<CoalescedRow> {
316     private final CoalescedRow originalRow;
317 
RealtimeRowFutureCallback(CoalescedRow originalRow)318     RealtimeRowFutureCallback(CoalescedRow originalRow) {
319       this.originalRow = originalRow;
320     }
321 
322     @Override
onSuccess(CoalescedRow updatedRow)323     public void onSuccess(CoalescedRow updatedRow) {
324       // If the user scrolled then this ViewHolder may not correspond to the completed task and
325       // there's nothing to do.
326       if (originalRow.getId() != currentRowId) {
327         popCounts.didNotPop++;
328         return;
329       }
330       // Only update the UI if the updated row differs from the original row (which has already
331       // been displayed).
332       if (!updatedRow.equals(originalRow)) {
333         displayRow(updatedRow);
334         popCounts.popped++;
335         return;
336       }
337       popCounts.didNotPop++;
338     }
339 
340     @Override
onFailure(Throwable throwable)341     public void onFailure(Throwable throwable) {
342       throw new RuntimeException("realtime processing failed", throwable);
343     }
344   }
345 }
346