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