Android Open Source - npr-android-app Playback Service






From Project

Back to project page npr-android-app.

License

The source code is released under:

Apache License

If you think the Android project npr-android-app 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

// Copyright 2009 Google Inc.
////  ww  w  .  j  a  va 2s .  c  om
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.npr.android.news;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnBufferingUpdateListener;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnInfoListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;

import org.npr.android.util.M3uParser;
import org.npr.android.util.PlaylistEntry;
import org.npr.android.util.PlaylistParser;
import org.npr.android.util.PlaylistProvider;
import org.npr.android.util.PlsParser;
import org.npr.android.util.PlaylistProvider.Items;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;

public class PlaybackService extends Service implements OnPreparedListener,
    OnBufferingUpdateListener, OnCompletionListener, OnErrorListener,
    OnInfoListener {

  private static final String LOG_TAG = PlaybackService.class.getName();

  private static final String SERVICE_PREFIX = "org.npr.android.news.";
  public static final String SERVICE_CHANGE_NAME = SERVICE_PREFIX + "CHANGE";
  public static final String SERVICE_CLOSE_NAME = SERVICE_PREFIX + "CLOSE";
  public static final String SERVICE_UPDATE_NAME = SERVICE_PREFIX + "UPDATE";
  
  public static final String EXTRA_TITLE = "title";
  public static final String EXTRA_DOWNLOADED = "downloaded";
  public static final String EXTRA_DURATION = "duration";
  public static final String EXTRA_POSITION = "position";

  private MediaPlayer mediaPlayer;
  private boolean isPrepared = false;

  private StreamProxy proxy;
  private NotificationManager notificationManager;
  private static final int NOTIFICATION_ID = 1;
  private int bindCount = 0;
  private PlaylistEntry current = null;
  private List<String> playlistUrls;

  private TelephonyManager telephonyManager;
  private PhoneStateListener listener;
  private boolean isPausedInCall = false;
  private Intent lastChangeBroadcast;
  private Intent lastUpdateBroadcast;
  private int lastBufferPercent = 0;
  private Thread updateProgressThread;

  // Amount of time to rewind playback when resuming after call 
  private final static int RESUME_REWIND_TIME = 3000;

  @Override
  public void onCreate() {
    mediaPlayer = new MediaPlayer();
    mediaPlayer.setOnBufferingUpdateListener(this);
    mediaPlayer.setOnCompletionListener(this);
    mediaPlayer.setOnErrorListener(this);
    mediaPlayer.setOnInfoListener(this);
    mediaPlayer.setOnPreparedListener(this);
    notificationManager = (NotificationManager) getSystemService(
        Context.NOTIFICATION_SERVICE);
    Log.w(LOG_TAG, "Playback service created");

    telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
    // Create a PhoneStateListener to watch for offhook and idle events
    listener = new PhoneStateListener() {
      @Override
      public void onCallStateChanged(int state, String incomingNumber) {
        switch (state) {
        case TelephonyManager.CALL_STATE_OFFHOOK:
        case TelephonyManager.CALL_STATE_RINGING:
          // Phone going offhook or ringing, pause the player.
          if (isPlaying()) {
            pause();
            isPausedInCall = true;
          }
          break;
        case TelephonyManager.CALL_STATE_IDLE:
          // Phone idle. Rewind a couple of seconds and start playing.
          if (isPausedInCall) {
            seekTo(Math.max(0, getPosition() - RESUME_REWIND_TIME));
            play();
          }
          break;
        }
      }
    };

    // Register the listener with the telephony manager.
    telephonyManager.listen(listener, PhoneStateListener.LISTEN_CALL_STATE);
  }

  @Override
  public IBinder onBind(Intent arg0) {
    bindCount++;
    Log.d(LOG_TAG, "Bound PlaybackService, count " + bindCount);
    return new ListenBinder();
  }

  @Override
  public boolean onUnbind(Intent arg0) {
    bindCount--;
    Log.d(LOG_TAG, "Unbinding PlaybackService, count " + bindCount);
    if (!isPlaying() && bindCount == 0) {
      Log.w(LOG_TAG, "Will stop self");
      stopSelf();
    } else {
      Log.d(LOG_TAG, "Will not stop self");
    }
    return false;
  }

  synchronized public boolean isPlaying() {
    if (isPrepared) {
      return mediaPlayer.isPlaying();
    }
    return false;
  }

  public PlaylistEntry getCurrentEntry() {
    PlaylistEntry c = current;
    return c;
  }

  public void setCurrent(PlaylistEntry c) {
    current = c;
  }

  synchronized public int getPosition() {
    if (isPrepared) {
      return mediaPlayer.getCurrentPosition();
    }
    return 0;
  }

  synchronized public int getDuration() {
    if (isPrepared) {
      return mediaPlayer.getDuration();
    }
    return 0;
  }

  synchronized public int getCurrentPosition() {
    if (isPrepared) {
      return mediaPlayer.getCurrentPosition();
    }
    return 0;
  }

  synchronized public void seekTo(int pos) {
    if (isPrepared) {
      mediaPlayer.seekTo(pos);
    }
  }

  synchronized public void play() {
    if (!isPrepared || current == null) {
      Log.e(LOG_TAG, "play - not prepared");
      return;
    }
    Log.d(LOG_TAG, "play " + current.id);
    mediaPlayer.start();
    markAsRead(current.id);

    int icon = R.drawable.stat_notify_musicplayer;
    CharSequence contentText = current.title;
    long when = System.currentTimeMillis();
    Notification notification = new Notification(icon, contentText, when);
    notification.flags = Notification.FLAG_NO_CLEAR
        | Notification.FLAG_ONGOING_EVENT;
    Context c = getApplicationContext();
    CharSequence title = getString(R.string.app_name);
    Intent notificationIntent;
    if (current.storyID != null) {
      notificationIntent = new Intent(this, NewsStoryActivity.class);
      notificationIntent.putExtra(Constants.EXTRA_STORY_ID, current.storyID);
      notificationIntent.putExtra(Constants.EXTRA_DESCRIPTION,
          R.string.msg_main_subactivity_nowplaying);
    } else {
      notificationIntent = new Intent(this, Main.class);
    }
    notificationIntent.setAction(Intent.ACTION_VIEW);
    notificationIntent.addCategory(Intent.CATEGORY_DEFAULT);
    notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    PendingIntent contentIntent = PendingIntent.getActivity(c, 0,
        notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT);
    notification.setLatestEventInfo(c, title, contentText, contentIntent);
    notificationManager.notify(NOTIFICATION_ID, notification);

    // Change broadcasts are sticky, so when a new receiver connects, it will
    // have the data without polling.
    if (lastChangeBroadcast != null) {
      getApplicationContext().removeStickyBroadcast(lastChangeBroadcast);
    }
    lastChangeBroadcast = new Intent(SERVICE_CHANGE_NAME);
    lastChangeBroadcast.putExtra(EXTRA_TITLE, current.title);
    getApplicationContext().sendStickyBroadcast(lastChangeBroadcast);
  }

  synchronized public void pause() {
    Log.d(LOG_TAG, "pause");
    if (isPrepared) {
      mediaPlayer.pause();
    }
    notificationManager.cancel(NOTIFICATION_ID);
  }

  synchronized public void stop() {
    Log.d(LOG_TAG, "stop");
    if (isPrepared) {
      if (proxy != null) {
        proxy.stop();
        proxy = null;
      }
      mediaPlayer.stop();
      isPrepared = false;
    }
    cleanup();
  }

  
  /**
   * Start listening to the given URL.
   */
  public void listen(String url, boolean stream)
      throws IllegalArgumentException, IllegalStateException, IOException {
    // First, clean up any existing audio.
    if (isPlaying()) {
      stop();
    }

    if (isPlaylist(url)) {
      downloadPlaylist();
      if (playlistUrls.size() > 0) {
        url = playlistUrls.remove(0);
      } else {
        return;
      }
    }

    Log.d(LOG_TAG, "listening to " + url + " stream=" + stream);
    String playUrl = url;
    // From 2.2 on (SDK ver 8), the local mediaplayer can handle Shoutcast
    // streams natively. Let's detect that, and not proxy.
    Log.d(LOG_TAG, "SDK Version " + Build.VERSION.SDK);
    int sdkVersion = 0;
    try {
      sdkVersion = Integer.parseInt(Build.VERSION.SDK);
    } catch (NumberFormatException e) {
    }

    if (stream && sdkVersion < 8) {
      if (proxy == null) {
        proxy = new StreamProxy();
        proxy.init();
        proxy.start();
      }
      String proxyUrl = String.format("http://127.0.0.1:%d/%s",
          proxy.getPort(), url);
      playUrl = proxyUrl;
    }

    synchronized (this) {
      Log.d(LOG_TAG, "reset: " + playUrl);
      mediaPlayer.reset();
      mediaPlayer.setDataSource(playUrl);
      mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
      Log.d(LOG_TAG, "Preparing: " + playUrl);
      mediaPlayer.prepareAsync();
      Log.d(LOG_TAG, "Waiting for prepare");
    }
  }

  @Override
  public void onPrepared(MediaPlayer mp) {
    Log.d(LOG_TAG, "Prepared");
    synchronized (this) {
      if (mediaPlayer != null) {
        isPrepared = true;
      }
    }
    play();
    if (onPreparedListener != null) {
      onPreparedListener.onPrepared(mp);
    }

    updateProgressThread = new Thread(new Runnable() {
      public void run() {
        // Initially, don't send any updates, since it takes a while for the
        // media player to settle down. 
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
          return;
        }
        while (true) {
          updateProgress();
          try {
            Thread.sleep(500);
          } catch (InterruptedException e) {
            break;
          }
        }
      }
    });
    updateProgressThread.start();
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
    Log.w(LOG_TAG, "Service exiting");

    if (updateProgressThread != null) {
      updateProgressThread.interrupt();
      try {
        updateProgressThread.join(3000);
      } catch (InterruptedException e) {
        Log.e(LOG_TAG, "", e);
      }
    }

    stop();
    synchronized (this) {
      if (mediaPlayer != null) {
        mediaPlayer.release();
        mediaPlayer = null;
      }
    }

    telephonyManager.listen(listener, PhoneStateListener.LISTEN_NONE);
  }

  public class ListenBinder extends Binder {

    public PlaybackService getService() {
      return PlaybackService.this;
    }
  }

  @Override
  public void onBufferingUpdate(MediaPlayer mp, int progress) {
    if (isPrepared) {
      lastBufferPercent = progress;
      updateProgress();
    }
  }

  /**
   * Sends an UPDATE broadcast with the latest info.
   */
  private void updateProgress() {
    if (isPrepared && mediaPlayer != null && mediaPlayer.isPlaying()) {
      // Update broadcasts are sticky, so when a new receiver connects, it will
      // have the data without polling.
      if (lastUpdateBroadcast != null) {
        getApplicationContext().removeStickyBroadcast(lastUpdateBroadcast);
      }
      lastUpdateBroadcast = new Intent(SERVICE_UPDATE_NAME);
      int position = mediaPlayer.getCurrentPosition();
      int duration = mediaPlayer.getDuration();
      lastUpdateBroadcast.putExtra(EXTRA_DURATION, mediaPlayer.getDuration());
      lastUpdateBroadcast.putExtra(EXTRA_DOWNLOADED,
          (int) ((lastBufferPercent / 100.0) * mediaPlayer.getDuration()));
      lastUpdateBroadcast.putExtra(EXTRA_POSITION,
          mediaPlayer.getCurrentPosition());
      getApplicationContext().sendStickyBroadcast(lastUpdateBroadcast);
    }
  }
  
  @Override
  public void onCompletion(MediaPlayer mp) {
    Log.w(LOG_TAG, "onComplete()");

    synchronized (this) {
      if (!isPrepared) {
        // This file was not good and MediaPlayer quit
        Log.w(LOG_TAG,
            "MediaPlayer refused to play current item. Bailing on prepare.");
      }
    }

    cleanup();
    
    if (onCompletionListener != null) {
      onCompletionListener.onCompletion(mp);
    }

    if (playlistUrls != null && playlistUrls.size() > 0) {
      // Unfinished playlist
      String url = playlistUrls.remove(0);
      try {
        listen(url, current.isStream);
      } catch (IllegalArgumentException e) {
        Log.e(LOG_TAG, "", e);
      } catch (IllegalStateException e) {
        Log.e(LOG_TAG, "", e);
      } catch (IOException e) {
        Log.e(LOG_TAG, "", e);
      }
      return;
    }

    playNext();
    if (bindCount == 0 && !isPlaying()) {
      stopSelf();
    }
  }

  @Override
  public boolean onError(MediaPlayer mp, int what, int extra) {
    Log.w(LOG_TAG, "onError(" + what + ", " + extra + ")");
    synchronized (this) {
      if (!isPrepared) {
        // This file was not good and MediaPlayer quit
        Log.w(LOG_TAG,
            "MediaPlayer refused to play current item. Bailing on prepare.");
      }
    }
    return false;
  }

  @Override
  public boolean onInfo(MediaPlayer arg0, int arg1, int arg2) {
    Log.w(LOG_TAG, "onInfo(" + arg1 + ", " + arg2 + ")");
    return false;
  }

  private void playNext() {
    Log.w(LOG_TAG, "Playing next track");
    if (current != null) {
      PlaylistEntry entry = getNextPlaylistItem(current.order);
      if (entry != null) {
        current = entry;
        String url = current.url;
        try {
          if (url == null || url.equals("")) {
            Log.d(LOG_TAG, "no url");
            // Do nothing.
          } else {
            listen(url, current.isStream);
          }
        } catch (IllegalArgumentException e) {
          Log.e(LOG_TAG, "", e);
          e.printStackTrace();
        } catch (IllegalStateException e) {
          Log.e(LOG_TAG, "", e);
        } catch (IOException e) {
          Log.e(LOG_TAG, "", e);
        }
        Log.d(LOG_TAG, "playing commenced");
      }
    }
  }

  /**
   * Remove all intents and notifications about the last media.
   */
  private void cleanup() {
    notificationManager.cancel(NOTIFICATION_ID);
    if (lastChangeBroadcast != null) {
      getApplicationContext().removeStickyBroadcast(lastChangeBroadcast);
    }
    if (lastUpdateBroadcast != null) {
      getApplicationContext().removeStickyBroadcast(lastUpdateBroadcast);
    }
    getApplicationContext().sendBroadcast(new Intent(SERVICE_CLOSE_NAME));
  }

  private boolean isPlaylist(String url) {
    return url.indexOf("m3u") > -1 || url.indexOf("pls") > -1;
  }

  private void downloadPlaylist() throws MalformedURLException, IOException {
    String url = current.url;
    Log.d(LOG_TAG, "downloading " + url);
    URLConnection cn = new URL(url).openConnection();
    cn.connect();
    InputStream stream = cn.getInputStream();
    if (stream == null) {
      Log.e(LOG_TAG, "Unable to create InputStream for url: + url");
    }

    File downloadingMediaFile = new File(getCacheDir(), "playlist_data");
    FileOutputStream out = new FileOutputStream(downloadingMediaFile);
    byte buf[] = new byte[16384];
    int totalBytesRead = 0, incrementalBytesRead = 0;
    int numread;
    while ((numread = stream.read(buf)) > 0) {
      out.write(buf, 0, numread);
      totalBytesRead += numread;
      incrementalBytesRead += numread;
    }

    stream.close();
    out.close();
    PlaylistParser parser;
    if (url.indexOf("m3u") > -1) {
      parser = new M3uParser(downloadingMediaFile);
    } else if (url.indexOf("pls") > -1) {
      parser = new PlsParser(downloadingMediaFile);
    } else {
      return;
    }
    playlistUrls = parser.getUrls();
  }

  private PlaylistEntry retrievePlaylistItem(int current, boolean next) {
    String selection = PlaylistProvider.Items.IS_READ + " = ?";
    String[] selectionArgs = new String[1];
    selectionArgs[0] = "0";
    String sort = PlaylistProvider.Items.PLAY_ORDER + (next ? " asc" : " desc");
    return retrievePlaylistItem(selection, selectionArgs, sort);
  }

  private PlaylistEntry getNextPlaylistItem(int current) {
    return retrievePlaylistItem(current, true);
  }

  private PlaylistEntry retrievePlaylistItem(String selection,
      String[] selectionArgs, String sort) {
    Cursor cursor = getContentResolver().query(PlaylistProvider.CONTENT_URI,
        null, selection, selectionArgs, sort);
    return getFromCursor(cursor);
  }

  private PlaylistEntry getFromCursor(Cursor c) {
    String title = null, url = null, storyID = null;
    long id;
    int order;
    if (c.moveToFirst()) {
      id = c.getInt(c.getColumnIndex(PlaylistProvider.Items._ID));
      title = c.getString(c.getColumnIndex(PlaylistProvider.Items.NAME));
      url = c.getString(c.getColumnIndex(PlaylistProvider.Items.URL));
      order = c.getInt(c.getColumnIndex(PlaylistProvider.Items.PLAY_ORDER));
      storyID = c.getString(c.getColumnIndex(PlaylistProvider.Items.STORY_ID));
      c.close();
      return new PlaylistEntry(id, url, title, false, order, storyID);
    }
    c.close();
    return null;
  }

  private void markAsRead(long id) {
    Uri update = ContentUris.withAppendedId(PlaylistProvider.CONTENT_URI, id);
    ContentValues values = new ContentValues();
    values.put(Items.IS_READ, true);
    @SuppressWarnings("unused")
    int result = getContentResolver().update(update, values, null, null);
  }

  // -----------
  // Some stuff added for inspection when testing

  private OnCompletionListener onCompletionListener;

  /**
   * Allows a class to be notified when the currently playing track is
   * completed. Mostly used for testing the service
   * 
   * @param listener
   */
  public void setOnCompletionListener(OnCompletionListener listener) {
    onCompletionListener = listener;
  }

  private OnPreparedListener onPreparedListener;

  /**
   * Allows a class to be notified when the currently selected track has been
   * prepared to start playing. Mostly used for testing.
   * 
   * @param listener
   */
  public void setOnPreparedListener(OnPreparedListener listener) {
    onPreparedListener = listener;
  }
}




Java Source Code List

org.npr.android.news.AboutActivity.java
org.npr.android.news.Constants.java
org.npr.android.news.DownloadDrawable.java
org.npr.android.news.EditPreferences.java
org.npr.android.news.ListenView.java
org.npr.android.news.Main.java
org.npr.android.news.NewsListActivity.java
org.npr.android.news.NewsListAdapter.java
org.npr.android.news.NewsStoryActivity.java
org.npr.android.news.NewsTopicActivity.java
org.npr.android.news.PlaybackService.java
org.npr.android.news.PlayerActivity.java
org.npr.android.news.PlaylistActivity.java
org.npr.android.news.PodcastActivity.java
org.npr.android.news.Refreshable.java
org.npr.android.news.SearchActivity.java
org.npr.android.news.SearchResultsActivity.java
org.npr.android.news.StationDetailsActivity.java
org.npr.android.news.StationListActivity.java
org.npr.android.news.StationListAdapter.java
org.npr.android.news.StationSearchActivity.java
org.npr.android.news.StreamProxy.java
org.npr.android.news.Trackable.java
org.npr.android.util.Eula.java
org.npr.android.util.FileUtils.java
org.npr.android.util.M3uParser.java
org.npr.android.util.NodeUtils.java
org.npr.android.util.PlaylistEntry.java
org.npr.android.util.PlaylistParser.java
org.npr.android.util.PlaylistProvider.java
org.npr.android.util.PlsParser.java
org.npr.android.util.Tracker.java
org.npr.android.util.TypefaceCache.java
org.npr.api.ApiConstants.java
org.npr.api.ApiElement.java
org.npr.api.Client.java
org.npr.api.IterableNodeList.java
org.npr.api.Podcast.java
org.npr.api.Program.java
org.npr.api.Station.java
org.npr.api.StoryGrouping.java
org.npr.api.Story.java
org.npr.api.Topic.java