/* Copyright (C) 2013 Haowen Ning This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.liberty.android.fantastischmemo.service; import android.app.PendingIntent; import android.content.Intent; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.support.v4.app.NotificationCompat; import android.util.Log; import com.google.common.base.Preconditions; import org.liberty.android.fantastischmemo.common.AnyMemoDBOpenHelper; import org.liberty.android.fantastischmemo.common.AnyMemoDBOpenHelperManager; import org.liberty.android.fantastischmemo.R; import org.liberty.android.fantastischmemo.common.BaseService; import org.liberty.android.fantastischmemo.entity.Card; import org.liberty.android.fantastischmemo.entity.Option; import org.liberty.android.fantastischmemo.service.cardplayer.CardPlayerContext; import org.liberty.android.fantastischmemo.service.cardplayer.CardPlayerEventHandler; import org.liberty.android.fantastischmemo.service.cardplayer.CardPlayerMessage; import org.liberty.android.fantastischmemo.ui.CardPlayerActivity; import org.liberty.android.fantastischmemo.utils.CardTTSUtil; import javax.inject.Inject; public class CardPlayerService extends BaseService { public static final String EXTRA_DBPATH = "dbpath"; public static final String EXTRA_CURRENT_CARD_ID = "current_card_id"; public static final String ACTION_GO_TO_CARD = "org.liberty.android.fantastischmemo.CardPlayerService.ACTION_GO_TO_CARD"; public static final String ACTION_PLAYING_STOPPED = "org.liberty.android.fantastischmemo.CardPlayerService.PLAYING_STOPPED"; private static final String TAG = CardPlayerService.class.getSimpleName(); // Magic id used for Card player's notification private static final int NOTIFICATION_ID = 9283372; // This is the object that receives interactions from clients. private final IBinder binder = new LocalBinder(); private String dbPath; private AnyMemoDBOpenHelper dbOpenHelper; private Handler handler; private CardTTSUtil cardTTSUtil; @Inject Option option; // The context used for card player state machine. private volatile CardPlayerContext cardPlayerContext = null; @Override public void onCreate() { super.onCreate(); appComponents().inject(this); } // Note, it is recommended for service binding in a thread different // from UI thread. The initialization like DAO creation is quite heavy @Override public IBinder onBind(Intent intent) { handler = new Handler(); Bundle extras = intent.getExtras(); assert extras != null : "dbpath is not passed to AMTTSService."; dbPath = extras.getString(EXTRA_DBPATH); final int cardId = extras.getInt(EXTRA_CURRENT_CARD_ID); cardTTSUtil = new CardTTSUtil(appComponents().applicationContext(), dbPath); dbOpenHelper = AnyMemoDBOpenHelperManager.getHelper(this, dbPath); // Assign a value to the cardPlayerContext so we do not need to check // null for every player methods. The initial STOPPED state will help // skipToPrev/skipToNext method to callback the event handler. reset(); cardPlayerContext.setCurrentCard(dbOpenHelper.getCardDao().queryForId(cardId)); return binder; } @Override public void onRebind(Intent intent) { super.onRebind(intent); } @Override public int onStartCommand(Intent intent, int flags, int startId) { // We want this service to continue running until it is explicitly // stopped, so return sticky. return START_STICKY; } @Override public boolean onUnbind(Intent intent) { AnyMemoDBOpenHelperManager.releaseHelper(dbOpenHelper); cardTTSUtil.release(); // Always stop service on unbind so the service will not be reused // for the next binding. return false; } @Override public void onDestroy() { super.onDestroy(); } public void startPlaying(Card startCard) { Preconditions.checkNotNull(startCard); // Always to create a new context if we start playing to ensure it is playing // from a clean state. reset(); cardPlayerContext.setCurrentCard(startCard); cardPlayerContext.getState().transition(cardPlayerContext, CardPlayerMessage.START_PLAYING); showNotification(); } public void skipToNext() { cardPlayerContext.getState().transition(cardPlayerContext, CardPlayerMessage.GO_TO_NEXT); } public void skipToPrev() { cardPlayerContext.getState().transition(cardPlayerContext, CardPlayerMessage.GO_TO_PREV); } public void stopPlaying() { Log.v(TAG, "Stop playing"); cancelNotification(); cardPlayerContext.getState().transition(cardPlayerContext, CardPlayerMessage.STOP_PLAYING); } /* * Stop playing and reset the context */ public void reset() { // When we reset, we want to know the current card // to set so the newly created context will have the // current card previously set // If it is null, we will leave it undertermined. Card currentCard = null; if (cardPlayerContext != null) { stopPlaying(); currentCard = cardPlayerContext.getCurrentCard(); } cardPlayerContext = new CardPlayerContext( cardPlayerEventHandler, cardTTSUtil, handler, dbOpenHelper, option.getCardPlayerIntervalBetweenQA(), option.getCardPlayerIntervalBetweenCards(), option.getCardPlayerShuffleEnabled(), option.getCardPlayerRepeatEnabled()); if (currentCard != null) { cardPlayerContext.setCurrentCard(currentCard); } } /* * A notification is shown if the player is playing. * This also put the service in foreground mode to prevent the service * being terminated. */ private void showNotification() { Intent resultIntent = new Intent(this, CardPlayerActivity.class); // Make sure to resume the activity instead of creating a new one. resultIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, resultIntent, 0); NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this) .setSmallIcon(R.drawable.ic_launcher) .setContentTitle(getString(R.string.card_player_notification_title)) .setContentText(getString(R.string.card_player_notification_text)) .setContentIntent(pendingIntent) .setOngoing(true); // Basically make the service foreground so a notification is shown // And the service is less susceptible to be kill by Android system. startForeground(NOTIFICATION_ID, mBuilder.build()); } /* * This handler is used for callback from the CardPlayerService's startPlaying. * This implementation send broadcast for ACTION_GO_TO_CARD and the * CardPlayerFragment will register the broadcast receiver in CardPlaeyrActivity. */ private CardPlayerEventHandler cardPlayerEventHandler = new CardPlayerEventHandler() { @Override public void onPlayCard(Card card) { Intent intent = new Intent(); intent.setAction(ACTION_GO_TO_CARD); intent.putExtra(EXTRA_CURRENT_CARD_ID, card.getId()); sendBroadcast(intent); } @Override public void onStopPlaying() { Intent intent = new Intent(); intent.setAction(ACTION_PLAYING_STOPPED); sendBroadcast(intent); } }; private void cancelNotification() { stopForeground(true); } // A local binder that works for local methos call. public class LocalBinder extends Binder { public CardPlayerService getService() { return CardPlayerService.this; } public Card getCurrentPlayingCard() { if (cardPlayerContext != null) { return cardPlayerContext.getCurrentCard(); } else { return null; } } } }