Media Playtrack trong Android (phần 2)

Kết thúc phần trước, chúng ta đã tìm hiểu đến phần wakelock trong media, hôm nay chúng ta sẽ tiếp tục các đặc tính khác trong media playtrack.

  1. Chạy như một service nền

    Service thường được dùng cho các công việc chạy trong background, chẳng hạn như lấy email, đồng bộ hóa dữ liệu... Trong cac trường hợp này, user không thể nhận ra sự hoạt động của service, và có lẽ thậm chí không nhận thấy nếu một số service này đã bị dán đoạn và sau đó khởi động lại.

    Nhưng xem xét trường hợp của một dịch vụ đang chơi nhạc. Rõ ràng đây là một service mà người nhận biết được và UX sẽ bị ảnh hưởng nghiêm trọng bởi bất kỳ sự gián đoạn nào. Ngoài ra, nó là service mà user có thể sẽ muốn tương tác trong thời gian thực hiện của nó.Trong trường hợp này, các service cần phải chạy như một "forceground service". Một forceground service giữ một vai trò cực kỳ quan trọng trong hệ thống, hệ thống hầu như sẽ không kill service, vì nó quan trọng trực tiếp với người dùng. Khi chạy trong forceground, service cũng phải cung cấp một status bar notification để đảm bảo rằng người dùng nhận biết được rằng service vẫn đang chạy và cho phép họ mở một activity có thể tương tác với service.

    Để bật service của bạn trong forceground service, bạn phải tạo một Notification cho statusbar và gọi startForceground() từ Service. Ví dụ:

    String songName;
    // assign the song name to songName
    PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0,
                    new Intent(getApplicationContext(), MainActivity.class),
                    PendingIntent.FLAG_UPDATE_CURRENT);
    Notification notification = new Notification();
    notification.tickerText = text;
    notification.icon = R.drawable.play0;
    notification.flags |= Notification.FLAG_ONGOING_EVENT;
    notification.setLatestEventInfo(getApplicationContext(), "MusicPlayerSample",
                    "Playing: " + songName, pi);
    startForeground(NOTIFICATION_ID, notification);
Trong khi service của bạn đang chạy trong forceground, notification đã được bạn config có thể nhìn thấy trên vùng notification của thiêt bị. Nếu user chọn vào notification, hệ thống sẽ gọi PendingIntent bọn đã cung cấp. Ở ví dụ trên, nó mở một activity (MainActivity).

Bạn chỉ nên giữ trạng thái "forceground service" khi service của bạn thực hiện một tiến trình mà người dùng cần nhận thức được. Một khi điều đó không còn đúng nữa, bạn nên giải phong nó bằng cách gọi stopForceground():
    stopForceground(true);
  1. Xử lý âm thanh tập trung

    Mặc dù chỉ có một activity có thể chạy ở bất kỳ thời điêm nào, Android vẫn là một môi trường đa nhiệm. Điều này đặt ra một thử thách đặc biệt khi sử dụng audio, vì chỉ có một đầu ra audio và có thể có vài media service cạnh tranh để sử dụng nó. Trước Android 2.2, không có một cơ chế nào được xây dựng để giải quyết vấn đề này, mà trong một số trường hợp có thể gây trải nghiệm xấu với người dùng. Ví dụ, khi người dùng đang nghe nhạc và một ứng dụng khác cần thông báo đến người dùng một thông tin quan trọng, người dùng có thể không nghe thấy tiếng của notification do nhạc quá to. Bắt đầu từ Android 2.2, nền tảng đưa ra một cách cho ứng dụng để đàm phán đầu ra âm thanh của thiết bị. Cơ chế này gọi là Audio Focus.

    Khi ứng dụng của bạn cần có đầu ra là âm thanh hoặc notification, bạn luôn nên request audio focus. Một khi có focus, nó có thể sử dụng đầu ra âm thanh một cách tự do, nhưng nó luôn nên lắng nghe để thay đổi trọng âm. Nếu được thông báo rằng nó mất audio focus, nó nên lập tức kill audio hoặc giảm âm lượng đến một mức độ yên tĩnh (được biết đến như "ducking"- có một flag để chỉ ra rằng nó thích hợp) và chỉ tiếp tục phát lại âm thanh sau khi nhận được focus lần nữa.

    Audio Focus là một sự hợp tác tự nhiên. Đó là, ứng dụng được hy vọng (và rất khuyển khích) để tuân theo với hướng dẫn của audio focus, nhưng các nguyên tắc không bị bắt buộc bởi hệ thống. Nếu một ứng dụng muốn chơi nhạc to kể cả sau khi mất audio focus, không gì trong hệ thống chống lại điều đó. Tuy nhiên, người dùng có nhiều khả năng sẽ có trải nghiệm xấu và sẽ gỡ bỏ các ứng dụng gây phiền phức.

    Để request audio focus, bạn phải gọi hàm requestAudioFocus() từ AudioManager, giống như ví dụ dưới đây:

    AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
        AudioManager.AUDIOFOCUS_GAIN);

    if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
        // could not get audio focus.
    }
Tham số đầu tiên để requestAudioFocus là một AudioManager.OnAudioFocusChangeListener, phương thức onAudioFocusChange() được gọi bất cứ khi nào có sự thay đổi ở audio focus. Theo đó, bạn cũng nên implement interface này trong service và activity của bạn. Ví dụ:
    class MyService extends Service
                implements AudioManager.OnAudioFocusChangeListener {
        // ....
        public void onAudioFocusChange(int focusChange) {
            // Do something based on focus change...
        }
    }
Tham số focusChange nói cho bạn cách mà audio focus thay đổi, và có thể là một trong các giá trị dưới (tất cả chúng được định nghĩa trong AudioManager):

AUDIOFOCUS_GAIN : bạn đã hoàn thành audio focus.

AUDIOFOCUS_LOSS : bạn đã mất audio focus trong một thời gian dài. Bạn phải dừng tất cả âm thanh playback. Vì bạn nên hy vọng không phải focus trở lại trong thời gian dài, nó sẽ là nơi tốt để ngốn tài nguyên của bạn càng nhiều càng tốt. Ví dụ, bạn nên release MediaPlayer.

AUDIOFOCUS_LOSS_TRANSIENT : bạn đã tạm thời mất audio focus, nhưng sẽ nhận lại trong thời gian ngắn. Bạn phải dừng tất cả audio playback, nhưng bạn có thể giữ lại tài nguyên vì bạn sẽ chuẩn bị lấy focus trở lại trong thời gian ngắn.

AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK : bạn đã tạm thời mất audio focus, nhưng bạn đã cho phép tiếp tục chơi audio một cách im lặng (ở âm lượng thấp) thay vì kill audio.

Đây là một ví dụ:
    public void onAudioFocusChange(int focusChange) {
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_GAIN:
                // resume playback
                if (mMediaPlayer == null) initMediaPlayer();
                else if (!mMediaPlayer.isPlaying()) mMediaPlayer.start();
                mMediaPlayer.setVolume(1.0f, 1.0f);
                break;

            case AudioManager.AUDIOFOCUS_LOSS:
                // Lost focus for an unbounded amount of time: stop playback and release media player
                if (mMediaPlayer.isPlaying()) mMediaPlayer.stop();
                mMediaPlayer.release();
                mMediaPlayer = null;
                break;

            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                // Lost focus for a short time, but we have to stop
                // playback. We don't release the media player because playback
                // is likely to resume
                if (mMediaPlayer.isPlaying()) mMediaPlayer.pause();
                break;

            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                // Lost focus for a short time, but it's ok to keep playing
                // at an attenuated level
                if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(0.1f, 0.1f);
                break;
        }
    }
Luôn nhớ răng audio focus APIs chỉ có thể sử dụng từ API level 8 trở lên , vì thế nếu bạn muốn hỗ trợ các version trước của Android, bạn nên áp dụng một chiến lược tương thích ngược, cho phép bạn sử dụng tính năng này nếu có,  và rơi trở lại nếu không.

Bạn có thể sử dụng tương thích ngược cả bởi gọi phương thức phản chiếu audio focus hoặc implement tất cả đặc tính của audio focus trong các class riêng biệt (AudioFocusHelper). Đây là một ví dụ:
    public class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener {
        AudioManager mAudioManager;

        // other fields here, you'll probably hold a reference to an interface
        // that you can use to communicate the focus changes to your Service

        public AudioFocusHelper(Context ctx, /* other arguments here */) {
            mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
            // ...
        }

        public boolean requestFocus() {
            return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
                mAudioManager.requestAudioFocus(mContext, AudioManager.STREAM_MUSIC,
                AudioManager.AUDIOFOCUS_GAIN);
        }

        public boolean abandonFocus() {
            return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
                mAudioManager.abandonAudioFocus(this);
        }

        @Override
        public void onAudioFocusChange(int focusChange) {
            // let your service know about the focus change
        }
    }
Bạn có thể tạo một bản sao của AudioFocusHelper chỉ khi bạn xác định được rằng hệ thống chạy API 8 trở lên. Ví dụ:
    if (android.os.Build.VERSION.SDK_INT >= 8) {
        mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
    } else {
        mAudioFocusHelper = null;
    }
  1. Performing cleanup

    Như đã đề cập trước đây, một đối tượng MediaPlayer có thể tiêu tốn đáng kể tài nguyên của hệ thống, vì thế bạn nên giữ nó trong thời gian bạn cần và gọi release() khi bạn đã thực hiện xong với nó. Điều quan trọng để gọi phương thức cleanup rõ ràng tin cậy hơn thu gom rác thải trong hệ thống vì nó có thể gọi vài lần trước khi thu gom MediaPlayer, vì nó chỉ nhạy cảm với nhu cầu bộ nhớ và không để tình trạng thiếu nguồn phương tiện truyền thông khác liên quan đến. Vì thế, trong trường hợp bạn sử dụng service, bạn luôn nên override onDestroy() để chắc rằng bạn sẽ giải phóng MediaPlayer:

    public class MyService extends Service {
       MediaPlayer mMediaPlayer;
       // ...

       @Override
       public void onDestroy() {
           if (mMediaPlayer != null) mMediaPlayer.release();
       }
    }
Bạn luôn nên tìm cơ hội khác để giải phóng MediaPlayer tốt hơn. Ví dụ, nếu bạn mong đợi không phải chơi lại media trong một khoảng thời gian dài (ví dụ sau khi mất audio focus), bạn nên xác định giải phóng sự tồn tại của MediaPlayer và tạo lại nó sau đó. Mặt khác, nếu bạn chỉ mong đợi để dừng phát lại trong một thời gian rất ngắn, bạn nên có thể giữ cho Media Player của bạn để tránh việc tạo ra và chuẩn bị nó một lần nữa.
  1. Xử lý AUDIO_BECOMING_NOISY Intent

    Rất nhiều ứng dụng chơi nhạc tốt tự động dừng playback khi một sự kiện sảy ra gây ra âm thanh trở nên ồn ào (đầu ra thông qua loa ngoài). Ví dụ, có thể xảy ra khi một người đang nghe nhạc bằng tai nghe và bất ngờ mất kết nối với tai nghe từ thiết bị. Mặc dù, hành vi này không xảy ra tự động. Nếu bạn không implement đặc tính này, audio sẽ phát ở loa ngoài, đó có thể không phải điều user muốn.

    Bạn có thể chắc rằng ứng dụng sẽ dừng phát nhạc trong các trường hợp này bởi xử lý ACTION_AUDIO_BECOMING_NOISY intent, mà bạn có thể đăng ký một receiver trong manifest:

    <receiver android:name=".MusicIntentReceiver">
       <intent-filter>
          <action android:name="android.media.AUDIO_BECOMING_NOISY" />
       </intent-filter>
    </receiver>
Và implement nó trong class:
    public class MusicIntentReceiver extends android.content.BroadcastReceiver {
       @Override
       public void onReceive(Context ctx, Intent intent) {
          if (intent.getAction().equals(
                        android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
              // signal your service to stop playback
              // (via an Intent, for instance)
          }
       }
    }
  1. Lấy Media từ một Content Resvoler

    Một đặc tính kahcs có thể hữu ích trong ứng dụng media player là khả năng lấy nhạc người dùng có trong thiết bị. Bạn có thể làm điều đó bằng cách truy vấn ContentResoler trong media ngoài:

    ContentResolver contentResolver = getContentResolver();
    Uri uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
    Cursor cursor = contentResolver.query(uri, null, null, null, null);
    if (cursor == null) {
        // query failed, handle error.
    } else if (!cursor.moveToFirst()) {
        // no media on the device
    } else {
        int titleColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media.TITLE);
        int idColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media._ID);
        do {
           long thisId = cursor.getLong(idColumn);
           String thisTitle = cursor.getString(titleColumn);
           // ...process entry...
        } while (cursor.moveToNext());
    }
Để dùng nó với MediaPlayer, bạn có thể làm như sau:
    long id = /* retrieve it from somewhere */;
    Uri contentUri = ContentUris.withAppendedId(
            android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);

    mMediaPlayer = new MediaPlayer();
    mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    mMediaPlayer.setDataSource(getApplicationContext(), contentUri);
  1. Tổng kết

    Đây là những kiến thức về MediaPlayer, mong rằng chúng sẽ giúp ích cho các bạn. Trong khi làm dự án hay làm bài tập về ứng dụng Android, nhiều bạn sẽ chỉ tìm cách để cho ứng dụng của mình hoạt động như ý muốn, với những bài viết lý thuyết như thế này, mình mong sẽ giúp các bạn hiểu sâu về những thứ mình làm, điều đó sẽ rất có ích. Hẹn gặp lại các bạn trong các bài viết sau.