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
17package com.android.fmradio;
18
19import android.app.Activity;
20import android.app.FragmentManager;
21import android.app.Notification;
22import android.app.Notification.Builder;
23import android.app.PendingIntent;
24import android.content.ComponentName;
25import android.content.ContentResolver;
26import android.content.ContentUris;
27import android.content.Context;
28import android.content.Intent;
29import android.content.ServiceConnection;
30import android.database.ContentObserver;
31import android.database.Cursor;
32import android.graphics.Bitmap;
33import android.net.Uri;
34import android.os.Bundle;
35import android.os.Handler;
36import android.os.Message;
37import android.text.TextUtils;
38import android.util.Log;
39import android.view.View;
40import android.widget.Button;
41import android.widget.TextView;
42import android.widget.Toast;
43
44import com.android.fmradio.FmStation.Station;
45import com.android.fmradio.dialogs.FmSaveDialog;
46import com.android.fmradio.views.FmVisualizerView;
47
48import java.io.File;
49import java.text.SimpleDateFormat;
50import java.util.Date;
51import java.util.Locale;
52
53/**
54 * This class interact with user, FM recording function.
55 */
56public 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