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