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.Context;
20 import android.support.annotation.IntDef;
21 import android.support.annotation.Nullable;
22 import android.support.v7.widget.RecyclerView;
23 import android.support.v7.widget.RecyclerView.ViewHolder;
24 import android.view.LayoutInflater;
25 import android.view.ViewGroup;
26 import com.android.dialer.calllog.model.CoalescedRow;
27 import com.android.dialer.calllogutils.CallLogDates;
28 import com.android.dialer.common.Assert;
29 import com.android.dialer.logging.Logger;
30 import com.android.dialer.promotion.Promotion;
31 import com.android.dialer.time.Clock;
32 import com.google.common.collect.ImmutableList;
33 import java.lang.annotation.Retention;
34 import java.lang.annotation.RetentionPolicy;
35 
36 /** {@link RecyclerView.Adapter} for the new call log fragment. */
37 final class NewCallLogAdapter extends RecyclerView.Adapter<ViewHolder> {
38 
39   /** IntDef for the different types of rows that can be shown in the call log. */
40   @Retention(RetentionPolicy.SOURCE)
41   @IntDef({
42     RowType.PROMOTION_CARD,
43     RowType.HEADER_TODAY,
44     RowType.HEADER_YESTERDAY,
45     RowType.HEADER_OLDER,
46     RowType.CALL_LOG_ENTRY
47   })
48   @interface RowType {
49     /** The promotion card. */
50     int PROMOTION_CARD = 1;
51 
52     /** Header that displays "Today". */
53     int HEADER_TODAY = 2;
54 
55     /** Header that displays "Yesterday". */
56     int HEADER_YESTERDAY = 3;
57 
58     /** Header that displays "Older". */
59     int HEADER_OLDER = 4;
60 
61     /** A row representing a call log entry (which could represent one or more calls). */
62     int CALL_LOG_ENTRY = 5;
63   }
64 
65   private final Clock clock;
66   private final Activity activity;
67   private final RealtimeRowProcessor realtimeRowProcessor;
68   private final PopCounts popCounts = new PopCounts();
69   @Nullable private final Promotion promotion;
70 
71   private ImmutableList<CoalescedRow> coalescedRows;
72 
73   /** Position of the promotion card. Null when it should not be displayed. */
74   @Nullable private Integer promotionCardPosition;
75 
76   /** Position of the "Today" header. Null when it should not be displayed. */
77   @Nullable private Integer todayHeaderPosition;
78 
79   /** Position of the "Yesterday" header. Null when it should not be displayed. */
80   @Nullable private Integer yesterdayHeaderPosition;
81 
82   /** Position of the "Older" header. Null when it should not be displayed. */
83   @Nullable private Integer olderHeaderPosition;
84 
NewCallLogAdapter( Activity activity, ImmutableList<CoalescedRow> coalescedRows, Clock clock, @Nullable Promotion promotion)85   NewCallLogAdapter(
86       Activity activity,
87       ImmutableList<CoalescedRow> coalescedRows,
88       Clock clock,
89       @Nullable Promotion promotion) {
90     this.activity = activity;
91     this.coalescedRows = coalescedRows;
92     this.clock = clock;
93     this.realtimeRowProcessor = CallLogUiComponent.get(activity).realtimeRowProcessor();
94     this.promotion = promotion;
95 
96     setCardAndHeaderPositions();
97   }
98 
updateRows(ImmutableList<CoalescedRow> coalescedRows)99   void updateRows(ImmutableList<CoalescedRow> coalescedRows) {
100     this.coalescedRows = coalescedRows;
101     this.realtimeRowProcessor.clearCache();
102     this.popCounts.reset();
103 
104     setCardAndHeaderPositions();
105     notifyDataSetChanged();
106   }
107 
clearCache()108   void clearCache() {
109     this.realtimeRowProcessor.clearCache();
110   }
111 
logMetrics(Context context)112   void logMetrics(Context context) {
113     Logger.get(context).logAnnotatedCallLogMetrics(popCounts.popped, popCounts.didNotPop);
114   }
115 
setCardAndHeaderPositions()116   private void setCardAndHeaderPositions() {
117     // Set the position for the promotion card if it should be shown.
118     promotionCardPosition = null;
119     int numCards = 0;
120     if (promotion != null && promotion.isEligibleToBeShown()) {
121       promotionCardPosition = 0;
122       numCards++;
123     }
124 
125     // If there are no rows to display, set all header positions to null.
126     if (coalescedRows.isEmpty()) {
127       todayHeaderPosition = null;
128       yesterdayHeaderPosition = null;
129       olderHeaderPosition = null;
130       return;
131     }
132 
133     // Calculate positions for headers.
134     long currentTimeMillis = clock.currentTimeMillis();
135 
136     int numItemsInToday = 0;
137     int numItemsInYesterday = 0;
138     int numItemsInOlder = 0;
139     for (CoalescedRow coalescedRow : coalescedRows) {
140       long timestamp = coalescedRow.getTimestamp();
141       long dayDifference = CallLogDates.getDayDifference(currentTimeMillis, timestamp);
142       if (dayDifference == 0) {
143         numItemsInToday++;
144       } else if (dayDifference == 1) {
145         numItemsInYesterday++;
146       } else {
147         numItemsInOlder = coalescedRows.size() - numItemsInToday - numItemsInYesterday;
148         break;
149       }
150     }
151 
152     if (numItemsInToday > 0) {
153       numItemsInToday++; // including the "Today" header;
154     }
155     if (numItemsInYesterday > 0) {
156       numItemsInYesterday++; // including the "Yesterday" header;
157     }
158     if (numItemsInOlder > 0) {
159       numItemsInOlder++; // include the "Older" header;
160     }
161 
162     // Set all header positions.
163     // A header position will be null if there is no item to be displayed under that header.
164     todayHeaderPosition = numItemsInToday > 0 ? numCards : null;
165     yesterdayHeaderPosition = numItemsInYesterday > 0 ? numItemsInToday + numCards : null;
166     olderHeaderPosition =
167         numItemsInOlder > 0 ? numItemsInToday + numItemsInYesterday + numCards : null;
168   }
169 
170   @Override
onAttachedToRecyclerView(RecyclerView recyclerView)171   public void onAttachedToRecyclerView(RecyclerView recyclerView) {
172     super.onAttachedToRecyclerView(recyclerView);
173 
174     // Register a OnScrollListener that records when the promotion is viewed.
175     if (promotion != null && promotion.isEligibleToBeShown()) {
176       recyclerView.addOnScrollListener(
177           new OnScrollListenerForRecordingPromotionCardFirstViewTime(promotion));
178     }
179   }
180 
181   @Override
onCreateViewHolder(ViewGroup viewGroup, @RowType int viewType)182   public ViewHolder onCreateViewHolder(ViewGroup viewGroup, @RowType int viewType) {
183     switch (viewType) {
184       case RowType.PROMOTION_CARD:
185         return new PromotionCardViewHolder(
186             LayoutInflater.from(activity)
187                 .inflate(
188                     R.layout.new_call_log_promotion_card, viewGroup, /* attachToRoot = */ false),
189             promotion);
190       case RowType.HEADER_TODAY:
191       case RowType.HEADER_YESTERDAY:
192       case RowType.HEADER_OLDER:
193         return new HeaderViewHolder(
194             LayoutInflater.from(activity)
195                 .inflate(R.layout.new_call_log_header, viewGroup, /* attachToRoot = */ false));
196       case RowType.CALL_LOG_ENTRY:
197         return new NewCallLogViewHolder(
198             activity,
199             LayoutInflater.from(activity)
200                 .inflate(R.layout.new_call_log_entry, viewGroup, /* attachToRoot = */ false),
201             clock,
202             realtimeRowProcessor,
203             popCounts);
204       default:
205         throw Assert.createUnsupportedOperationFailException("Unsupported view type: " + viewType);
206     }
207   }
208 
209   @Override
onBindViewHolder(ViewHolder viewHolder, int position)210   public void onBindViewHolder(ViewHolder viewHolder, int position) {
211     @RowType int viewType = getItemViewType(position);
212     switch (viewType) {
213       case RowType.PROMOTION_CARD:
214         ((PromotionCardViewHolder) viewHolder)
215             .setDismissListener(
216                 () -> {
217                   notifyItemRemoved(promotionCardPosition);
218                   setCardAndHeaderPositions();
219                 });
220         break;
221       case RowType.HEADER_TODAY:
222         ((HeaderViewHolder) viewHolder).setHeader(R.string.new_call_log_header_today);
223         break;
224       case RowType.HEADER_YESTERDAY:
225         ((HeaderViewHolder) viewHolder).setHeader(R.string.new_call_log_header_yesterday);
226         break;
227       case RowType.HEADER_OLDER:
228         ((HeaderViewHolder) viewHolder).setHeader(R.string.new_call_log_header_older);
229         break;
230       case RowType.CALL_LOG_ENTRY:
231         NewCallLogViewHolder newCallLogViewHolder = (NewCallLogViewHolder) viewHolder;
232         int previousCardAndHeaders = 0;
233         if (promotionCardPosition != null && position > promotionCardPosition) {
234           previousCardAndHeaders++;
235         }
236         if (todayHeaderPosition != null && position > todayHeaderPosition) {
237           previousCardAndHeaders++;
238         }
239         if (yesterdayHeaderPosition != null && position > yesterdayHeaderPosition) {
240           previousCardAndHeaders++;
241         }
242         if (olderHeaderPosition != null && position > olderHeaderPosition) {
243           previousCardAndHeaders++;
244         }
245         newCallLogViewHolder.bind(coalescedRows.get(position - previousCardAndHeaders));
246         break;
247       default:
248         throw Assert.createIllegalStateFailException(
249             "Unexpected view type " + viewType + " at position: " + position);
250     }
251   }
252 
253   @Override
254   @RowType
getItemViewType(int position)255   public int getItemViewType(int position) {
256     if (promotionCardPosition != null && position == promotionCardPosition) {
257       return RowType.PROMOTION_CARD;
258     }
259     if (todayHeaderPosition != null && position == todayHeaderPosition) {
260       return RowType.HEADER_TODAY;
261     }
262     if (yesterdayHeaderPosition != null && position == yesterdayHeaderPosition) {
263       return RowType.HEADER_YESTERDAY;
264     }
265     if (olderHeaderPosition != null && position == olderHeaderPosition) {
266       return RowType.HEADER_OLDER;
267     }
268     return RowType.CALL_LOG_ENTRY;
269   }
270 
271   @Override
getItemCount()272   public int getItemCount() {
273     int numberOfCards = 0;
274     int numberOfHeaders = 0;
275 
276     if (promotionCardPosition != null) {
277       numberOfCards++;
278     }
279     if (todayHeaderPosition != null) {
280       numberOfHeaders++;
281     }
282     if (yesterdayHeaderPosition != null) {
283       numberOfHeaders++;
284     }
285     if (olderHeaderPosition != null) {
286       numberOfHeaders++;
287     }
288     return coalescedRows.size() + numberOfHeaders + numberOfCards;
289   }
290 
291   /**
292    * A {@link RecyclerView.OnScrollListener} that records the timestamp at which the promotion card
293    * is first viewed.
294    *
295    * <p>We consider the card as viewed if the user scrolls the containing RecyclerView since such
296    * action is a strong proof.
297    */
298   private static final class OnScrollListenerForRecordingPromotionCardFirstViewTime
299       extends RecyclerView.OnScrollListener {
300 
301     private final Promotion promotion;
302 
OnScrollListenerForRecordingPromotionCardFirstViewTime(Promotion promotion)303     OnScrollListenerForRecordingPromotionCardFirstViewTime(Promotion promotion) {
304       this.promotion = promotion;
305     }
306 
307     @Override
onScrollStateChanged(RecyclerView recyclerView, int newState)308     public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
309       if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
310         promotion.onViewed();
311 
312         // Recording promotion is viewed is this listener's sole responsibility.
313         // We can remove it from the containing RecyclerView after the job is done.
314         recyclerView.removeOnScrollListener(this);
315       }
316 
317       super.onScrollStateChanged(recyclerView, newState);
318     }
319   }
320 
321   static class PopCounts {
322     int popped;
323     int didNotPop;
324 
reset()325     private void reset() {
326       popped = 0;
327       didNotPop = 0;
328     }
329   }
330 }
331