1 /* 2 * Copyright (C) 2011 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 17 package com.android.contacts.voicemail; 18 19 import static com.android.contacts.CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK; 20 import static com.android.contacts.CallDetailActivity.EXTRA_VOICEMAIL_URI; 21 22 import com.android.common.io.MoreCloseables; 23 import com.android.contacts.ProximitySensorAware; 24 import com.android.contacts.R; 25 import com.android.contacts.util.AsyncTaskExecutors; 26 import com.android.ex.variablespeed.MediaPlayerProxy; 27 import com.android.ex.variablespeed.VariableSpeed; 28 import com.google.common.base.Preconditions; 29 30 import android.app.Activity; 31 import android.app.Fragment; 32 import android.content.ContentResolver; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.database.ContentObserver; 36 import android.database.Cursor; 37 import android.media.AudioManager; 38 import android.net.Uri; 39 import android.os.Bundle; 40 import android.os.PowerManager; 41 import android.provider.VoicemailContract; 42 import android.util.Log; 43 import android.view.LayoutInflater; 44 import android.view.View; 45 import android.view.ViewGroup; 46 import android.widget.ImageButton; 47 import android.widget.SeekBar; 48 import android.widget.TextView; 49 50 import java.util.concurrent.ExecutorService; 51 import java.util.concurrent.Executors; 52 import java.util.concurrent.ScheduledExecutorService; 53 import java.util.concurrent.TimeUnit; 54 55 import javax.annotation.concurrent.GuardedBy; 56 import javax.annotation.concurrent.NotThreadSafe; 57 58 /** 59 * Displays and plays back a single voicemail. 60 * <p> 61 * When the Activity containing this Fragment is created, voicemail playback 62 * will begin immediately. The Activity is expected to be started via an intent 63 * containing a suitable voicemail uri to playback. 64 * <p> 65 * This class is not thread-safe, it is thread-confined. All calls to all public 66 * methods on this class are expected to come from the main ui thread. 67 */ 68 @NotThreadSafe 69 public class VoicemailPlaybackFragment extends Fragment { 70 private static final String TAG = "VoicemailPlayback"; 71 private static final int NUMBER_OF_THREADS_IN_POOL = 2; 72 private static final String[] HAS_CONTENT_PROJECTION = new String[] { 73 VoicemailContract.Voicemails.HAS_CONTENT, 74 }; 75 76 private VoicemailPlaybackPresenter mPresenter; 77 private ScheduledExecutorService mScheduledExecutorService; 78 private View mPlaybackLayout; 79 80 @Override 81 public View onCreateView(LayoutInflater inflater, ViewGroup container, 82 Bundle savedInstanceState) { 83 mPlaybackLayout = inflater.inflate(R.layout.playback_layout, null); 84 return mPlaybackLayout; 85 } 86 87 @Override 88 public void onActivityCreated(Bundle savedInstanceState) { 89 super.onActivityCreated(savedInstanceState); 90 mScheduledExecutorService = createScheduledExecutorService(); 91 Bundle arguments = getArguments(); 92 Preconditions.checkNotNull(arguments, "fragment must be started with arguments"); 93 Uri voicemailUri = arguments.getParcelable(EXTRA_VOICEMAIL_URI); 94 Preconditions.checkNotNull(voicemailUri, "fragment must contain EXTRA_VOICEMAIL_URI"); 95 boolean startPlayback = arguments.getBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, false); 96 PowerManager powerManager = 97 (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE); 98 PowerManager.WakeLock wakeLock = 99 powerManager.newWakeLock( 100 PowerManager.SCREEN_DIM_WAKE_LOCK, getClass().getSimpleName()); 101 mPresenter = new VoicemailPlaybackPresenter(createPlaybackViewImpl(), 102 createMediaPlayer(mScheduledExecutorService), voicemailUri, 103 mScheduledExecutorService, startPlayback, 104 AsyncTaskExecutors.createAsyncTaskExecutor(), wakeLock); 105 mPresenter.onCreate(savedInstanceState); 106 } 107 108 @Override 109 public void onSaveInstanceState(Bundle outState) { 110 mPresenter.onSaveInstanceState(outState); 111 super.onSaveInstanceState(outState); 112 } 113 114 @Override 115 public void onDestroy() { 116 mPresenter.onDestroy(); 117 mScheduledExecutorService.shutdown(); 118 super.onDestroy(); 119 } 120 121 @Override 122 public void onPause() { 123 mPresenter.onPause(); 124 super.onPause(); 125 } 126 127 private PlaybackViewImpl createPlaybackViewImpl() { 128 return new PlaybackViewImpl(new ActivityReference(), getActivity().getApplicationContext(), 129 mPlaybackLayout); 130 } 131 132 private MediaPlayerProxy createMediaPlayer(ExecutorService executorService) { 133 return VariableSpeed.createVariableSpeed(executorService); 134 } 135 136 private ScheduledExecutorService createScheduledExecutorService() { 137 return Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL); 138 } 139 140 /** 141 * Formats a number of milliseconds as something that looks like {@code 00:05}. 142 * <p> 143 * We always use four digits, two for minutes two for seconds. In the very unlikely event 144 * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes. 145 */ 146 private static String formatAsMinutesAndSeconds(int millis) { 147 int seconds = millis / 1000; 148 int minutes = seconds / 60; 149 seconds -= minutes * 60; 150 if (minutes > 99) { 151 minutes = 99; 152 } 153 return String.format("%02d:%02d", minutes, seconds); 154 } 155 156 /** 157 * An object that can provide us with an Activity. 158 * <p> 159 * Fragments suffer the drawback that the Activity they belong to may sometimes be null. This 160 * can happen if the Fragment is detached, for example. In that situation a call to 161 * {@link Fragment#getString(int)} will throw and {@link IllegalStateException}. Also, calling 162 * {@link Fragment#getActivity()} is dangerous - it may sometimes return null. And thus blindly 163 * calling a method on the result of getActivity() is dangerous too. 164 * <p> 165 * To work around this, I have made the {@link PlaybackViewImpl} class static, so that it does 166 * not have access to any Fragment methods directly. Instead it uses an application Context for 167 * things like accessing strings, accessing system services. It only uses the Activity when it 168 * absolutely needs it - and does so through this class. This makes it easy to see where we have 169 * to check for null properly. 170 */ 171 private final class ActivityReference { 172 /** Gets this Fragment's Activity: <b>may be null</b>. */ 173 public final Activity get() { 174 return getActivity(); 175 } 176 } 177 178 /** Methods required by the PlaybackView for the VoicemailPlaybackPresenter. */ 179 private static final class PlaybackViewImpl implements VoicemailPlaybackPresenter.PlaybackView { 180 private final ActivityReference mActivityReference; 181 private final Context mApplicationContext; 182 private final SeekBar mPlaybackSeek; 183 private final ImageButton mStartStopButton; 184 private final ImageButton mPlaybackSpeakerphone; 185 private final ImageButton mRateDecreaseButton; 186 private final ImageButton mRateIncreaseButton; 187 private final TextViewWithMessagesController mTextController; 188 189 public PlaybackViewImpl(ActivityReference activityReference, Context applicationContext, 190 View playbackLayout) { 191 Preconditions.checkNotNull(activityReference); 192 Preconditions.checkNotNull(applicationContext); 193 Preconditions.checkNotNull(playbackLayout); 194 mActivityReference = activityReference; 195 mApplicationContext = applicationContext; 196 mPlaybackSeek = (SeekBar) playbackLayout.findViewById(R.id.playback_seek); 197 mStartStopButton = (ImageButton) playbackLayout.findViewById( 198 R.id.playback_start_stop); 199 mPlaybackSpeakerphone = (ImageButton) playbackLayout.findViewById( 200 R.id.playback_speakerphone); 201 mRateDecreaseButton = (ImageButton) playbackLayout.findViewById( 202 R.id.rate_decrease_button); 203 mRateIncreaseButton = (ImageButton) playbackLayout.findViewById( 204 R.id.rate_increase_button); 205 mTextController = new TextViewWithMessagesController( 206 (TextView) playbackLayout.findViewById(R.id.playback_position_text), 207 (TextView) playbackLayout.findViewById(R.id.playback_speed_text)); 208 } 209 210 @Override 211 public void finish() { 212 Activity activity = mActivityReference.get(); 213 if (activity != null) { 214 activity.finish(); 215 } 216 } 217 218 @Override 219 public void runOnUiThread(Runnable runnable) { 220 Activity activity = mActivityReference.get(); 221 if (activity != null) { 222 activity.runOnUiThread(runnable); 223 } 224 } 225 226 @Override 227 public Context getDataSourceContext() { 228 return mApplicationContext; 229 } 230 231 @Override 232 public void setRateDecreaseButtonListener(View.OnClickListener listener) { 233 mRateDecreaseButton.setOnClickListener(listener); 234 } 235 236 @Override 237 public void setRateIncreaseButtonListener(View.OnClickListener listener) { 238 mRateIncreaseButton.setOnClickListener(listener); 239 } 240 241 @Override 242 public void setStartStopListener(View.OnClickListener listener) { 243 mStartStopButton.setOnClickListener(listener); 244 } 245 246 @Override 247 public void setSpeakerphoneListener(View.OnClickListener listener) { 248 mPlaybackSpeakerphone.setOnClickListener(listener); 249 } 250 251 @Override 252 public void setRateDisplay(float rate, int stringResourceId) { 253 mTextController.setTemporaryText( 254 mApplicationContext.getString(stringResourceId), 1, TimeUnit.SECONDS); 255 } 256 257 @Override 258 public void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener) { 259 mPlaybackSeek.setOnSeekBarChangeListener(listener); 260 } 261 262 @Override 263 public void playbackStarted() { 264 mStartStopButton.setImageResource(R.drawable.ic_hold_pause_holo_dark); 265 } 266 267 @Override 268 public void playbackStopped() { 269 mStartStopButton.setImageResource(R.drawable.ic_play); 270 } 271 272 @Override 273 public void enableProximitySensor() { 274 // Only change the state if the activity is still around. 275 Activity activity = mActivityReference.get(); 276 if (activity != null && activity instanceof ProximitySensorAware) { 277 ((ProximitySensorAware) activity).enableProximitySensor(); 278 } 279 } 280 281 @Override 282 public void disableProximitySensor() { 283 // Only change the state if the activity is still around. 284 Activity activity = mActivityReference.get(); 285 if (activity != null && activity instanceof ProximitySensorAware) { 286 ((ProximitySensorAware) activity).disableProximitySensor(true); 287 } 288 } 289 290 @Override 291 public void registerContentObserver(Uri uri, ContentObserver observer) { 292 mApplicationContext.getContentResolver().registerContentObserver(uri, false, observer); 293 } 294 295 @Override 296 public void unregisterContentObserver(ContentObserver observer) { 297 mApplicationContext.getContentResolver().unregisterContentObserver(observer); 298 } 299 300 @Override 301 public void setClipPosition(int clipPositionInMillis, int clipLengthInMillis) { 302 int seekBarPosition = Math.max(0, clipPositionInMillis); 303 int seekBarMax = Math.max(seekBarPosition, clipLengthInMillis); 304 if (mPlaybackSeek.getMax() != seekBarMax) { 305 mPlaybackSeek.setMax(seekBarMax); 306 } 307 mPlaybackSeek.setProgress(seekBarPosition); 308 mTextController.setPermanentText( 309 formatAsMinutesAndSeconds(seekBarMax - seekBarPosition)); 310 } 311 312 private String getString(int resId) { 313 return mApplicationContext.getString(resId); 314 } 315 316 @Override 317 public void setIsBuffering() { 318 disableUiElements(); 319 mTextController.setPermanentText(getString(R.string.voicemail_buffering)); 320 } 321 322 @Override 323 public void setIsFetchingContent() { 324 disableUiElements(); 325 mTextController.setPermanentText(getString(R.string.voicemail_fetching_content)); 326 } 327 328 @Override 329 public void setFetchContentTimeout() { 330 disableUiElements(); 331 mTextController.setPermanentText(getString(R.string.voicemail_fetching_timout)); 332 } 333 334 @Override 335 public int getDesiredClipPosition() { 336 return mPlaybackSeek.getProgress(); 337 } 338 339 @Override 340 public void disableUiElements() { 341 mRateIncreaseButton.setEnabled(false); 342 mRateDecreaseButton.setEnabled(false); 343 mStartStopButton.setEnabled(false); 344 mPlaybackSpeakerphone.setEnabled(false); 345 mPlaybackSeek.setProgress(0); 346 mPlaybackSeek.setEnabled(false); 347 } 348 349 @Override 350 public void playbackError(Exception e) { 351 disableUiElements(); 352 mTextController.setPermanentText(getString(R.string.voicemail_playback_error)); 353 Log.e(TAG, "Could not play voicemail", e); 354 } 355 356 @Override 357 public void enableUiElements() { 358 mRateIncreaseButton.setEnabled(true); 359 mRateDecreaseButton.setEnabled(true); 360 mStartStopButton.setEnabled(true); 361 mPlaybackSpeakerphone.setEnabled(true); 362 mPlaybackSeek.setEnabled(true); 363 } 364 365 @Override 366 public void sendFetchVoicemailRequest(Uri voicemailUri) { 367 Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, voicemailUri); 368 mApplicationContext.sendBroadcast(intent); 369 } 370 371 @Override 372 public boolean queryHasContent(Uri voicemailUri) { 373 ContentResolver contentResolver = mApplicationContext.getContentResolver(); 374 Cursor cursor = contentResolver.query( 375 voicemailUri, HAS_CONTENT_PROJECTION, null, null, null); 376 try { 377 if (cursor != null && cursor.moveToNext()) { 378 return cursor.getInt(cursor.getColumnIndexOrThrow( 379 VoicemailContract.Voicemails.HAS_CONTENT)) == 1; 380 } 381 } finally { 382 MoreCloseables.closeQuietly(cursor); 383 } 384 return false; 385 } 386 387 private AudioManager getAudioManager() { 388 return (AudioManager) mApplicationContext.getSystemService(Context.AUDIO_SERVICE); 389 } 390 391 @Override 392 public boolean isSpeakerPhoneOn() { 393 return getAudioManager().isSpeakerphoneOn(); 394 } 395 396 @Override 397 public void setSpeakerPhoneOn(boolean on) { 398 getAudioManager().setSpeakerphoneOn(on); 399 if (on) { 400 mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_on); 401 } else { 402 mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_off); 403 } 404 } 405 406 @Override 407 public void setVolumeControlStream(int streamType) { 408 Activity activity = mActivityReference.get(); 409 if (activity != null) { 410 activity.setVolumeControlStream(streamType); 411 } 412 } 413 } 414 415 /** 416 * Controls a TextView with dynamically changing text. 417 * <p> 418 * There are two methods here of interest, 419 * {@link TextViewWithMessagesController#setPermanentText(String)} and 420 * {@link TextViewWithMessagesController#setTemporaryText(String, long, TimeUnit)}. The 421 * former is used to set the text on the text view immediately, and is used in our case for 422 * the countdown of duration remaining during voicemail playback. The second is used to 423 * temporarily replace this countdown with a message, in our case faster voicemail speed or 424 * slower voicemail speed, before returning to the countdown display. 425 * <p> 426 * All the methods on this class must be called from the ui thread. 427 */ 428 private static final class TextViewWithMessagesController { 429 private static final float VISIBLE = 1; 430 private static final float INVISIBLE = 0; 431 private static final long SHORT_ANIMATION_MS = 200; 432 private static final long LONG_ANIMATION_MS = 400; 433 private final Object mLock = new Object(); 434 private final TextView mPermanentTextView; 435 private final TextView mTemporaryTextView; 436 @GuardedBy("mLock") private Runnable mRunnable; 437 438 public TextViewWithMessagesController(TextView permanentTextView, 439 TextView temporaryTextView) { 440 mPermanentTextView = permanentTextView; 441 mTemporaryTextView = temporaryTextView; 442 } 443 444 public void setPermanentText(String text) { 445 mPermanentTextView.setText(text); 446 } 447 448 public void setTemporaryText(String text, long duration, TimeUnit units) { 449 synchronized (mLock) { 450 mTemporaryTextView.setText(text); 451 mTemporaryTextView.animate().alpha(VISIBLE).setDuration(SHORT_ANIMATION_MS); 452 mPermanentTextView.animate().alpha(INVISIBLE).setDuration(SHORT_ANIMATION_MS); 453 mRunnable = new Runnable() { 454 @Override 455 public void run() { 456 synchronized (mLock) { 457 // We check for (mRunnable == this) becuase if not true, then another 458 // setTemporaryText call has taken place in the meantime, and this 459 // one is now defunct and needs to take no action. 460 if (mRunnable == this) { 461 mRunnable = null; 462 mTemporaryTextView.animate() 463 .alpha(INVISIBLE).setDuration(LONG_ANIMATION_MS); 464 mPermanentTextView.animate() 465 .alpha(VISIBLE).setDuration(LONG_ANIMATION_MS); 466 } 467 } 468 } 469 }; 470 mTemporaryTextView.postDelayed(mRunnable, units.toMillis(duration)); 471 } 472 } 473 } 474 } 475