1 /* 2 * Copyright (C) 2014 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.fmradio; 18 19 import android.app.Activity; 20 import android.app.FragmentManager; 21 import android.app.Notification; 22 import android.app.Notification.Builder; 23 import android.app.PendingIntent; 24 import android.content.ComponentName; 25 import android.content.ContentResolver; 26 import android.content.ContentUris; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.ServiceConnection; 30 import android.database.ContentObserver; 31 import android.database.Cursor; 32 import android.graphics.Bitmap; 33 import android.net.Uri; 34 import android.os.Bundle; 35 import android.os.Handler; 36 import android.os.Message; 37 import android.text.TextUtils; 38 import android.util.Log; 39 import android.view.View; 40 import android.widget.Button; 41 import android.widget.TextView; 42 import android.widget.Toast; 43 44 import com.android.fmradio.FmStation.Station; 45 import com.android.fmradio.dialogs.FmSaveDialog; 46 import com.android.fmradio.views.FmVisualizerView; 47 48 import java.io.File; 49 import java.text.SimpleDateFormat; 50 import java.util.Date; 51 import java.util.Locale; 52 53 /** 54 * This class interact with user, FM recording function. 55 */ 56 public class FmRecordActivity extends Activity implements 57 FmSaveDialog.OnRecordingDialogClickListener { 58 private static final String TAG = "FmRecordActivity"; 59 60 private static final String FM_STOP_RECORDING = "fmradio.stop.recording"; 61 private static final String FM_ENTER_RECORD_SCREEN = "fmradio.enter.record.screen"; 62 private static final String TAG_SAVE_RECORDINGD = "SaveRecording"; 63 private static final int MSG_UPDATE_NOTIFICATION = 1000; 64 private static final int TIME_BASE = 60; 65 private Context mContext; 66 private TextView mMintues; 67 private TextView mSeconds; 68 private TextView mFrequency; 69 private View mStationInfoLayout; 70 private TextView mStationName; 71 private TextView mRadioText; 72 private Button mStopRecordButton; 73 private FmVisualizerView mPlayIndicator; 74 private FmService mService = null; 75 private FragmentManager mFragmentManager; 76 private boolean mIsInBackground = false; 77 private int mRecordState = FmRecorder.STATE_INVALID; 78 private int mCurrentStation = FmUtils.DEFAULT_STATION; 79 private Notification.Builder mNotificationBuilder = null; 80 81 @Override 82 protected void onCreate(Bundle savedInstanceState) { 83 super.onCreate(savedInstanceState); 84 Log.d(TAG, "onCreate"); 85 mContext = getApplicationContext(); 86 mFragmentManager = getFragmentManager(); 87 setContentView(R.layout.fm_record_activity); 88 89 mMintues = (TextView) findViewById(R.id.minutes); 90 mSeconds = (TextView) findViewById(R.id.seconds); 91 92 mFrequency = (TextView) findViewById(R.id.frequency); 93 mStationInfoLayout = findViewById(R.id.station_name_rt); 94 mStationName = (TextView) findViewById(R.id.station_name); 95 mRadioText = (TextView) findViewById(R.id.radio_text); 96 97 mStopRecordButton = (Button) findViewById(R.id.btn_stop_record); 98 mStopRecordButton.setEnabled(false); 99 mStopRecordButton.setOnClickListener(new View.OnClickListener() { 100 @Override 101 public void onClick(View v) { 102 // Stop recording and wait service notify stop record state to show dialog 103 mService.stopRecordingAsync(); 104 } 105 }); 106 107 mPlayIndicator = (FmVisualizerView) findViewById(R.id.fm_play_indicator); 108 109 if (savedInstanceState != null) { 110 mCurrentStation = savedInstanceState.getInt(FmStation.CURRENT_STATION); 111 mRecordState = savedInstanceState.getInt("last_record_state"); 112 } else { 113 Intent intent = getIntent(); 114 mCurrentStation = intent.getIntExtra(FmStation.CURRENT_STATION, 115 FmUtils.DEFAULT_STATION); 116 mRecordState = intent.getIntExtra("last_record_state", FmRecorder.STATE_INVALID); 117 } 118 bindService(new Intent(this, FmService.class), mServiceConnection, 119 Context.BIND_AUTO_CREATE); 120 updateUi(); 121 } 122 123 private void updateUi() { 124 // TODO it's on UI thread, change to sub thread 125 ContentResolver resolver = mContext.getContentResolver(); 126 mFrequency.setText("FM " + FmUtils.formatStation(mCurrentStation)); 127 Cursor cursor = null; 128 try { 129 cursor = resolver.query( 130 Station.CONTENT_URI, 131 FmStation.COLUMNS, 132 Station.FREQUENCY + "=?", 133 new String[] { String.valueOf(mCurrentStation) }, 134 null); 135 if (cursor != null && cursor.moveToFirst()) { 136 // If the station name does not exist, show program service(PS) instead 137 String stationName = cursor.getString(cursor.getColumnIndex(Station.STATION_NAME)); 138 if (TextUtils.isEmpty(stationName)) { 139 stationName = cursor.getString(cursor.getColumnIndex(Station.PROGRAM_SERVICE)); 140 } 141 String radioText = cursor.getString(cursor.getColumnIndex(Station.RADIO_TEXT)); 142 mStationName.setText(stationName); 143 mRadioText.setText(radioText); 144 int id = cursor.getInt(cursor.getColumnIndex(Station._ID)); 145 resolver.registerContentObserver( 146 ContentUris.withAppendedId(Station.CONTENT_URI, id), false, 147 mContentObserver); 148 // If no station name and no radio text, hide the view 149 if ((!TextUtils.isEmpty(stationName)) 150 || (!TextUtils.isEmpty(radioText))) { 151 mStationInfoLayout.setVisibility(View.VISIBLE); 152 } else { 153 mStationInfoLayout.setVisibility(View.GONE); 154 } 155 Log.d(TAG, "updateUi, frequency = " + mCurrentStation + ", stationName = " 156 + stationName + ", radioText = " + radioText); 157 } 158 } finally { 159 if (cursor != null) { 160 cursor.close(); 161 } 162 } 163 } 164 165 private void updateRecordingNotification(long recordTime) { 166 if (mNotificationBuilder == null) { 167 Intent intent = new Intent(FM_STOP_RECORDING); 168 intent.setClass(mContext, FmRecordActivity.class); 169 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 170 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 171 PendingIntent.FLAG_UPDATE_CURRENT); 172 173 Bitmap largeIcon = FmUtils.createNotificationLargeIcon(mContext, 174 FmUtils.formatStation(mCurrentStation)); 175 mNotificationBuilder = new Builder(this) 176 .setContentText(getText(R.string.record_notification_message)) 177 .setShowWhen(false) 178 .setAutoCancel(true) 179 .setSmallIcon(R.drawable.ic_launcher) 180 .setLargeIcon(largeIcon) 181 .addAction(R.drawable.btn_fm_rec_stop_enabled, getText(R.string.stop_record), 182 pendingIntent); 183 184 Intent cIntent = new Intent(FM_ENTER_RECORD_SCREEN); 185 cIntent.setClass(mContext, FmRecordActivity.class); 186 cIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 187 PendingIntent contentPendingIntent = PendingIntent.getActivity(mContext, 0, cIntent, 188 PendingIntent.FLAG_UPDATE_CURRENT); 189 mNotificationBuilder.setContentIntent(contentPendingIntent); 190 } 191 // Format record time to show on title 192 Date date = new Date(recordTime); 193 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss", Locale.ENGLISH); 194 String time = simpleDateFormat.format(date); 195 196 mNotificationBuilder.setContentTitle(time); 197 if (mService != null) { 198 mService.showRecordingNotification(mNotificationBuilder.build()); 199 } 200 } 201 202 @Override 203 public void onNewIntent(Intent intent) { 204 if (intent != null && intent.getAction() != null) { 205 String action = intent.getAction(); 206 if (FM_STOP_RECORDING.equals(action)) { 207 // If click stop button in notification, need to stop recording 208 if (mService != null && !isStopRecording()) { 209 mService.stopRecordingAsync(); 210 } 211 } else if (FM_ENTER_RECORD_SCREEN.equals(action)) { 212 // Just enter record screen, do nothing 213 } 214 } 215 } 216 217 @Override 218 protected void onResume() { 219 super.onResume(); 220 mIsInBackground = false; 221 if (null != mService) { 222 mService.setFmRecordActivityForeground(true); 223 } 224 // Show save dialog if record has stopped and never show it before. 225 if (isStopRecording() && !isSaveDialogShown()) { 226 showSaveDialog(); 227 } 228 // Trigger to refreshing timer text if still in record 229 if (!isStopRecording()) { 230 mHandler.removeMessages(FmListener.MSGID_REFRESH); 231 mHandler.sendEmptyMessage(FmListener.MSGID_REFRESH); 232 } 233 // Clear notification, it only need show when in background 234 removeNotification(); 235 } 236 237 @Override 238 protected void onPause() { 239 super.onPause(); 240 mIsInBackground = true; 241 if (null != mService) { 242 mService.setFmRecordActivityForeground(false); 243 } 244 // Stop refreshing timer text 245 mHandler.removeMessages(FmListener.MSGID_REFRESH); 246 // Show notification when switch to background 247 showNotification(); 248 } 249 250 private void showNotification() { 251 // If have stopped recording, need not show notification 252 if (!isStopRecording()) { 253 mHandler.sendEmptyMessage(MSG_UPDATE_NOTIFICATION); 254 } else if (isSaveDialogShown()) { 255 // Only when save dialog is shown and FM radio is back to background, 256 // it is necessary to update playing notification. 257 // Otherwise, FmMainActivity will update playing notification. 258 mService.updatePlayingNotification(); 259 } 260 } 261 262 private void removeNotification() { 263 mHandler.removeMessages(MSG_UPDATE_NOTIFICATION); 264 if (mService != null) { 265 mService.removeNotification(); 266 mService.updatePlayingNotification(); 267 } 268 } 269 270 @Override 271 protected void onSaveInstanceState(Bundle outState) { 272 outState.putInt(FmStation.CURRENT_STATION, mCurrentStation); 273 outState.putInt("last_record_state", mRecordState); 274 super.onSaveInstanceState(outState); 275 } 276 277 @Override 278 protected void onDestroy() { 279 removeNotification(); 280 mHandler.removeCallbacksAndMessages(null); 281 if (mService != null) { 282 mService.unregisterFmRadioListener(mFmListener); 283 } 284 unbindService(mServiceConnection); 285 mContext.getContentResolver().unregisterContentObserver(mContentObserver); 286 super.onDestroy(); 287 } 288 289 /** 290 * Recording dialog click 291 * 292 * @param recordingName The new recording name 293 */ 294 @Override 295 public void onRecordingDialogClick( 296 String recordingName) { 297 // Happen when activity recreate, such as switch language 298 if (mIsInBackground) { 299 return; 300 } 301 302 if (recordingName != null && mService != null) { 303 mService.saveRecordingAsync(recordingName); 304 returnResult(recordingName, getString(R.string.toast_record_saved)); 305 } else { 306 returnResult(null, getString(R.string.toast_record_not_saved)); 307 } 308 finish(); 309 } 310 311 @Override 312 public void onBackPressed() { 313 if (mService != null & !isStopRecording()) { 314 // Stop recording and wait service notify stop record state to show dialog 315 mService.stopRecordingAsync(); 316 return; 317 } 318 super.onBackPressed(); 319 } 320 321 private final ServiceConnection mServiceConnection = new ServiceConnection() { 322 @Override 323 public void onServiceConnected(ComponentName name, android.os.IBinder service) { 324 mService = ((FmService.ServiceBinder) service).getService(); 325 mService.registerFmRadioListener(mFmListener); 326 mService.setFmRecordActivityForeground(!mIsInBackground); 327 // 1. If have stopped recording, we need check whether need show save dialog again. 328 // Because when stop recording in background, we need show it when switch to foreground. 329 if (isStopRecording()) { 330 if (!isSaveDialogShown()) { 331 showSaveDialog(); 332 } 333 return; 334 } 335 // 2. If not start recording, start it directly, this case happen when start this 336 // activity from main fm activity. 337 if (!isStartRecording()) { 338 mService.startRecordingAsync(); 339 } 340 mPlayIndicator.startAnimation(); 341 mStopRecordButton.setEnabled(true); 342 mHandler.removeMessages(FmListener.MSGID_REFRESH); 343 mHandler.sendEmptyMessage(FmListener.MSGID_REFRESH); 344 }; 345 346 @Override 347 public void onServiceDisconnected(android.content.ComponentName name) { 348 mService = null; 349 }; 350 }; 351 352 private String addPaddingForString(long time) { 353 StringBuilder builder = new StringBuilder(); 354 if (time >= 0 && time < 10) { 355 builder.append("0"); 356 } 357 return builder.append(time).toString(); 358 } 359 360 private final Handler mHandler = new Handler() { 361 @Override 362 public void handleMessage(Message msg) { 363 switch (msg.what) { 364 case FmListener.MSGID_REFRESH: 365 if (mService != null) { 366 long recordTimeInMillis = mService.getRecordTime(); 367 long recordTimeInSec = recordTimeInMillis / 1000L; 368 mMintues.setText(addPaddingForString(recordTimeInSec / TIME_BASE)); 369 mSeconds.setText(addPaddingForString(recordTimeInSec % TIME_BASE)); 370 checkStorageSpaceAndStop(); 371 } 372 mHandler.sendEmptyMessageDelayed(FmListener.MSGID_REFRESH, 1000); 373 break; 374 375 case MSG_UPDATE_NOTIFICATION: 376 if (mService != null) { 377 updateRecordingNotification(mService.getRecordTime()); 378 checkStorageSpaceAndStop(); 379 } 380 mHandler.sendEmptyMessageDelayed(MSG_UPDATE_NOTIFICATION, 1000); 381 break; 382 383 case FmListener.LISTEN_RECORDSTATE_CHANGED: 384 // State change from STATE_INVALID to STATE_RECORDING mean begin recording 385 // State change from STATE_RECORDING to STATE_IDLE mean stop recording 386 int newState = mService.getRecorderState(); 387 Log.d(TAG, "handleMessage, record state changed: newState = " + newState 388 + ", mRecordState = " + mRecordState); 389 if (mRecordState == FmRecorder.STATE_INVALID 390 && newState == FmRecorder.STATE_RECORDING) { 391 mRecordState = FmRecorder.STATE_RECORDING; 392 } else if (mRecordState == FmRecorder.STATE_RECORDING 393 && newState == FmRecorder.STATE_IDLE) { 394 mRecordState = FmRecorder.STATE_IDLE; 395 mPlayIndicator.stopAnimation(); 396 showSaveDialog(); 397 } 398 break; 399 400 case FmListener.LISTEN_RECORDERROR: 401 Bundle bundle = msg.getData(); 402 int errorType = bundle.getInt(FmListener.KEY_RECORDING_ERROR_TYPE); 403 handleRecordError(errorType); 404 break; 405 406 default: 407 break; 408 } 409 }; 410 }; 411 412 private void checkStorageSpaceAndStop() { 413 long recordTimeInMillis = mService.getRecordTime(); 414 long recordTimeInSec = recordTimeInMillis / 1000L; 415 // Check storage free space 416 String recordingSdcard = FmUtils.getDefaultStoragePath(); 417 if (!FmUtils.hasEnoughSpace(recordingSdcard)) { 418 // Need to record more than 1s. 419 // Avoid calling MediaRecorder.stop() before native record starts. 420 if (recordTimeInSec >= 1) { 421 // Insufficient storage 422 mService.stopRecordingAsync(); 423 Toast.makeText(FmRecordActivity.this, 424 R.string.toast_sdcard_insufficient_space, 425 Toast.LENGTH_SHORT).show(); 426 } 427 } 428 } 429 430 private void handleRecordError(int errorType) { 431 Log.d(TAG, "handleRecordError, errorType = " + errorType); 432 String showString = null; 433 switch (errorType) { 434 case FmRecorder.ERROR_SDCARD_NOT_PRESENT: 435 showString = getString(R.string.toast_sdcard_missing); 436 returnResult(null, showString); 437 finish(); 438 break; 439 440 case FmRecorder.ERROR_SDCARD_INSUFFICIENT_SPACE: 441 showString = getString(R.string.toast_sdcard_insufficient_space); 442 returnResult(null, showString); 443 finish(); 444 break; 445 446 case FmRecorder.ERROR_RECORDER_INTERNAL: 447 showString = getString(R.string.toast_recorder_internal_error); 448 Toast.makeText(mContext, showString, Toast.LENGTH_SHORT).show(); 449 break; 450 451 case FmRecorder.ERROR_SDCARD_WRITE_FAILED: 452 showString = getString(R.string.toast_recorder_internal_error); 453 returnResult(null, showString); 454 finish(); 455 break; 456 457 default: 458 Log.w(TAG, "handleRecordError, invalid record error"); 459 break; 460 } 461 } 462 463 private void returnResult(String recordName, String resultString) { 464 Intent intent = new Intent(); 465 intent.putExtra(FmMainActivity.EXTRA_RESULT_STRING, resultString); 466 if (recordName != null) { 467 intent.setData(Uri.parse("file://" + FmService.getRecordingSdcard() 468 + File.separator + FmRecorder.FM_RECORD_FOLDER + File.separator 469 + Uri.encode(recordName) + FmRecorder.RECORDING_FILE_EXTENSION)); 470 } 471 setResult(RESULT_OK, intent); 472 } 473 474 private final ContentObserver mContentObserver = new ContentObserver(new Handler()) { 475 public void onChange(boolean selfChange) { 476 updateUi(); 477 }; 478 }; 479 480 // Service listener 481 private final FmListener mFmListener = new FmListener() { 482 @Override 483 public void onCallBack(Bundle bundle) { 484 int flag = bundle.getInt(FmListener.CALLBACK_FLAG); 485 if (flag == FmListener.MSGID_FM_EXIT) { 486 mHandler.removeCallbacksAndMessages(null); 487 } 488 489 // remove tag message first, avoid too many same messages in queue. 490 Message msg = mHandler.obtainMessage(flag); 491 msg.setData(bundle); 492 mHandler.removeMessages(flag); 493 mHandler.sendMessage(msg); 494 } 495 }; 496 497 /** 498 * Show save record dialog 499 */ 500 public void showSaveDialog() { 501 removeNotification(); 502 if (mIsInBackground) { 503 Log.d(TAG, "showSaveDialog, activity is in background, show it later"); 504 return; 505 } 506 String sdcard = FmService.getRecordingSdcard(); 507 String recordingName = mService.getRecordingName(); 508 String saveName = null; 509 if (TextUtils.isEmpty(mStationName.getText())) { 510 saveName = FmRecorder.RECORDING_FILE_PREFIX + "_" + recordingName; 511 } else { 512 saveName = FmRecorder.RECORDING_FILE_PREFIX + "_" + mStationName.getText() + "_" 513 + recordingName; 514 } 515 FmSaveDialog newFragment = new FmSaveDialog(sdcard, recordingName, saveName); 516 newFragment.show(mFragmentManager, TAG_SAVE_RECORDINGD); 517 mFragmentManager.executePendingTransactions(); 518 mHandler.removeMessages(FmListener.MSGID_REFRESH); 519 } 520 521 private boolean isStartRecording() { 522 return mRecordState == FmRecorder.STATE_RECORDING; 523 } 524 525 private boolean isStopRecording() { 526 return mRecordState == FmRecorder.STATE_IDLE; 527 } 528 529 private boolean isSaveDialogShown() { 530 FmSaveDialog saveDialog = (FmSaveDialog) 531 mFragmentManager.findFragmentByTag(TAG_SAVE_RECORDINGD); 532 return saveDialog != null; 533 } 534 } 535