Home | History | Annotate | Download | only in ui
      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