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 */
16package com.android.dialer.voicemail.listui;
17
18import android.app.FragmentManager;
19import android.content.ContentValues;
20import android.content.Context;
21import android.content.Intent;
22import android.database.Cursor;
23import android.media.MediaPlayer;
24import android.media.MediaPlayer.OnCompletionListener;
25import android.media.MediaPlayer.OnErrorListener;
26import android.media.MediaPlayer.OnPreparedListener;
27import android.net.Uri;
28import android.provider.VoicemailContract.Voicemails;
29import android.support.annotation.IntDef;
30import android.support.annotation.Nullable;
31import android.support.annotation.WorkerThread;
32import android.support.v7.widget.RecyclerView;
33import android.support.v7.widget.RecyclerView.ViewHolder;
34import android.util.ArrayMap;
35import android.util.ArraySet;
36import android.util.Pair;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.ViewGroup;
40import com.android.dialer.calllogutils.CallLogDates;
41import com.android.dialer.common.Assert;
42import com.android.dialer.common.LogUtil;
43import com.android.dialer.common.concurrent.DialerExecutor.Worker;
44import com.android.dialer.common.concurrent.DialerExecutorComponent;
45import com.android.dialer.common.concurrent.ThreadUtil;
46import com.android.dialer.glidephotomanager.GlidePhotoManager;
47import com.android.dialer.time.Clock;
48import com.android.dialer.voicemail.listui.NewVoicemailViewHolder.NewVoicemailViewHolderListener;
49import com.android.dialer.voicemail.listui.error.VoicemailErrorMessage;
50import com.android.dialer.voicemail.listui.error.VoicemailErrorMessageCreator;
51import com.android.dialer.voicemail.listui.error.VoicemailStatus;
52import com.android.dialer.voicemail.model.VoicemailEntry;
53import com.android.voicemail.VoicemailClient;
54import com.google.common.collect.ImmutableList;
55import java.lang.annotation.Retention;
56import java.lang.annotation.RetentionPolicy;
57import java.util.Locale;
58import java.util.Objects;
59import java.util.Set;
60
61/** {@link RecyclerView.Adapter} for the new voicemail call log fragment. */
62final class NewVoicemailAdapter extends RecyclerView.Adapter<ViewHolder>
63    implements NewVoicemailViewHolderListener {
64
65  /** IntDef for the different types of rows that can be shown in the call log. */
66  @Retention(RetentionPolicy.SOURCE)
67  @IntDef({RowType.HEADER, RowType.VOICEMAIL_ENTRY, RowType.VOICEMAIL_ALERT})
68  @interface RowType {
69    /** A row representing a voicemail alert. */
70    int VOICEMAIL_ALERT = 1;
71    /** Header that displays "Today", "Yesterday" or "Older". */
72    int HEADER = 2;
73    /** A row representing a voicemail entry. */
74    int VOICEMAIL_ENTRY = 3;
75  }
76
77  private Cursor cursor;
78  private final Clock clock;
79  private final GlidePhotoManager glidePhotoManager;
80
81  /** {@link Integer#MAX_VALUE} when the "Today" header should not be displayed. */
82  private int todayHeaderPosition = Integer.MAX_VALUE;
83  /** {@link Integer#MAX_VALUE} when the "Yesterday" header should not be displayed. */
84  private int yesterdayHeaderPosition = Integer.MAX_VALUE;
85  /** {@link Integer#MAX_VALUE} when the "Older" header should not be displayed. */
86  private int olderHeaderPosition = Integer.MAX_VALUE;
87  /** {@link Integer#MAX_VALUE} when the voicemail alert message should not be displayed. */
88  private int voicemailAlertPosition = Integer.MAX_VALUE;
89
90  private final FragmentManager fragmentManager;
91  /** A valid id for {@link VoicemailEntry} is greater than 0 */
92  private int currentlyExpandedViewHolderId = -1;
93
94  private VoicemailErrorMessage voicemailErrorMessage;
95
96  /**
97   * It takes time to delete voicemails from the server, so we "remove" them and remember the
98   * positions we removed until a new cursor is ready.
99   */
100  Set<Integer> deletedVoicemailPosition = new ArraySet<>();
101
102  /**
103   * A set of (re-usable) view holders being used by the recycler view to display voicemails. This
104   * set may include multiple view holder with the same ID and shouldn't be used to lookup a
105   * specific viewholder based on this value, instead use newVoicemailViewHolderArrayMap for that
106   * purpose.
107   */
108  private final Set<NewVoicemailViewHolder> newVoicemailViewHolderSet = new ArraySet<>();
109  /**
110   * This allows us to retrieve the view holder corresponding to a particular view holder id, and
111   * will always ensure there is only (up-to-date) view holder corresponding to a view holder id,
112   * unlike the newVoicemailViewHolderSet.
113   */
114  private final ArrayMap<Integer, NewVoicemailViewHolder> newVoicemailViewHolderArrayMap =
115      new ArrayMap<>();
116
117  // A single instance of a media player re-used across the expanded view holders.
118  private final NewVoicemailMediaPlayer mediaPlayer =
119      new NewVoicemailMediaPlayer(new MediaPlayer());
120
121  /** @param cursor whose projection is {@link VoicemailCursorLoader#VOICEMAIL_COLUMNS} */
122  NewVoicemailAdapter(
123      Cursor cursor,
124      Clock clock,
125      FragmentManager fragmentManager,
126      GlidePhotoManager glidePhotoManager) {
127    LogUtil.enterBlock("NewVoicemailAdapter");
128    this.cursor = cursor;
129    this.clock = clock;
130    this.fragmentManager = fragmentManager;
131    this.glidePhotoManager = glidePhotoManager;
132    initializeMediaPlayerListeners();
133    updateHeaderPositions();
134  }
135
136  private void updateHeaderPositions() {
137    LogUtil.i(
138        "NewVoicemailAdapter.updateHeaderPositions",
139        "before updating todayPos:%d, yestPos:%d, olderPos:%d, alertPos:%d",
140        todayHeaderPosition,
141        yesterdayHeaderPosition,
142        olderHeaderPosition,
143        voicemailAlertPosition);
144
145    // If there are no rows to display, set all header positions to MAX_VALUE.
146    if (!cursor.moveToFirst()) {
147      todayHeaderPosition = Integer.MAX_VALUE;
148      yesterdayHeaderPosition = Integer.MAX_VALUE;
149      olderHeaderPosition = Integer.MAX_VALUE;
150      return;
151    }
152
153    long currentTimeMillis = clock.currentTimeMillis();
154
155    int numItemsInToday = 0;
156    int numItemsInYesterday = 0;
157
158    do {
159      long timestamp = VoicemailCursorLoader.getTimestamp(cursor);
160      long dayDifference = CallLogDates.getDayDifference(currentTimeMillis, timestamp);
161      if (dayDifference == 0) {
162        numItemsInToday++;
163      } else if (dayDifference == 1) {
164        numItemsInYesterday++;
165      } else {
166        break;
167      }
168    } while (cursor.moveToNext());
169
170    if (numItemsInToday > 0) {
171      numItemsInToday++; // including the "Today" header;
172    }
173    if (numItemsInYesterday > 0) {
174      numItemsInYesterday++; // including the "Yesterday" header;
175    }
176
177    int alertOffSet = 0;
178    if (voicemailAlertPosition != Integer.MAX_VALUE) {
179      Assert.checkArgument(
180          voicemailAlertPosition == 0, "voicemail alert can only be 0, when showing");
181      alertOffSet = 1;
182    }
183
184    // Set all header positions.
185    // A header position will be MAX_VALUE if there is no item to be displayed under that header.
186    todayHeaderPosition = numItemsInToday > 0 ? alertOffSet : Integer.MAX_VALUE;
187    yesterdayHeaderPosition =
188        numItemsInYesterday > 0 ? numItemsInToday + alertOffSet : Integer.MAX_VALUE;
189    olderHeaderPosition =
190        !cursor.isAfterLast()
191            ? numItemsInToday + numItemsInYesterday + alertOffSet
192            : Integer.MAX_VALUE;
193
194    LogUtil.i(
195        "NewVoicemailAdapter.updateHeaderPositions",
196        "after updating todayPos:%d, yestPos:%d, olderPos:%d, alertOffSet:%d, alertPos:%d",
197        todayHeaderPosition,
198        yesterdayHeaderPosition,
199        olderHeaderPosition,
200        alertOffSet,
201        voicemailAlertPosition);
202  }
203
204  private void initializeMediaPlayerListeners() {
205    mediaPlayer.setOnCompletionListener(onCompletionListener);
206    mediaPlayer.setOnPreparedListener(onPreparedListener);
207    mediaPlayer.setOnErrorListener(onErrorListener);
208  }
209
210  public void updateCursor(Cursor updatedCursor) {
211    LogUtil.enterBlock("NewVoicemailAdapter.updateCursor");
212    deletedVoicemailPosition.clear();
213    this.cursor = updatedCursor;
214    updateHeaderPositions();
215    notifyDataSetChanged();
216  }
217
218  @Override
219  public ViewHolder onCreateViewHolder(ViewGroup viewGroup, @RowType int viewType) {
220    LogUtil.enterBlock("NewVoicemailAdapter.onCreateViewHolder");
221    LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
222    View view;
223    switch (viewType) {
224      case RowType.VOICEMAIL_ALERT:
225        view = inflater.inflate(R.layout.new_voicemail_entry_alert, viewGroup, false);
226        return new NewVoicemailAlertViewHolder(view);
227      case RowType.HEADER:
228        view = inflater.inflate(R.layout.new_voicemail_entry_header, viewGroup, false);
229        return new NewVoicemailHeaderViewHolder(view);
230      case NewVoicemailAdapter.RowType.VOICEMAIL_ENTRY:
231        view = inflater.inflate(R.layout.new_voicemail_entry, viewGroup, false);
232        NewVoicemailViewHolder newVoicemailViewHolder =
233            new NewVoicemailViewHolder(view, clock, this, glidePhotoManager);
234        newVoicemailViewHolderSet.add(newVoicemailViewHolder);
235        return newVoicemailViewHolder;
236      default:
237        throw Assert.createUnsupportedOperationFailException("Unsupported view type: " + viewType);
238    }
239  }
240
241  // TODO(uabdullah): a bug - Clean up logging in this function, here for debugging during
242  // development.
243  @Override
244  public void onBindViewHolder(ViewHolder viewHolder, int position) {
245    LogUtil.enterBlock("NewVoicemailAdapter.onBindViewHolder, pos:" + position);
246    // Re-request a bind when a viewholder is deleted to ensure correct position
247    if (deletedVoicemailPosition.contains(position)) {
248      LogUtil.i(
249          "NewVoicemailAdapter.onBindViewHolder",
250          "pos:%d contains deleted voicemail, re-bind. #of deleted voicemail positions: %d",
251          position,
252          deletedVoicemailPosition.size());
253      // TODO(uabdullah): This should be removed when we support multi-select delete
254      Assert.checkArgument(
255          deletedVoicemailPosition.size() == 1, "multi-deletes not currently supported");
256      onBindViewHolder(viewHolder, ++position);
257      return;
258    }
259
260    // TODO(uabdullah): a bug Remove logging, temporarily here for debugging.
261    printHashSet();
262    // TODO(uabdullah): a bug Remove logging, temporarily here for debugging.
263    printArrayMap();
264
265    if (viewHolder instanceof NewVoicemailHeaderViewHolder) {
266      LogUtil.i(
267          "NewVoicemailAdapter.onBindViewHolder", "view holder at pos:%d is a header", position);
268      onBindHeaderViewHolder(viewHolder, position);
269      return;
270    }
271
272    if (viewHolder instanceof NewVoicemailAlertViewHolder) {
273      LogUtil.i(
274          "NewVoicemailAdapter.onBindViewHolder", "view holder at pos:%d is a alert", position);
275      onBindAlertViewHolder(viewHolder, position);
276      return;
277    }
278
279    LogUtil.i(
280        "NewVoicemailAdapter.onBindViewHolder",
281        "view holder at pos:%d is a not a header or an alert",
282        position);
283
284    NewVoicemailViewHolder newVoicemailViewHolder = (NewVoicemailViewHolder) viewHolder;
285    int nonVoicemailEntryHeaders = getHeaderCountAtPosition(position);
286
287    LogUtil.i(
288        "NewVoicemailAdapter.onBindViewHolder",
289        "view holder at pos:%d, nonVoicemailEntryHeaders:%d",
290        position,
291        nonVoicemailEntryHeaders);
292
293    // Remove if the viewholder is being recycled.
294    if (newVoicemailViewHolderArrayMap.containsKey(newVoicemailViewHolder.getViewHolderId())) {
295      // TODO(uabdullah): a bug Remove logging, temporarily here for debugging.
296      LogUtil.i(
297          "NewVoicemailAdapter.onBindViewHolder",
298          "Removing from hashset:%d, hashsetSize:%d, currExpanded:%d",
299          newVoicemailViewHolder.getViewHolderId(),
300          newVoicemailViewHolderArrayMap.size(),
301          currentlyExpandedViewHolderId);
302
303      newVoicemailViewHolderArrayMap.remove(newVoicemailViewHolder.getViewHolderId());
304      printHashSet();
305      printArrayMap();
306    }
307
308    newVoicemailViewHolder.reset();
309    cursor.moveToPosition(position - nonVoicemailEntryHeaders);
310    newVoicemailViewHolder.bindViewHolderValuesFromAdapter(
311        cursor, fragmentManager, mediaPlayer, position, currentlyExpandedViewHolderId);
312
313    // TODO(uabdullah): a bug Remove logging, temporarily here for debugging.
314    LogUtil.i(
315        "NewVoicemailAdapter.onBindViewHolder",
316        "Adding to hashset:%d, hashsetSize:%d, pos:%d, currExpanded:%d",
317        newVoicemailViewHolder.getViewHolderId(),
318        newVoicemailViewHolderArrayMap.size(),
319        position,
320        currentlyExpandedViewHolderId);
321
322    // Need this to ensure correct getCurrentlyExpandedViewHolder() value
323    newVoicemailViewHolderArrayMap.put(
324        newVoicemailViewHolder.getViewHolderId(), newVoicemailViewHolder);
325
326    // TODO(uabdullah): a bug Remove logging, temporarily here for debugging.
327    printHashSet();
328    // TODO(uabdullah): a bug Remove logging, temporarily here for debugging.
329    printArrayMap();
330
331    // If the viewholder is playing the voicemail, keep updating its media player view (seekbar,
332    // duration etc.)
333    if (newVoicemailViewHolder.isViewHolderExpanded() && mediaPlayer.isPlaying()) {
334      LogUtil.i(
335          "NewVoicemailAdapter.onBindViewHolder",
336          "Adding to hashset:%d, hashsetSize:%d, pos:%d, currExpanded:%d",
337          newVoicemailViewHolderSet.size(),
338          newVoicemailViewHolderArrayMap.size(),
339          position,
340          currentlyExpandedViewHolderId);
341
342      Assert.checkArgument(
343          newVoicemailViewHolder
344              .getViewHolderVoicemailUri()
345              .equals(mediaPlayer.getLastPlayedOrPlayingVoicemailUri()),
346          "only the expanded view holder can be playing.");
347      Assert.isNotNull(getCurrentlyExpandedViewHolder());
348      Assert.checkArgument(
349          getCurrentlyExpandedViewHolder()
350              .getViewHolderVoicemailUri()
351              .equals(mediaPlayer.getLastPlayedOrPlayingVoicemailUri()));
352
353      recursivelyUpdateMediaPlayerViewOfExpandedViewHolder(newVoicemailViewHolder);
354    }
355    // Updates the hashmap with the most up-to-date state of the viewholder.
356    newVoicemailViewHolderArrayMap.put(
357        newVoicemailViewHolder.getViewHolderId(), newVoicemailViewHolder);
358
359    // TODO(uabdullah): a bug Remove logging, temporarily here for debugging.
360    printHashSet();
361    // TODO(uabdullah): a bug Remove logging, temporarily here for debugging.
362    printArrayMap();
363  }
364
365  private int getHeaderCountAtPosition(int position) {
366    int previousHeaders = 0;
367    if (voicemailAlertPosition != Integer.MAX_VALUE && position > voicemailAlertPosition) {
368      previousHeaders++;
369    }
370    if (todayHeaderPosition != Integer.MAX_VALUE && position > todayHeaderPosition) {
371      previousHeaders++;
372    }
373    if (yesterdayHeaderPosition != Integer.MAX_VALUE && position > yesterdayHeaderPosition) {
374      previousHeaders++;
375    }
376    if (olderHeaderPosition != Integer.MAX_VALUE && position > olderHeaderPosition) {
377      previousHeaders++;
378    }
379    return previousHeaders;
380  }
381
382  private void onBindAlertViewHolder(ViewHolder viewHolder, int position) {
383    LogUtil.i(
384        "NewVoicemailAdapter.onBindAlertViewHolder",
385        "pos:%d, voicemailAlertPosition:%d",
386        position,
387        voicemailAlertPosition);
388
389    NewVoicemailAlertViewHolder alertViewHolder = (NewVoicemailAlertViewHolder) viewHolder;
390    @RowType int viewType = getItemViewType(position);
391
392    Assert.checkArgument(position == 0, "position is not 0");
393    Assert.checkArgument(
394        position == voicemailAlertPosition,
395        String.format(
396            Locale.US,
397            "position:%d and voicemailAlertPosition:%d are different",
398            position,
399            voicemailAlertPosition));
400    Assert.checkArgument(viewType == RowType.VOICEMAIL_ALERT, "Invalid row type: " + viewType);
401    Assert.checkArgument(
402        voicemailErrorMessage.getActions().size() <= 2,
403        "Too many actions: " + voicemailErrorMessage.getActions().size());
404
405    alertViewHolder.setTitle(voicemailErrorMessage.getTitle());
406    alertViewHolder.setDescription(voicemailErrorMessage.getDescription());
407
408    if (!voicemailErrorMessage.getActions().isEmpty()) {
409      alertViewHolder.setPrimaryButton(voicemailErrorMessage.getActions().get(0));
410    }
411    if (voicemailErrorMessage.getActions().size() > 1) {
412      alertViewHolder.setSecondaryButton(voicemailErrorMessage.getActions().get(1));
413    }
414  }
415
416  private void onBindHeaderViewHolder(ViewHolder viewHolder, int position) {
417    NewVoicemailHeaderViewHolder headerViewHolder = (NewVoicemailHeaderViewHolder) viewHolder;
418    @RowType int viewType = getItemViewType(position);
419    if (position == todayHeaderPosition) {
420      headerViewHolder.setHeader(R.string.new_voicemail_header_today);
421    } else if (position == yesterdayHeaderPosition) {
422      headerViewHolder.setHeader(R.string.new_voicemail_header_yesterday);
423    } else if (position == olderHeaderPosition) {
424      headerViewHolder.setHeader(R.string.new_voicemail_header_older);
425    } else {
426      throw Assert.createIllegalStateFailException(
427          "Unexpected view type " + viewType + " at position: " + position);
428    }
429  }
430
431  private void printArrayMap() {
432    LogUtil.i(
433        "NewVoicemailAdapter.printArrayMap",
434        "hashMapSize: %d, currentlyExpandedViewHolderId:%d",
435        newVoicemailViewHolderArrayMap.size(),
436        currentlyExpandedViewHolderId);
437
438    if (!newVoicemailViewHolderArrayMap.isEmpty()) {
439      String ids = "";
440      for (int id : newVoicemailViewHolderArrayMap.keySet()) {
441        ids = ids + id + " ";
442      }
443      LogUtil.i("NewVoicemailAdapter.printArrayMap", "ids are " + ids);
444    }
445  }
446
447  private void printHashSet() {
448    LogUtil.i(
449        "NewVoicemailAdapter.printHashSet",
450        "hashSetSize: %d, currentlyExpandedViewHolderId:%d",
451        newVoicemailViewHolderSet.size(),
452        currentlyExpandedViewHolderId);
453
454    if (!newVoicemailViewHolderSet.isEmpty()) {
455      String viewHolderID = "";
456      for (NewVoicemailViewHolder vh : newVoicemailViewHolderSet) {
457        viewHolderID = viewHolderID + vh.getViewHolderId() + " ";
458      }
459      LogUtil.i("NewVoicemailAdapter.printHashSet", "ids are " + viewHolderID);
460    }
461  }
462
463  /**
464   * The {@link NewVoicemailAdapter} needs to keep track of {@link NewVoicemailViewHolder} that has
465   * been expanded. This is so that the adapter can ensure the correct {@link
466   * NewVoicemailMediaPlayerView} and {@link NewVoicemailViewHolder} states are maintained
467   * (playing/paused/reset) for the expanded viewholder, especially when views are recycled in
468   * {@link RecyclerView}. Since we can only have one expanded voicemail view holder, this method
469   * ensures that except for the currently expanded view holder, all the other view holders visible
470   * on the screen are collapsed.
471   *
472   * <p>The {@link NewVoicemailMediaPlayer} is also reset, if there is an existing playing
473   * voicemail.
474   *
475   * <p>This is the function that is responsible of keeping track of the expanded viewholder in the
476   * {@link NewVoicemailAdapter}
477   *
478   * <p>This is the first function called in the adapter when a viewholder has been expanded.
479   *
480   * <p>This is the function that is responsible of keeping track of the expanded viewholder in the
481   * {@link NewVoicemailAdapter}
482   *
483   * @param viewHolderRequestedToExpand is the view holder that is currently expanded.
484   * @param voicemailEntryOfViewHolder
485   */
486  @Override
487  public void expandViewHolderFirstTimeAndCollapseAllOtherVisibleViewHolders(
488      NewVoicemailViewHolder viewHolderRequestedToExpand,
489      VoicemailEntry voicemailEntryOfViewHolder,
490      NewVoicemailViewHolderListener listener) {
491
492    LogUtil.i(
493        "NewVoicemailAdapter.expandViewHolderFirstTimeAndCollapseAllOtherVisibleViewHolders",
494        "viewholder id:%d being request to expand, isExpanded:%b, size of our view holder "
495            + "dataset:%d, hashmap size:%d",
496        viewHolderRequestedToExpand.getViewHolderId(),
497        viewHolderRequestedToExpand.isViewHolderExpanded(),
498        newVoicemailViewHolderSet.size(),
499        newVoicemailViewHolderArrayMap.size());
500
501    currentlyExpandedViewHolderId = viewHolderRequestedToExpand.getViewHolderId();
502
503    for (NewVoicemailViewHolder viewHolder : newVoicemailViewHolderSet) {
504      if (viewHolder.getViewHolderId() != viewHolderRequestedToExpand.getViewHolderId()) {
505        viewHolder.collapseViewHolder();
506      }
507    }
508
509    // If the media player is playing and we expand something other than the currently playing one
510    // we should stop playing the media player
511    if (mediaPlayer.isPlaying()
512        && !Objects.equals(
513            mediaPlayer.getLastPlayedOrPlayingVoicemailUri(),
514            viewHolderRequestedToExpand.getViewHolderVoicemailUri())) {
515      LogUtil.i(
516          "NewVoicemailAdapter.expandViewHolderFirstTimeAndCollapseAllOtherVisibleViewHolders",
517          "Reset the media player since we expanded something other that the playing "
518              + "voicemail, MP was playing:%s, viewholderExpanded:%d, MP.isPlaying():%b",
519          String.valueOf(mediaPlayer.getLastPlayedOrPlayingVoicemailUri()),
520          viewHolderRequestedToExpand.getViewHolderId(),
521          mediaPlayer.isPlaying());
522      mediaPlayer.reset();
523    }
524
525    // If the media player is paused and we expand something other than the currently paused one
526    // we should stop playing the media player
527    if (mediaPlayer.isPaused()
528        && !Objects.equals(
529            mediaPlayer.getLastPausedVoicemailUri(),
530            viewHolderRequestedToExpand.getViewHolderVoicemailUri())) {
531      LogUtil.i(
532          "NewVoicemailAdapter.expandViewHolderFirstTimeAndCollapseAllOtherVisibleViewHolders",
533          "There was an existing paused viewholder, the media player should reset since we "
534              + "expanded something other that the paused voicemail, MP.paused:%s",
535          String.valueOf(mediaPlayer.getLastPausedVoicemailUri()));
536      mediaPlayer.reset();
537    }
538
539    Assert.checkArgument(
540        !viewHolderRequestedToExpand.isViewHolderExpanded(),
541        "cannot expand a voicemail that is not collapsed");
542
543    viewHolderRequestedToExpand.expandAndBindViewHolderAndMediaPlayerViewWithAdapterValues(
544        voicemailEntryOfViewHolder, fragmentManager, mediaPlayer, listener);
545
546    // There should be nothing playing when we expand a viewholder for the first time
547    Assert.checkArgument(!mediaPlayer.isPlaying());
548  }
549
550  /**
551   * Ensures that when we collapse the expanded view, we don't expand it again when we are recycling
552   * the viewholders. If we collapse an existing playing voicemail viewholder, we should stop
553   * playing it.
554   *
555   * @param collapseViewHolder is the view holder that is currently collapsed.
556   */
557  @Override
558  public void collapseExpandedViewHolder(NewVoicemailViewHolder collapseViewHolder) {
559    Assert.checkArgument(collapseViewHolder.getViewHolderId() == currentlyExpandedViewHolderId);
560    collapseViewHolder.collapseViewHolder();
561    currentlyExpandedViewHolderId = -1;
562
563    // If the view holder is currently playing, then we should stop playing it.
564    if (mediaPlayer.isPlaying()) {
565      Assert.checkArgument(
566          Objects.equals(
567              mediaPlayer.getLastPlayedOrPlayingVoicemailUri(),
568              collapseViewHolder.getViewHolderVoicemailUri()),
569          "the voicemail being played should have been of the recently collapsed view holder.");
570      mediaPlayer.reset();
571    }
572  }
573
574  @Override
575  public void pauseViewHolder(NewVoicemailViewHolder expandedViewHolder) {
576    Assert.isNotNull(
577        getCurrentlyExpandedViewHolder(),
578        "cannot have pressed pause if the viewholder wasn't expanded");
579    Assert.checkArgument(
580        getCurrentlyExpandedViewHolder()
581            .getViewHolderVoicemailUri()
582            .equals(expandedViewHolder.getViewHolderVoicemailUri()),
583        "view holder whose pause button was pressed has to have been the expanded "
584            + "viewholder being tracked by the adapter.");
585    mediaPlayer.pauseMediaPlayer(expandedViewHolder.getViewHolderVoicemailUri());
586    expandedViewHolder.setPausedStateOfMediaPlayerView(
587        expandedViewHolder.getViewHolderVoicemailUri(), mediaPlayer);
588  }
589
590  @Override
591  public void resumePausedViewHolder(NewVoicemailViewHolder expandedViewHolder) {
592    Assert.isNotNull(
593        getCurrentlyExpandedViewHolder(),
594        "cannot have pressed pause if the viewholder wasn't expanded");
595    Assert.checkArgument(
596        getCurrentlyExpandedViewHolder()
597            .getViewHolderVoicemailUri()
598            .equals(expandedViewHolder.getViewHolderVoicemailUri()),
599        "view holder whose play button was pressed has to have been the expanded "
600            + "viewholder being tracked by the adapter.");
601    Assert.isNotNull(
602        mediaPlayer.getLastPausedVoicemailUri(), "there should be be an pausedUri to resume");
603    Assert.checkArgument(
604        mediaPlayer
605            .getLastPlayedOrPlayingVoicemailUri()
606            .equals(expandedViewHolder.getViewHolderVoicemailUri()),
607        "only the last playing uri can be resumed");
608    Assert.checkArgument(
609        mediaPlayer
610            .getLastPreparedOrPreparingToPlayVoicemailUri()
611            .equals(expandedViewHolder.getViewHolderVoicemailUri()),
612        "only the last prepared uri can be resumed");
613    Assert.checkArgument(
614        mediaPlayer
615            .getLastPreparedOrPreparingToPlayVoicemailUri()
616            .equals(mediaPlayer.getLastPlayedOrPlayingVoicemailUri()),
617        "the last prepared and playing voicemails have to be the same when resuming");
618
619    onPreparedListener.onPrepared(mediaPlayer.getMediaPlayer());
620  }
621
622  @Override
623  public void deleteViewHolder(
624      Context context,
625      FragmentManager fragmentManager,
626      NewVoicemailViewHolder expandedViewHolder,
627      Uri voicemailUri) {
628    LogUtil.i(
629        "NewVoicemailAdapter.deleteViewHolder",
630        "deleting adapter position %d, id:%d, uri:%s ",
631        expandedViewHolder.getAdapterPosition(),
632        expandedViewHolder.getViewHolderId(),
633        String.valueOf(voicemailUri));
634
635    deletedVoicemailPosition.add(expandedViewHolder.getAdapterPosition());
636
637    Assert.checkArgument(expandedViewHolder.getViewHolderVoicemailUri().equals(voicemailUri));
638
639    Assert.checkArgument(currentlyExpandedViewHolderId == expandedViewHolder.getViewHolderId());
640
641    collapseExpandedViewHolder(expandedViewHolder);
642
643    Worker<Pair<Context, Uri>, Void> deleteVoicemail = this::deleteVoicemail;
644
645    DialerExecutorComponent.get(context)
646        .dialerExecutorFactory()
647        .createNonUiTaskBuilder(deleteVoicemail)
648        .build()
649        .executeSerial(new Pair<>(context, voicemailUri));
650
651    notifyItemRemoved(expandedViewHolder.getAdapterPosition());
652  }
653
654  @WorkerThread
655  private Void deleteVoicemail(Pair<Context, Uri> contextUriPair) {
656    Assert.isWorkerThread();
657    LogUtil.enterBlock("NewVoicemailAdapter.deleteVoicemail");
658
659    Context context = contextUriPair.first;
660    Uri uri = contextUriPair.second;
661    LogUtil.i("NewVoicemailAdapter.deleteVoicemail", "deleting uri:%s", String.valueOf(uri));
662    ContentValues values = new ContentValues();
663    values.put(Voicemails.DELETED, "1");
664
665    int numRowsUpdated = context.getContentResolver().update(uri, values, null, null);
666
667    LogUtil.i("NewVoicemailAdapter.onVoicemailDeleted", "return value:%d", numRowsUpdated);
668    Assert.checkArgument(numRowsUpdated == 1, "voicemail delete was not successful");
669
670    Intent intent = new Intent(VoicemailClient.ACTION_UPLOAD);
671    intent.setPackage(context.getPackageName());
672    context.sendBroadcast(intent);
673    return null;
674  }
675
676  /**
677   * This function is called recursively to update the seekbar, duration, play/pause buttons of the
678   * expanded view holder if its playing.
679   *
680   * <p>Since this function is called at 30 frames/second, its possible (and eventually will happen)
681   * that between each update the playing voicemail state could have changed, in which case this
682   * method should stop calling itself. These conditions are:
683   *
684   * <ul>
685   *   <li>The user scrolled the playing voicemail out of view.
686   *   <li>Another view holder was expanded.
687   *   <li>The playing voicemail was paused.
688   *   <li>The media player returned {@link MediaPlayer#isPlaying()} to be true but had its {@link
689   *       MediaPlayer#getCurrentPosition()} > {@link MediaPlayer#getDuration()}.
690   *   <li>The {@link MediaPlayer} stopped playing.
691   * </ul>
692   *
693   * <p>Note: Since the update happens at 30 frames/second, it's also possible that the viewholder
694   * was recycled when scrolling the playing voicemail out of view.
695   *
696   * @param expandedViewHolderPossiblyPlaying the view holder that was expanded and could or could
697   *     not be playing. This viewholder can be recycled.
698   */
699  private void recursivelyUpdateMediaPlayerViewOfExpandedViewHolder(
700      NewVoicemailViewHolder expandedViewHolderPossiblyPlaying) {
701    // TODO(uabdullah): a bug Remove logging, temporarily here for debugging.
702    LogUtil.i(
703        "NewVoicemailAdapter.recursivelyUpdateMediaPlayerViewOfExpandedViewHolder",
704        "currentlyExpanded:%d",
705        currentlyExpandedViewHolderId);
706
707    // It's possible that by the time this is run, the expanded view holder has been
708    // scrolled out of view (and possibly recycled)
709    if (getCurrentlyExpandedViewHolder() == null) {
710      LogUtil.i(
711          "NewVoicemailAdapter.recursivelyUpdateMediaPlayerViewOfExpandedViewHolder",
712          "viewholder:%d media player view, no longer on screen, no need to update",
713          expandedViewHolderPossiblyPlaying.getViewHolderId());
714      return;
715    }
716
717    // Another viewholder was expanded, no need to update
718    if (!getCurrentlyExpandedViewHolder().equals(expandedViewHolderPossiblyPlaying)) {
719      LogUtil.i(
720          "NewVoicemailAdapter.recursivelyUpdateMediaPlayerViewOfExpandedViewHolder",
721          "currentlyExpandedViewHolderId:%d and the one we are attempting to update:%d "
722              + "aren't the same.",
723          currentlyExpandedViewHolderId,
724          expandedViewHolderPossiblyPlaying.getViewHolderId());
725      return;
726    }
727
728    Assert.checkArgument(expandedViewHolderPossiblyPlaying.isViewHolderExpanded());
729    Assert.checkArgument(
730        expandedViewHolderPossiblyPlaying.getViewHolderId()
731            == getCurrentlyExpandedViewHolder().getViewHolderId());
732
733    // If the viewholder was paused, there is no need to update the media player view
734    if (mediaPlayer.isPaused()) {
735      Assert.checkArgument(
736          expandedViewHolderPossiblyPlaying
737              .getViewHolderVoicemailUri()
738              .equals(mediaPlayer.getLastPausedVoicemailUri()),
739          "only the expanded viewholder can be paused.");
740
741      LogUtil.i(
742          "NewVoicemailAdapter.recursivelyUpdateMediaPlayerViewOfExpandedViewHolder",
743          "set the media player to a paused state");
744      expandedViewHolderPossiblyPlaying.setPausedStateOfMediaPlayerView(
745          expandedViewHolderPossiblyPlaying.getViewHolderVoicemailUri(), mediaPlayer);
746      return;
747    }
748
749    // In some weird corner cases a media player could return isPlaying() as true but would
750    // have getCurrentPosition > getDuration(). We consider that as the voicemail has finished
751    // playing.
752    if (mediaPlayer.isPlaying() && mediaPlayer.getCurrentPosition() < mediaPlayer.getDuration()) {
753
754      Assert.checkArgument(
755          mediaPlayer
756              .getLastPlayedOrPlayingVoicemailUri()
757              .equals(getCurrentlyExpandedViewHolder().getViewHolderVoicemailUri()));
758      // TODO(uabdullah): a bug Remove logging, temporarily here for debugging.
759      LogUtil.i(
760          "NewVoicemailAdapter.recursivelyUpdateMediaPlayerViewOfExpandedViewHolder",
761          "recursely update the player, currentlyExpanded:%d",
762          expandedViewHolderPossiblyPlaying.getViewHolderId());
763
764      Assert.checkArgument(
765          expandedViewHolderPossiblyPlaying
766              .getViewHolderVoicemailUri()
767              .equals(getCurrentlyExpandedViewHolder().getViewHolderVoicemailUri()));
768
769      expandedViewHolderPossiblyPlaying.updateMediaPlayerViewWithPlayingState(
770          expandedViewHolderPossiblyPlaying, mediaPlayer);
771
772      ThreadUtil.postDelayedOnUiThread(
773          new Runnable() {
774            @Override
775            public void run() {
776              recursivelyUpdateMediaPlayerViewOfExpandedViewHolder(
777                  expandedViewHolderPossiblyPlaying);
778            }
779          },
780          1000 / 30 /*30 FPS*/);
781      return;
782    }
783
784    if (!mediaPlayer.isPlaying()
785        || (mediaPlayer.isPlaying()
786            && mediaPlayer.getCurrentPosition() > mediaPlayer.getDuration())) {
787      LogUtil.i(
788          "NewVoicemailAdapter.recursivelyUpdateMediaPlayerViewOfExpandedViewHolder",
789          "resetting the player, currentlyExpanded:%d, MPPlaying:%b",
790          getCurrentlyExpandedViewHolder().getViewHolderId(),
791          mediaPlayer.isPlaying());
792      mediaPlayer.reset();
793      Assert.checkArgument(
794          expandedViewHolderPossiblyPlaying
795              .getViewHolderVoicemailUri()
796              .equals(getCurrentlyExpandedViewHolder().getViewHolderVoicemailUri()));
797      expandedViewHolderPossiblyPlaying.setMediaPlayerViewToResetState(
798          expandedViewHolderPossiblyPlaying, mediaPlayer);
799      return;
800    }
801
802    String error =
803        String.format(
804            "expandedViewHolderPossiblyPlaying:%d, expanded:%b, CurrentExpanded:%d, uri:%s, "
805                + "MPPlaying:%b, MPPaused:%b, MPPreparedUri:%s, MPPausedUri:%s",
806            expandedViewHolderPossiblyPlaying.getViewHolderId(),
807            expandedViewHolderPossiblyPlaying.isViewHolderExpanded(),
808            currentlyExpandedViewHolderId,
809            String.valueOf(expandedViewHolderPossiblyPlaying.getViewHolderVoicemailUri()),
810            mediaPlayer.isPlaying(),
811            mediaPlayer.isPaused(),
812            String.valueOf(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri()),
813            String.valueOf(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri()));
814
815    throw Assert.createAssertionFailException(
816        "All cases should have been handled before. Error " + error);
817  }
818
819  // When a voicemail has finished playing.
820  OnCompletionListener onCompletionListener =
821      new OnCompletionListener() {
822
823        @Override
824        public void onCompletion(MediaPlayer mp) {
825          Assert.checkArgument(
826              mediaPlayer
827                  .getLastPlayedOrPlayingVoicemailUri()
828                  .equals(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri()));
829          Assert.checkArgument(!mediaPlayer.isPlaying());
830
831          LogUtil.i(
832              "NewVoicemailAdapter.onCompletionListener",
833              "completed playing voicemailUri: %s, expanded viewholder is %d, visibility :%b",
834              mediaPlayer.getLastPlayedOrPlayingVoicemailUri().toString(),
835              currentlyExpandedViewHolderId,
836              isCurrentlyExpandedViewHolderInViewHolderSet());
837
838          Assert.checkArgument(
839              currentlyExpandedViewHolderId != -1,
840              "a voicemail that was never expanded, should never be playing.");
841          mediaPlayer.reset();
842        }
843      };
844
845  // When a voicemail has been prepared and can be played
846  private final OnPreparedListener onPreparedListener =
847      new OnPreparedListener() {
848
849        /**
850         * When a user pressed the play button, this listener should be called immediately. The
851         * asserts ensures that is the case. This function starts playing the voicemail and updates
852         * the UI.
853         */
854        @Override
855        public void onPrepared(MediaPlayer mp) {
856          LogUtil.i(
857              "NewVoicemailAdapter.onPrepared",
858              "MPPreparedUri: %s, currentlyExpandedViewHolderId:%d, and its visibility on "
859                  + "the screen is:%b",
860              String.valueOf(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri()),
861              currentlyExpandedViewHolderId,
862              isCurrentlyExpandedViewHolderInViewHolderSet());
863
864          NewVoicemailViewHolder currentlyExpandedViewHolder = getCurrentlyExpandedViewHolder();
865          Assert.checkArgument(currentlyExpandedViewHolder != null);
866          Assert.checkArgument(
867              currentlyExpandedViewHolder
868                  .getViewHolderVoicemailUri()
869                  .equals(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri()),
870              "should only have prepared the last expanded view holder.");
871
872          mediaPlayer.start(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri());
873
874          recursivelyUpdateMediaPlayerViewOfExpandedViewHolder(currentlyExpandedViewHolder);
875
876          Assert.checkArgument(mediaPlayer.isPlaying());
877          LogUtil.i("NewVoicemailAdapter.onPrepared", "voicemail should be playing");
878        }
879      };
880
881  // TODO(uabdullah): when playing the voicemail results in an error
882  // we must update the viewholder and mention there was an error playing the voicemail, and reset
883  // the media player and the media player view
884  private final OnErrorListener onErrorListener =
885      new OnErrorListener() {
886        @Override
887        public boolean onError(MediaPlayer mp, int what, int extra) {
888          LogUtil.e("NewVoicemailAdapter.onError", "onError, what:%d, extra:%d", what, extra);
889          Assert.checkArgument(
890              mediaPlayer.getMediaPlayer().equals(mp),
891              "there should always only be one instance of the media player");
892          Assert.checkArgument(
893              mediaPlayer
894                  .getLastPlayedOrPlayingVoicemailUri()
895                  .equals(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri()));
896          LogUtil.i(
897              "NewVoicemailAdapter.onErrorListener",
898              "error playing voicemailUri: %s",
899              mediaPlayer.getLastPlayedOrPlayingVoicemailUri().toString());
900          return false;
901        }
902      };
903
904  private boolean isCurrentlyExpandedViewHolderInViewHolderSet() {
905    for (NewVoicemailViewHolder viewHolder : newVoicemailViewHolderSet) {
906      if (viewHolder.getViewHolderId() == currentlyExpandedViewHolderId) {
907        return true;
908      }
909    }
910    return false;
911  }
912
913  /**
914   * The expanded view holder may or may not be visible on the screen. Since the {@link
915   * NewVoicemailViewHolder} may be recycled, it's possible that the expanded view holder is
916   * recycled for a non-expanded view holder when the expanded view holder is scrolled out of view.
917   *
918   * @return the expanded view holder if it is amongst the recycled views on the screen, otherwise
919   *     null.
920   */
921  @Nullable
922  private NewVoicemailViewHolder getCurrentlyExpandedViewHolder() {
923    if (newVoicemailViewHolderArrayMap.containsKey(currentlyExpandedViewHolderId)) {
924      Assert.checkArgument(
925          newVoicemailViewHolderArrayMap.get(currentlyExpandedViewHolderId).getViewHolderId()
926              == currentlyExpandedViewHolderId);
927      return newVoicemailViewHolderArrayMap.get(currentlyExpandedViewHolderId);
928    } else {
929      // returned when currentlyExpandedViewHolderId = -1 (viewholder was collapsed)
930      LogUtil.i(
931          "NewVoicemailAdapter.getCurrentlyExpandedViewHolder",
932          "no view holder found in hashmap size:%d for %d",
933          newVoicemailViewHolderArrayMap.size(),
934          currentlyExpandedViewHolderId);
935      // TODO(uabdullah): a bug Remove logging, temporarily here for debugging.
936      printHashSet();
937      printArrayMap();
938      return null;
939    }
940  }
941
942  @Override
943  public int getItemCount() {
944    // TODO(uabdullah): a bug Remove logging, temporarily here for debugging.
945    LogUtil.enterBlock("NewVoicemailAdapter.getItemCount");
946    int numberOfHeaders = 0;
947    if (voicemailAlertPosition != Integer.MAX_VALUE) {
948      numberOfHeaders++;
949    }
950    if (todayHeaderPosition != Integer.MAX_VALUE) {
951      numberOfHeaders++;
952    }
953    if (yesterdayHeaderPosition != Integer.MAX_VALUE) {
954      numberOfHeaders++;
955    }
956    if (olderHeaderPosition != Integer.MAX_VALUE) {
957      numberOfHeaders++;
958    }
959    // TODO(uabdullah): a bug Remove logging, temporarily here for debugging.
960    LogUtil.i(
961        "NewVoicemailAdapter.getItemCount",
962        "cursor cnt:%d, num of headers:%d, delete size:%d",
963        cursor.getCount(),
964        numberOfHeaders,
965        deletedVoicemailPosition.size());
966    return cursor.getCount() + numberOfHeaders - deletedVoicemailPosition.size();
967  }
968
969  @RowType
970  @Override
971  public int getItemViewType(int position) {
972    LogUtil.enterBlock("NewVoicemailAdapter.getItemViewType");
973    if (voicemailAlertPosition != Integer.MAX_VALUE && position == voicemailAlertPosition) {
974      return RowType.VOICEMAIL_ALERT;
975    }
976    if (todayHeaderPosition != Integer.MAX_VALUE && position == todayHeaderPosition) {
977      return RowType.HEADER;
978    }
979    if (yesterdayHeaderPosition != Integer.MAX_VALUE && position == yesterdayHeaderPosition) {
980      return RowType.HEADER;
981    }
982    if (olderHeaderPosition != Integer.MAX_VALUE && position == olderHeaderPosition) {
983      return RowType.HEADER;
984    }
985    return RowType.VOICEMAIL_ENTRY;
986  }
987
988  /**
989   * This will be called once the voicemail that was attempted to be played (and was not locally
990   * available) was downloaded from the server. However it is possible that by the time the download
991   * was completed, the view holder was collapsed. In that case we shouldn't play the voicemail.
992   */
993  public void checkAndPlayVoicemail() {
994    LogUtil.i(
995        "NewVoicemailAdapter.checkAndPlayVoicemail",
996        "expandedViewHolder:%d, inViewHolderSet:%b, MPRequestToDownload:%s",
997        currentlyExpandedViewHolderId,
998        isCurrentlyExpandedViewHolderInViewHolderSet(),
999        String.valueOf(mediaPlayer.getVoicemailRequestedToDownload()));
1000
1001    NewVoicemailViewHolder currentlyExpandedViewHolder = getCurrentlyExpandedViewHolder();
1002    if (currentlyExpandedViewHolderId != -1
1003        && isCurrentlyExpandedViewHolderInViewHolderSet()
1004        && currentlyExpandedViewHolder != null
1005        // Used to differentiate underlying table changes from voicemail downloads and other changes
1006        // (e.g delete)
1007        && mediaPlayer.getVoicemailRequestedToDownload() != null
1008        && (mediaPlayer
1009            .getVoicemailRequestedToDownload()
1010            .equals(currentlyExpandedViewHolder.getViewHolderVoicemailUri()))) {
1011      currentlyExpandedViewHolder.clickPlayButtonOfViewHoldersMediaPlayerView(
1012          currentlyExpandedViewHolder);
1013    } else {
1014      LogUtil.i("NewVoicemailAdapter.checkAndPlayVoicemail", "not playing downloaded voicemail");
1015    }
1016  }
1017
1018  /**
1019   * Updates the voicemail alert message to reflect the state of the {@link VoicemailStatus} table.
1020   * TODO(uabdullah): Handle ToS properly (a bug)
1021   */
1022  public void updateVoicemailAlertWithMostRecentStatus(
1023      Context context, ImmutableList<VoicemailStatus> voicemailStatuses) {
1024
1025    if (voicemailStatuses.isEmpty()) {
1026      LogUtil.i(
1027          "NewVoicemailAdapter.updateVoicemailAlertWithMostRecentStatus",
1028          "voicemailStatuses was empty");
1029      return;
1030    }
1031
1032    voicemailErrorMessage = null;
1033    VoicemailErrorMessageCreator messageCreator = new VoicemailErrorMessageCreator();
1034
1035    for (VoicemailStatus status : voicemailStatuses) {
1036      voicemailErrorMessage = messageCreator.create(context, status, null);
1037      if (voicemailErrorMessage != null) {
1038        break;
1039      }
1040    }
1041
1042    if (voicemailErrorMessage != null) {
1043      LogUtil.i("NewVoicemailAdapter.updateVoicemailAlertWithMostRecentStatus", "showing alert");
1044      voicemailAlertPosition = 0;
1045      updateHeaderPositions();
1046      notifyItemChanged(0);
1047    }
1048  }
1049}
1050