Android Open Source - SimpleMusicPlayer Player






From Project

Back to project page SimpleMusicPlayer.

License

The source code is released under:

Copyright 2011 Micha? Kazior. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: ...

If you think the Android project SimpleMusicPlayer listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.michalkazior.simplemusicplayer;
/*from  w  w  w .jav  a  2 s  .com*/
import java.util.ArrayList;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.provider.MediaStore;
import android.telephony.TelephonyManager;
import android.widget.Toast;

/**
 * Player backend.
 * 
 * It manages the playlist, handles operations, sends events to UI client(s).
 * 
 * @author kazik
 */
public class Player extends Service {
  enum Event {
    EnqueuedSongsChanged, StateChanged,
  };

  /**
   * Proxy Binder class for direct remote communication with Player class.
   */
  public class Proxy extends Binder {
    Player getPlayer() {
      return Player.this;
    }
  };

  /**
   * Possible Player states.
   */
  public enum State {
    IS_STOPPED, IS_PLAYING, IS_PAUSED,
  };

  private boolean isOnHoldByCall = false;
  private boolean isOnHoldByHeadset = false;
  private ArrayList<Song> enqueuedSongs = new ArrayList<Song>();
  private MediaPlayer mp = null;
  private State state = State.IS_STOPPED;
  private Song playing = null;
  private ArrayList<Messenger> clients = new ArrayList<Messenger>();

  @Override
  public IBinder onBind(Intent intent) {
    return new Proxy();
  }

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    return START_STICKY;
  }

  public synchronized void registerHandler(Messenger m) {
    clients.add(m);
  }

  public synchronized Song getPlaying() {
    return playing;
  }

  public synchronized void setPlaying(Song song) {
    playing = song;
  }

  public synchronized State getState() {
    return state;
  }

  private synchronized void setState(State s) {
    state = s;
    emit(Event.StateChanged);
  }

  /**
   * Get current song duration in msecs.
   */
  public synchronized int getDuration() {
    switch (state) {
      case IS_PLAYING:
      case IS_PAUSED:
        return mp.getDuration();
      case IS_STOPPED:
        return 0;
    }
    return 0;
  }

  /**
   * Get current song position in msecs.
   */
  public synchronized int getPosition() {
    switch (state) {
      case IS_PLAYING:
      case IS_PAUSED:
        return mp.getCurrentPosition();
      case IS_STOPPED:
        return 0;
    }
    return 0;
  }

  /**
   * Get a list for currently enqueued songs.
   * 
   * @return
   */
  public synchronized Song[] getEnqueuedSongs() {
    return enqueuedSongs.toArray(new Song[] {});
  }

  /**
   * Get a list of all available songs.
   * 
   * 
   * The function returns a list of songs stored in the media database from an
   * external storage (i.e. memory card).
   */
  public synchronized Song[] getAllSongs() {
    /*
     * Doing a query() would result in a fatal error when external storage
     * is missing.
     * 
     * So instead, return an empty list when external storage isn't present.
     */
    if (!isExternalStorageMounted()) {
      Toast.makeText(this, R.string.msg_err_notmounted, Toast.LENGTH_LONG).show();
      return new Song[] {};
    }

    /*
     * Fixme: Should a Song have more info?
     */
    ArrayList<Song> list = new ArrayList<Song>();
    Uri uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
    String[] columns = { MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.DISPLAY_NAME };
    Cursor c = getContentResolver().query(uri, columns, null, null,
        MediaStore.Audio.Media.DATE_ADDED + " DESC");

    int dataIndex = c.getColumnIndex(MediaStore.Audio.Media.DATA);
    int displayIndex = c.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME);

    if (c.moveToFirst()) {
      do {
        list.add(new Song(c.getString(dataIndex), c.getString(displayIndex)));
      } while (c.moveToNext());
    }
    c.close();

    return list.toArray(new Song[] {});
  }

  /**
   * Enqueue a song at a given index.
   * 
   * @param song
   * @param index
   *            value less than 0 appends
   */
  public synchronized void enqueueSong(Song song, int index) {
    if (index >= 0) {
      if (index > enqueuedSongs.size()) index = 0;
      enqueuedSongs.add(index, song);
    }
    else {
      enqueuedSongs.add(song);
    }

    emit(Event.EnqueuedSongsChanged);
  }

  /**
   * Move a song by an offset.
   * 
   * Moving an only song yields no effect.
   * 
   * Moving a song beyond enqueued songs list is done by cutting offset to
   * list size accordingly.
   * 
   * @param song
   * @param offset
   */
  public synchronized void moveSong(Song song, int offset) {
    int index = enqueuedSongs.indexOf(song) + offset;

    if (index < 0) index = 0;
    if (index >= enqueuedSongs.size()) index = enqueuedSongs.size() - 1;

    enqueuedSongs.remove(song);
    enqueuedSongs.add(index, song);
    emit(Event.EnqueuedSongsChanged);
  }

  /**
   * Remove a song.
   * 
   * Removing a now playing song (i.e. the first) restarts playback with a new
   * song at index 0.
   * 
   * Removing a non-existing song may send Reply.enqueuedSongs
   * 
   * @param song
   */
  public synchronized void removeSong(Song song) {
    if (playing == song) {
      playNext();
    }
    else {
      enqueuedSongs.remove(song);
      emit(Event.EnqueuedSongsChanged);
    }
  }

  /**
   * Make sure the playback is on.
   * 
   * This call is valid in any state.
   */
  public synchronized void play() {
    switch (state) {
      case IS_STOPPED:
        validate();
        if (playing != null) {
          try {
            mp = new MediaPlayer();
            mp.setOnCompletionListener(new OnCompletionListener() {
              @Override
              public void onCompletion(MediaPlayer mp) {
                playNext();
              }
            });
            mp.setOnErrorListener(new OnErrorListener() {
              @Override
              public boolean onError(MediaPlayer mp, int what, int extra) {
                Toast.makeText(getApplicationContext(), R.string.msg_mp_error,
                    Toast.LENGTH_LONG).show();
                mp.reset();
                mp.release();
                setState(State.IS_STOPPED);
                return false;
              }
            });
            mp.setDataSource(playing.getPath());
            mp.prepare();
            mp.start();
            setState(State.IS_PLAYING);
          }
          catch (Exception e) {
            Toast.makeText(
                this,
                String.format(getText(R.string.msg_mp_error_info).toString(),
                    e.getMessage()), Toast.LENGTH_LONG).show();
            mp.reset();
            mp.release();
            setState(State.IS_STOPPED);
          }
        }
        break;

      case IS_PLAYING:
        /* ignore */
        break;

      case IS_PAUSED:
        mp.start();
        setState(State.IS_PLAYING);
        break;
    }
  }

  /**
   * Remove the currently playing song and play the next one.
   * 
   * This call is valid in any state.
   */
  public synchronized void playNext() {
    if (playing != null) {
      State oldstate = state;
      int idx = enqueuedSongs.indexOf(playing);
      enqueuedSongs.remove(playing);
      emit(Event.EnqueuedSongsChanged);
      reset();

      /*
       * Idx now point to the next song (since the previous nowPlaying has
       * been removed thus shifting array items).
       */
      if (idx < enqueuedSongs.size()) {
        playing = enqueuedSongs.get(idx);
      }
      if (oldstate == State.IS_PLAYING)
        play();
    }
  }

  /**
   * Stop playback.
   * 
   * Yields effect only when IS_PLAYING.
   */
  public synchronized void stop() {
    switch (state) {
      case IS_PLAYING:
        mp.pause();
        setState(State.IS_PAUSED);
        break;
    }
  }

  /**
   * Seek to a position.
   * 
   * Yields effect always but when IS_STOPPED.
   * 
   * @param position
   */
  public synchronized void seek(int position) {
    switch (state) {
      case IS_PLAYING:
      case IS_PAUSED:
        mp.seekTo(position);
        break;
    }
  }

  /**
   * Reset player state.
   * 
   * This call changes the state to IS_STOPPED.
   * 
   * Yields effect always but when IS_STOPPED.
   */
  public synchronized void reset() {
    switch (state) {
      case IS_PLAYING:
        mp.stop();
      case IS_PAUSED:
        mp.reset();
        mp.release();
        mp = null;
        playing = null;
        isOnHoldByCall = false;
        isOnHoldByHeadset = false;
        setState(State.IS_STOPPED);
        break;
    }
  }

  /**
   * Validate if nowPlaying is at index 0.
   * 
   * This is a helper function.
   */
  private synchronized void validate() {
    if (enqueuedSongs.size() == 0) {
      reset();
    }
    else {
      if (playing == null) {
        playing = enqueuedSongs.get(0);
      }
    }
  }

  /**
   * Try playback.
   * 
   * Will playback on if all isOnHold* variables are false. 
   */
  public synchronized void play_try() {
    if (!isOnHoldByCall && !isOnHoldByHeadset)
      play();
  }

  @Override
  public void onCreate() {
    super.onCreate();

    Notification n = new Notification(
        R.drawable.icon,
        getText(R.string.msg_service_started),
        System.currentTimeMillis());
    PendingIntent i = PendingIntent.getActivity(this, 0, new Intent(this, SongQueue.class)
        .setAction("android.intent.action.MAIN")
        .addCategory("android.intent.category.LAUNCHER"), 0);
    n.setLatestEventInfo(this, getText(R.string.app_name), "", i);
    startForeground(this.hashCode(), n);

    /*
     * Handle incomming and outcomming calls.
     * 
     * When the playback is on and there's an incomming call, or an outgoing
     * is being dialed we want to hold the playback. Finishing the call will
     * resume the playback.
     */
    registerReceiver(new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
        String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
        if (state.compareTo(TelephonyManager.EXTRA_STATE_IDLE) == 0) {
          isOnHoldByCall = false;
          play_try();
        }
        else {
          isOnHoldByCall = true;
          stop();
        }
      }
    }, new IntentFilter(TelephonyManager.ACTION_PHONE_STATE_CHANGED));

    /*
     * Handle (un)plugging a headset
     * 
     * When the playback is on and a connected headset is unplugged we want
     * to hold the playback. Plugging back a headset back will resume the
     * playback.
     * 
     * The AudioManager.ACTION_AUDIO_BECOMING_NOISY is for unplugging only
     * and reacts instantenously.
     * 
     * The Intent.ACTION_HEADSET_PLUG has a hardcoded polling time (in the
     * Android framework) and would lag for about 1s before stopping. The
     * lag is acceptable when plugging in though.
     */
    registerReceiver(new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
        switch (intent.getIntExtra("state", -1)) {
          case 0: /* unplugged */
            isOnHoldByHeadset = true;
            stop();
            break;
          case 1: /* plugged */
            isOnHoldByHeadset = false;
            play_try();
            break;
        }
      }
    }, new IntentFilter(Intent.ACTION_HEADSET_PLUG));

    registerReceiver(new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
        isOnHoldByHeadset = true;
        stop();
      }
    }, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));

    /*
     * Handle external storage removal
     */
    registerReceiver(new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
        stop();
        reset();
        enqueuedSongs.clear();
        Toast.makeText(getApplicationContext(), R.string.msg_err_ejected, Toast.LENGTH_LONG)
            .show();
      }
    }, new IntentFilter(Intent.ACTION_MEDIA_EJECT));

    Toast.makeText(this, R.string.msg_service_started, Toast.LENGTH_SHORT).show();
  }

  @Override
  public void onDestroy() {
    stopForeground(true);
    Toast.makeText(this, R.string.msg_service_stopped, Toast.LENGTH_LONG).show();
    reset();
    super.onDestroy();
  }

  /**
   * Emit a signal that is propagated to listening clients.
   * 
   * @param e
   */
  private void emit(Event e) {
    for (Messenger m : clients) {
      try {
        m.send(Message.obtain(null, e.ordinal()));
      }
      catch (RemoteException exception) {
        /* The client must've died */
        clients.remove(m);
      }
    }
  }

  /**
   * Check whether external storage is mounted or not.
   */
  public static boolean isExternalStorageMounted() {
    return android.os.Environment.getExternalStorageState().compareTo(
        android.os.Environment.MEDIA_MOUNTED) == 0;
  }
}




Java Source Code List

com.michalkazior.simplemusicplayer.Player.java
com.michalkazior.simplemusicplayer.SongAdapter.java
com.michalkazior.simplemusicplayer.SongList.java
com.michalkazior.simplemusicplayer.SongQueue.java
com.michalkazior.simplemusicplayer.Song.java