Android Open Source - SpotifyTray-Android Player Service






From Project

Back to project page SpotifyTray-Android.

License

The source code is released under:

MIT License

If you think the Android project SpotifyTray-Android 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.droidprojects.spotifytray;
/*from  w w  w .  j a v  a2  s. c  om*/
import java.io.IOException;
import java.io.InputStream;
import java.util.Timer;
import java.util.TimerTask;

import com.droidprojects.spotifytray.controller.MockPlaylist;
import com.droidprojects.spotifytray.controller.MockPlaylist.MockPlaylistListener;
import com.example.serviceproject.R;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.graphics.drawable.BitmapDrawable;
import android.os.Handler;
import android.os.IBinder;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.ScaleAnimation;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;

@SuppressWarnings("deprecation")
public class PlayerService extends Service
              implements MockPlaylistListener{

  // 
  private static final int TRAY_HIDDEN_FRACTION       = 6;   // Controls fraction of the tray hidden when open
  private static final int TRAY_MOVEMENT_REGION_FRACTION   = 6;  // Controls fraction of y-axis on screen within which the tray stays.
  private static final int TRAY_CROP_FRACTION       = 12;  // Controls fraction of the tray chipped at the right end.
  private static final int ANIMATION_FRAME_RATE       = 30;  // Animation frame rate per second.
  private static final int TRAY_DIM_X_DP           = 170;  // Width of the tray in dps
  private static final int TRAY_DIM_Y_DP           = 160;   // Height of the tray in dps
  private static final int BUTTONS_DIM_Y_DP         = 27;  // Height of the buttons in dps
  
  // Layout containers for various widgets
  private WindowManager         mWindowManager;      // Reference to the window
  private WindowManager.LayoutParams   mRootLayoutParams;    // Parameters of the root layout
  private RelativeLayout         mRootLayout;      // Root layout
  private RelativeLayout         mContentContainerLayout;// Contains everything other than buttons and song info
  private RelativeLayout         mLogoLayout;      // Contains Cpotify logo
  private RelativeLayout         mAlbumCoverLayout;    // Contains album cover of the active song
  private RelativeLayout         mAlbumCoverHelperLayout;// Contains cover of the previous song. This helps with fade animations.
  private LinearLayout         mPlayerButtonsLayout;  // Contains playback buttons
  private LinearLayout         mSongInfoLayout;    // Contains Text information on the current song

  // Widgets
  private ImageButton mPlaySongButton;
  private ImageButton mPauseSongButton;
  private TextView mSongTitleView;
  private TextView mSingerView;
  
  // Variables that control drag
  private int mStartDragX;
  //private int mStartDragY; // Unused as yet
  private int mPrevDragX;
  private int mPrevDragY;
  
  private boolean mIsTrayOpen = true;
  
  // Controls for animations
  private Timer           mTrayAnimationTimer;
  private TrayAnimationTimerTask   mTrayTimerTask;
  private Handler         mAnimationHandler = new Handler();
  
  // Mock song data
  private MockPlaylist mPlaylist;

  @Override
  public IBinder onBind(Intent intent) {
    // Not used
    return null;
  }

  @Override
  public void onCreate() {

    // Get references to all the views and add them to root view as needed.
    mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);

    mRootLayout = (RelativeLayout) LayoutInflater.from(this).
        inflate(R.layout.service_player, null);
    mContentContainerLayout = (RelativeLayout) mRootLayout.findViewById(R.id.content_container);
    mContentContainerLayout.setOnTouchListener(new TrayTouchListener());
    
    mLogoLayout = (RelativeLayout) mRootLayout.findViewById(R.id.logo_layout);
    mAlbumCoverLayout = (RelativeLayout) mRootLayout.findViewById(R.id.cover_layout);
    mAlbumCoverHelperLayout = (RelativeLayout) mRootLayout.findViewById(R.id.cover_helper_layout);
    
    mPlayerButtonsLayout = (LinearLayout) LayoutInflater.from(this).
        inflate(R.layout.viewgroup_player_buttons, null);
    mRootLayout.addView(mPlayerButtonsLayout);
    
    mSongInfoLayout = (LinearLayout) LayoutInflater.from(this).
        inflate(R.layout.viewgroup_song_info, null);
    mRootLayout.addView(mSongInfoLayout);
    
    mRootLayoutParams = new WindowManager.LayoutParams(
        Utils.dpToPixels(TRAY_DIM_X_DP, getResources()),
        Utils.dpToPixels(TRAY_DIM_Y_DP, getResources()),
        WindowManager.LayoutParams.TYPE_PHONE, 
        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 
        | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, 
        PixelFormat.TRANSLUCENT);

    mRootLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
    mWindowManager.addView(mRootLayout, mRootLayoutParams);
    
    mPlaySongButton = (ImageButton) mPlayerButtonsLayout.findViewById(R.id.button_play);
    mPauseSongButton = (ImageButton) mPlayerButtonsLayout.findViewById(R.id.button_pause);
    mSongTitleView = (TextView) mSongInfoLayout.findViewById(R.id.song_name);
    mSingerView = (TextView) mSongInfoLayout.findViewById(R.id.singer_name);
    
    mPlaylist = new MockPlaylist(this);
    
    // Post these actions at the end of looper message queue so that the layout is
    // fully inflated once these functions execute
    mRootLayout.postDelayed(new Runnable() {
      @Override
      public void run() {
        
        // Reusable variables
        RelativeLayout.LayoutParams params;
        InputStream is;
        Bitmap bmap;
        
        // Setup background spotify logo
        is = getResources().openRawResource(R.drawable.spot_bg);
        int containerNewWidth = (TRAY_CROP_FRACTION-1)*mLogoLayout.getHeight()/TRAY_CROP_FRACTION;
        bmap = Utils.loadMaskedBitmap(is, mLogoLayout.getHeight(), containerNewWidth);
        params = (RelativeLayout.LayoutParams) mLogoLayout.getLayoutParams();
        params.width = (bmap.getWidth() * mLogoLayout.getHeight()) / bmap.getHeight();
        params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,0);
        mLogoLayout.setLayoutParams(params);
        mLogoLayout.requestLayout();
        mLogoLayout.setBackgroundDrawable(new BitmapDrawable(getResources(), bmap));
        
        // Setup background album cover
        is=null;
        try {
          is = getAssets().open(mPlaylist.getCurrentSongInfo().mAlbumCoverPath);
        } catch (IOException e) {
          e.printStackTrace();
        }
        bmap = Utils.loadMaskedBitmap(is, mAlbumCoverLayout.getHeight(), containerNewWidth);
        params = (RelativeLayout.LayoutParams) mAlbumCoverLayout.getLayoutParams();
        params.width = (bmap.getWidth() * mAlbumCoverLayout.getHeight()) / bmap.getHeight();
        params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,0);
        mAlbumCoverLayout.setLayoutParams(params);
        mAlbumCoverLayout.requestLayout();
        mAlbumCoverHelperLayout.setLayoutParams(params);
        mAlbumCoverHelperLayout.requestLayout();
        mAlbumCoverLayout.setBackgroundDrawable(new BitmapDrawable(getResources(), bmap));

        // Setup playback buttons
        params = new RelativeLayout.LayoutParams(
            RelativeLayout.LayoutParams.MATCH_PARENT, 
            Utils.dpToPixels(BUTTONS_DIM_Y_DP, getResources()));
        params.addRule(RelativeLayout.ALIGN_RIGHT, R.id.tray_opener);
        params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
        params.leftMargin = mRootLayout.getWidth()/TRAY_HIDDEN_FRACTION;
        mRootLayout.updateViewLayout(mPlayerButtonsLayout, params);
        
        // setup song info views
        params = new RelativeLayout.LayoutParams(
            RelativeLayout.LayoutParams.MATCH_PARENT, 
            RelativeLayout.LayoutParams.WRAP_CONTENT);
        params.addRule(RelativeLayout.ALIGN_RIGHT, R.id.tray_opener);
        params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
        int marg = Utils.dpToPixels(5, getResources());
        params.setMargins(
            marg/2 + mRootLayout.getWidth()/TRAY_HIDDEN_FRACTION, 
            marg, 
            marg*3, 
            marg);
        mRootLayout.updateViewLayout(mSongInfoLayout, params);
        mSongTitleView.setText(mPlaylist.getCurrentSongInfo().mTitle);
        mSingerView.setText(mPlaylist.getCurrentSongInfo().mSinger);
        
        // Setup the root layout
        mRootLayoutParams.x = -mLogoLayout.getLayoutParams().width;
        mRootLayoutParams.y = (getApplicationContext().getResources().getDisplayMetrics().heightPixels-mRootLayout.getHeight()) / 2;
        mWindowManager.updateViewLayout(mRootLayout, mRootLayoutParams);
        
        // Make everything visible
        mRootLayout.setVisibility(View.VISIBLE);
        
        // Animate the Tray
        mTrayTimerTask = new TrayAnimationTimerTask();
        mTrayAnimationTimer = new Timer();
        mTrayAnimationTimer.schedule(mTrayTimerTask, 0, ANIMATION_FRAME_RATE);
      }
    }, ANIMATION_FRAME_RATE);
  }

  // The phone orientation has changed. Update the widget's position.
  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    if (mIsTrayOpen)
      mRootLayoutParams.x = -mRootLayout.getWidth()/TRAY_HIDDEN_FRACTION;
    else
      mRootLayoutParams.x = -mLogoLayout.getWidth();
    mRootLayoutParams.y = (getResources().getDisplayMetrics().heightPixels-mRootLayout.getHeight()) / 2;
    mWindowManager.updateViewLayout(mRootLayout, mRootLayoutParams);
    animateButtons();
  }
  
  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    
    
    if (intent.getBooleanExtra("stop_service", false)){
      // If it's a call from the notification, stop the service.
      stopSelf();
    }else{
      // Make the service run in foreground so that the system does not shut it down.
      Intent notificationIntent = new Intent(this, PlayerService.class);
      notificationIntent.putExtra("stop_service", true);
      PendingIntent pendingIntent = PendingIntent.getService(this, 0, notificationIntent, 0);
      Notification notification = new Notification(
          R.drawable.ic_launcher, 
          "Spotify tray launched",
              System.currentTimeMillis());
      notification.setLatestEventInfo(
          this, 
          "Spotify tray",
              "Tap to close the widget.", 
              pendingIntent);
      startForeground(86, notification);
    }
    return START_STICKY;
  }

  // The app is closing.
  @Override
  public void onDestroy() {
    mPlaylist.stopCurrentSong();
    if (mRootLayout != null)
      mWindowManager.removeView(mRootLayout);
  }

  // Drags the tray as per touch info
  private void dragTray(int action, int x, int y){
    switch (action){
    case MotionEvent.ACTION_DOWN:
      
      // Cancel any currently running animations/automatic tray movements.
      if (mTrayTimerTask!=null){
        mTrayTimerTask.cancel();
        mTrayAnimationTimer.cancel();
      }
      
      // Store the start points
      mStartDragX = x;
      //mStartDragY = y;
      mPrevDragX = x;
      mPrevDragY = y;
      break;
      
    case MotionEvent.ACTION_MOVE:
      
      // Calculate position of the whole tray according to the drag, and update layout.
      float deltaX = x-mPrevDragX;
      float deltaY = y-mPrevDragY;
      mRootLayoutParams.x += deltaX;
      mRootLayoutParams.y += deltaY;
      mPrevDragX = x;
      mPrevDragY = y;
      animateButtons();
      mWindowManager.updateViewLayout(mRootLayout, mRootLayoutParams);
      break;
      
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL:
      
      // When the tray is released, bring it back to "open" or "closed" state.
      if ((mIsTrayOpen && (x-mStartDragX)<=0) ||
        (!mIsTrayOpen && (x-mStartDragX)>=0))
        mIsTrayOpen = !mIsTrayOpen;
      
      mTrayTimerTask = new TrayAnimationTimerTask();
      mTrayAnimationTimer = new Timer();
      mTrayAnimationTimer.schedule(mTrayTimerTask, 0, ANIMATION_FRAME_RATE);
      break;
    }
  }

  // Listens to the touch events on the tray.
  private class TrayTouchListener implements OnTouchListener {
    @Override
    public boolean onTouch(View v, MotionEvent event) {

      final int action = event.getActionMasked();

      switch (action) {
      case MotionEvent.ACTION_DOWN: 
      case MotionEvent.ACTION_MOVE:
      case MotionEvent.ACTION_UP:
      case MotionEvent.ACTION_CANCEL:
        // Filter and redirect the events to dragTray()
        dragTray(action, (int)event.getRawX(), (int)event.getRawY());
        break;
      default:
        return false;
      }
      return true;

    }
  }
  
  // Timer for animation/automatic movement of the tray.
  private class TrayAnimationTimerTask extends TimerTask{
    
    // Ultimate destination coordinates toward which the tray will move
    int mDestX;
    int mDestY;
    
    public TrayAnimationTimerTask(){
      
      // Setup destination coordinates based on the tray state. 
      super();
      if (!mIsTrayOpen){
        mDestX = -mLogoLayout.getWidth();
      }else{
        mDestX = -mRootLayout.getWidth()/TRAY_HIDDEN_FRACTION;
      }
      
      // Keep upper edge of the widget within the upper limit of screen
      int screenHeight = getResources().getDisplayMetrics().heightPixels;
      mDestY = Math.max(
          screenHeight/TRAY_MOVEMENT_REGION_FRACTION, 
          mRootLayoutParams.y);
      
      // Keep lower edge of the widget within the lower limit of screen
      mDestY = Math.min(
          ((TRAY_MOVEMENT_REGION_FRACTION-1)*screenHeight)/TRAY_MOVEMENT_REGION_FRACTION - mRootLayout.getWidth(), 
          mDestY);
    }
    
    // This function is called after every frame.
    @Override
    public void run() {
      
      // handler is used to run the function on main UI thread in order to
      // access the layouts and UI elements.
      mAnimationHandler.post(new Runnable() {
        @Override
        public void run() {
          
          // Update coordinates of the tray
          mRootLayoutParams.x = (2*(mRootLayoutParams.x-mDestX))/3 + mDestX;
          mRootLayoutParams.y = (2*(mRootLayoutParams.y-mDestY))/3 + mDestY;
          mWindowManager.updateViewLayout(mRootLayout, mRootLayoutParams);
          animateButtons();
          
          // Cancel animation when the destination is reached
          if (Math.abs(mRootLayoutParams.x-mDestX)<2 && Math.abs(mRootLayoutParams.y-mDestY)<2){
            TrayAnimationTimerTask.this.cancel();
            mTrayAnimationTimer.cancel();
          }
        }
      });
    }
  }
  
  // This function animates the buttons based on the position of the tray.
  private void animateButtons(){
    
    // Animate only if the tray is between open and close state.
    if (mRootLayoutParams.x < -mRootLayout.getWidth()/TRAY_HIDDEN_FRACTION){
      
      // Scale the distance between open and close states to 0-1. 
      float relativeDistance = (mRootLayoutParams.x + mLogoLayout.getWidth())/(float)
          (-mRootLayout.getWidth()/TRAY_HIDDEN_FRACTION + mLogoLayout.getWidth());
      
      // Limit it to 0-1 if it goes beyond 0-1 for any reason.
      relativeDistance=Math.max(relativeDistance, 0);
      relativeDistance=Math.min(relativeDistance, 1);
      
      // Setup animations
      AnimationSet animations = new AnimationSet(true);
      animations.setFillAfter(true);
      Animation animationAlpha = new AlphaAnimation(
          relativeDistance, 
          relativeDistance);
      animations.addAnimation(animationAlpha);

      Animation animationScale = new ScaleAnimation(
          relativeDistance, 
          relativeDistance, 
          relativeDistance, 
          relativeDistance);
      animations.addAnimation(animationScale);
      
      // Play the animations
      mPlayerButtonsLayout.startAnimation(animations);
      mSongInfoLayout.startAnimation(animations);
      mAlbumCoverLayout.startAnimation(animationAlpha);
    }else{
      
      // Clear all animations if the tray is being dragged - that is, when it is beyond the
      // normal open state.
      mPlayerButtonsLayout.clearAnimation();
      mSongInfoLayout.clearAnimation();
      mAlbumCoverLayout.clearAnimation();
    }
  }
  
  // Load new album cover image
  private void changeSongDisplayInfo(){
    
    InputStream is=null;
    try {
      is = getAssets().open(mPlaylist.getCurrentSongInfo().mAlbumCoverPath);
    } catch (IOException e) {
      e.printStackTrace();
    }
    
    // Load new bitmap
    Bitmap bmap = Utils.loadMaskedBitmap(is, mAlbumCoverLayout.getHeight(), mAlbumCoverLayout.getWidth());
    
    // Change backgrounds
    mAlbumCoverHelperLayout.setBackgroundDrawable(mAlbumCoverLayout.getBackground());
    mAlbumCoverLayout.setBackgroundDrawable(new BitmapDrawable(getResources(), bmap));
    
    // Animate the two layouts in order to achieve the fade in/fade out effect.
    // First normalise the distance between open and closed states (0-1)
    float relativeDistance = (mRootLayoutParams.x + mLogoLayout.getWidth())/(float)
        (-mRootLayout.getWidth()/TRAY_HIDDEN_FRACTION + mLogoLayout.getWidth());
    relativeDistance=Math.max(relativeDistance, 0);
    relativeDistance=Math.min(relativeDistance, 1);
    
    // Then use it to set final alpha in the animation.
    Animation fadeOutAnim = new AlphaAnimation(relativeDistance,0.f);
    fadeOutAnim.setFillAfter(true);
    fadeOutAnim.setDuration(1000);
    Animation fadeInAnim = new AlphaAnimation(0.f,relativeDistance);
    fadeInAnim.setFillAfter(true);
    fadeInAnim.setDuration(1000);
    mAlbumCoverHelperLayout.startAnimation(fadeOutAnim);
    mAlbumCoverLayout.startAnimation(fadeInAnim);
    
    // Set new song info
    mSongTitleView.setText(mPlaylist.getCurrentSongInfo().mTitle);
    mSingerView.setText(mPlaylist.getCurrentSongInfo().mSinger);
  }
  
  /**************************** Callbacks **************************************/
  
  // Play current song
  public void playButtonClicked(View view){
    mPlaySongButton.setVisibility(View.INVISIBLE);
    mPauseSongButton.setVisibility(View.VISIBLE);
    mPlaylist.playCurrentSong();
  }

  // Pause current song
  public void pauseButtonClicked(View view){
    mPauseSongButton.setVisibility(View.INVISIBLE);
    mPlaySongButton.setVisibility(View.VISIBLE);
    mPlaylist.pauseCurrentSong();
  }
  
  // Play next song
  public void nextButtonClicked(View view){
    mPlaySongButton.setVisibility(View.INVISIBLE);
    mPauseSongButton.setVisibility(View.VISIBLE);
    mPlaylist.playNextSong();
    changeSongDisplayInfo();
  }
  
  // Play previous song
  public void prevButtonClicked(View view){
    mPlaySongButton.setVisibility(View.INVISIBLE);
    mPauseSongButton.setVisibility(View.VISIBLE);
    mPlaylist.playPreviousSong();
    changeSongDisplayInfo();
  }

  
  // Mock song playlist callback - Notifies the UI about song progress. 
  @Override
  public void updateSongProgress(int playheadPosition) {
    // TODO - Will be implemented later
  }

  // Mock song playlist callback - Notifies the UI to update song info. Current song has changed.
  @Override
  public void startedNextSong() {
    changeSongDisplayInfo();
  }

  // Mock song playlist callback - Provides the UI thread handler so that the callee thread could update UI
  @Override
  public Handler getHandler() {
    return new Handler();
  }
}




Java Source Code List

com.droidprojects.spotifytray.MainActivity.java
com.droidprojects.spotifytray.PlayerService.java
com.droidprojects.spotifytray.Utils.java
com.droidprojects.spotifytray.controller.MockPlaylist.java
com.droidprojects.spotifytray.controller.MockSong.java