• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
NewCallLogViewHolder( View view, Clock clock, RealtimeRowProcessor realtimeRowProcessor, GlidePhotoManager glidePhotoManager)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}. */
bind(Cursor cursor)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 
displayRow(CoalescedRow row)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 
setNumberCalls(CoalescedRow row)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 
isNewMissedCall(CoalescedRow row)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 
setPhoto(CoalescedRow row)152   private void setPhoto(CoalescedRow row) {
153     glidePhotoManager.loadQuickContactBadge(
154         quickContactBadge,
155         NumberAttributesConverter.toPhotoInfoBuilder(row.numberAttributes())
156             .setFormattedNumber(row.formattedNumber())
157             .build());
158   }
159 
setFeatureIcons(CoalescedRow row)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 
setCallTypeIcon(CoalescedRow row)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 
setPhoneAccounts(CoalescedRow row)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 
setOnClickListenerForRow(CoalescedRow row)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 
setOnClickListenerForMenuButon(CoalescedRow row)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 
RealtimeRowFutureCallback(CoalescedRow originalRow)259     RealtimeRowFutureCallback(CoalescedRow originalRow) {
260       this.originalRow = originalRow;
261     }
262 
263     @Override
onSuccess(CoalescedRow updatedRow)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
onFailure(Throwable throwable)278     public void onFailure(Throwable throwable) {
279       throw new RuntimeException("realtime processing failed", throwable);
280     }
281   }
282 }
283