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