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.content.Context; 19 import android.content.Intent; 20 import android.content.res.ColorStateList; 21 import android.database.Cursor; 22 import android.provider.CallLog.Calls; 23 import android.support.annotation.DrawableRes; 24 import android.support.v7.widget.RecyclerView; 25 import android.view.View; 26 import android.widget.ImageView; 27 import android.widget.QuickContactBadge; 28 import android.widget.TextView; 29 import com.android.dialer.calllog.model.CoalescedRow; 30 import com.android.dialer.calllog.ui.menu.NewCallLogMenu; 31 import com.android.dialer.calllogutils.CallLogEntryText; 32 import com.android.dialer.calllogutils.CallLogIntents; 33 import com.android.dialer.calllogutils.NumberAttributesConverter; 34 import com.android.dialer.common.concurrent.DialerExecutorComponent; 35 import com.android.dialer.compat.AppCompatConstants; 36 import com.android.dialer.compat.telephony.TelephonyManagerCompat; 37 import com.android.dialer.glidephotomanager.GlidePhotoManager; 38 import com.android.dialer.oem.MotorolaUtils; 39 import com.android.dialer.time.Clock; 40 import com.google.common.util.concurrent.FutureCallback; 41 import com.google.common.util.concurrent.Futures; 42 import java.util.Locale; 43 import java.util.concurrent.ExecutorService; 44 45 /** {@link RecyclerView.ViewHolder} for the new call log. */ 46 final class NewCallLogViewHolder extends RecyclerView.ViewHolder { 47 48 private final Context context; 49 private final TextView primaryTextView; 50 private final TextView callCountTextView; 51 private final TextView secondaryTextView; 52 private final QuickContactBadge quickContactBadge; 53 private final ImageView callTypeIcon; 54 private final ImageView hdIcon; 55 private final ImageView wifiIcon; 56 private final ImageView assistedDialIcon; 57 private final TextView phoneAccountView; 58 private final ImageView menuButton; 59 60 private final Clock clock; 61 private final RealtimeRowProcessor realtimeRowProcessor; 62 private final ExecutorService uiExecutorService; 63 64 private final GlidePhotoManager glidePhotoManager; 65 66 private int currentRowId; 67 68 NewCallLogViewHolder( 69 View view, 70 Clock clock, 71 RealtimeRowProcessor realtimeRowProcessor, 72 GlidePhotoManager glidePhotoManager) { 73 super(view); 74 this.context = view.getContext(); 75 primaryTextView = view.findViewById(R.id.primary_text); 76 callCountTextView = view.findViewById(R.id.call_count); 77 secondaryTextView = view.findViewById(R.id.secondary_text); 78 quickContactBadge = view.findViewById(R.id.quick_contact_photo); 79 callTypeIcon = view.findViewById(R.id.call_type_icon); 80 hdIcon = view.findViewById(R.id.hd_icon); 81 wifiIcon = view.findViewById(R.id.wifi_icon); 82 assistedDialIcon = view.findViewById(R.id.assisted_dial_icon); 83 phoneAccountView = view.findViewById(R.id.phone_account); 84 menuButton = view.findViewById(R.id.menu_button); 85 86 this.clock = clock; 87 this.realtimeRowProcessor = realtimeRowProcessor; 88 this.glidePhotoManager = glidePhotoManager; 89 uiExecutorService = DialerExecutorComponent.get(context).uiExecutor(); 90 } 91 92 /** @param cursor a cursor from {@link CoalescedAnnotatedCallLogCursorLoader}. */ 93 void bind(Cursor cursor) { 94 CoalescedRow row = CoalescedAnnotatedCallLogCursorLoader.toRow(cursor); 95 currentRowId = row.id(); // Used to make sure async updates are applied to the correct views 96 97 // Even if there is additional real time processing necessary, we still want to immediately show 98 // what information we have, rather than an empty card. For example, if CP2 information needs to 99 // be queried on the fly, we can still show the phone number until the contact name loads. 100 displayRow(row); 101 102 // Note: This leaks the view holder via the callback (which is an inner class), but this is OK 103 // because we only create ~10 of them (and they'll be collected assuming all jobs finish). 104 Futures.addCallback( 105 realtimeRowProcessor.applyRealtimeProcessing(row), 106 new RealtimeRowFutureCallback(row), 107 uiExecutorService); 108 } 109 110 private void displayRow(CoalescedRow row) { 111 // TODO(zachh): Handle RTL properly. 112 primaryTextView.setText(CallLogEntryText.buildPrimaryText(context, row)); 113 secondaryTextView.setText(CallLogEntryText.buildSecondaryTextForEntries(context, clock, row)); 114 115 if (isNewMissedCall(row)) { 116 primaryTextView.setTextAppearance(R.style.primary_textview_new_call); 117 callCountTextView.setTextAppearance(R.style.primary_textview_new_call); 118 secondaryTextView.setTextAppearance(R.style.secondary_textview_new_call); 119 phoneAccountView.setTextAppearance(R.style.phoneaccount_textview_new_call); 120 } else { 121 primaryTextView.setTextAppearance(R.style.primary_textview); 122 callCountTextView.setTextAppearance(R.style.primary_textview); 123 secondaryTextView.setTextAppearance(R.style.secondary_textview); 124 phoneAccountView.setTextAppearance(R.style.phoneaccount_textview); 125 } 126 127 setNumberCalls(row); 128 setPhoto(row); 129 setFeatureIcons(row); 130 setCallTypeIcon(row); 131 setPhoneAccounts(row); 132 setOnClickListenerForRow(row); 133 setOnClickListenerForMenuButon(row); 134 } 135 136 private void setNumberCalls(CoalescedRow row) { 137 int numberCalls = row.coalescedIds().getCoalescedIdCount(); 138 if (numberCalls > 1) { 139 callCountTextView.setText(String.format(Locale.getDefault(), "(%d)", numberCalls)); 140 callCountTextView.setVisibility(View.VISIBLE); 141 } else { 142 callCountTextView.setVisibility(View.GONE); 143 } 144 } 145 146 private boolean isNewMissedCall(CoalescedRow row) { 147 // Show missed call styling if the most recent call in the group was missed and it is still 148 // marked as NEW. It is not clear what IS_READ should be used for and it is currently not used. 149 return row.callType() == Calls.MISSED_TYPE && row.isNew(); 150 } 151 152 private void setPhoto(CoalescedRow row) { 153 glidePhotoManager.loadQuickContactBadge( 154 quickContactBadge, 155 NumberAttributesConverter.toPhotoInfoBuilder(row.numberAttributes()) 156 .setFormattedNumber(row.formattedNumber()) 157 .build()); 158 } 159 160 private void setFeatureIcons(CoalescedRow row) { 161 ColorStateList colorStateList = 162 ColorStateList.valueOf( 163 context.getColor( 164 isNewMissedCall(row) 165 ? R.color.feature_icon_unread_color 166 : R.color.feature_icon_read_color)); 167 168 // Handle HD Icon 169 if ((row.features() & Calls.FEATURES_HD_CALL) == Calls.FEATURES_HD_CALL) { 170 hdIcon.setVisibility(View.VISIBLE); 171 hdIcon.setImageTintList(colorStateList); 172 } else { 173 hdIcon.setVisibility(View.GONE); 174 } 175 176 // Handle Wifi Icon 177 if (MotorolaUtils.shouldShowWifiIconInCallLog(context, row.features())) { 178 wifiIcon.setVisibility(View.VISIBLE); 179 wifiIcon.setImageTintList(colorStateList); 180 } else { 181 wifiIcon.setVisibility(View.GONE); 182 } 183 184 // Handle Assisted Dialing Icon 185 if ((row.features() & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING) 186 == TelephonyManagerCompat.FEATURES_ASSISTED_DIALING) { 187 assistedDialIcon.setVisibility(View.VISIBLE); 188 assistedDialIcon.setImageTintList(colorStateList); 189 } else { 190 assistedDialIcon.setVisibility(View.GONE); 191 } 192 } 193 194 private void setCallTypeIcon(CoalescedRow row) { 195 @DrawableRes int resId; 196 switch (row.callType()) { 197 case AppCompatConstants.CALLS_INCOMING_TYPE: 198 case AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE: 199 resId = R.drawable.quantum_ic_call_received_vd_theme_24; 200 break; 201 case AppCompatConstants.CALLS_OUTGOING_TYPE: 202 resId = R.drawable.quantum_ic_call_made_vd_theme_24; 203 break; 204 case AppCompatConstants.CALLS_MISSED_TYPE: 205 resId = R.drawable.quantum_ic_call_missed_vd_theme_24; 206 break; 207 case AppCompatConstants.CALLS_VOICEMAIL_TYPE: 208 throw new IllegalStateException("Voicemails not expected in call log"); 209 case AppCompatConstants.CALLS_BLOCKED_TYPE: 210 resId = R.drawable.quantum_ic_block_vd_theme_24; 211 break; 212 default: 213 // It is possible for users to end up with calls with unknown call types in their 214 // call history, possibly due to 3rd party call log implementations (e.g. to 215 // distinguish between rejected and missed calls). Instead of crashing, just 216 // assume that all unknown call types are missed calls. 217 resId = R.drawable.quantum_ic_call_missed_vd_theme_24; 218 break; 219 } 220 callTypeIcon.setImageResource(resId); 221 222 if (isNewMissedCall(row)) { 223 callTypeIcon.setImageTintList( 224 ColorStateList.valueOf(context.getColor(R.color.call_type_icon_unread_color))); 225 } else { 226 callTypeIcon.setImageTintList( 227 ColorStateList.valueOf(context.getColor(R.color.call_type_icon_read_color))); 228 } 229 } 230 231 private void setPhoneAccounts(CoalescedRow row) { 232 if (row.phoneAccountLabel() != null) { 233 phoneAccountView.setText(row.phoneAccountLabel()); 234 phoneAccountView.setTextColor(row.phoneAccountColor()); 235 phoneAccountView.setVisibility(View.VISIBLE); 236 } else { 237 phoneAccountView.setVisibility(View.GONE); 238 } 239 } 240 241 private void setOnClickListenerForRow(CoalescedRow row) { 242 itemView.setOnClickListener( 243 (view) -> { 244 Intent callbackIntent = CallLogIntents.getCallBackIntent(context, row); 245 if (callbackIntent != null) { 246 context.startActivity(callbackIntent); 247 } 248 }); 249 } 250 251 private void setOnClickListenerForMenuButon(CoalescedRow row) { 252 menuButton.setOnClickListener( 253 NewCallLogMenu.createOnClickListener(context, row, glidePhotoManager)); 254 } 255 256 private class RealtimeRowFutureCallback implements FutureCallback<CoalescedRow> { 257 private final CoalescedRow originalRow; 258 259 RealtimeRowFutureCallback(CoalescedRow originalRow) { 260 this.originalRow = originalRow; 261 } 262 263 @Override 264 public void onSuccess(CoalescedRow updatedRow) { 265 // If the user scrolled then this ViewHolder may not correspond to the completed task and 266 // there's nothing to do. 267 if (originalRow.id() != currentRowId) { 268 return; 269 } 270 // Only update the UI if the updated row differs from the original row (which has already 271 // been displayed). 272 if (!updatedRow.equals(originalRow)) { 273 displayRow(updatedRow); 274 } 275 } 276 277 @Override 278 public void onFailure(Throwable throwable) { 279 throw new RuntimeException("realtime processing failed", throwable); 280 } 281 } 282 } 283