/* * Copyright (C) 2012, Katy Hilgenberg. * Special acknowledgments to: Knowledge & Data Engineering Group, University of Kassel (http://www.kde.cs.uni-kassel.de). * Contact: sdcf@cs.uni-kassel.de * * This file is part of the SDCFramework (Sensor Data Collection Framework) project. * * The SDCFramework is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The SDCFramework 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the SDCFramework. If not, see <http://www.gnu.org/licenses/>. */ package de.unikassel.android.sdcframework.app.scheduler; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.TimeZone; import de.unikassel.android.sdcframework.app.facade.ISDCService; import de.unikassel.android.sdcframework.data.Weekday; import de.unikassel.android.sdcframework.data.WeekdaySchedule; import de.unikassel.android.sdcframework.data.WeekdayScheduleEntry; import de.unikassel.android.sdcframework.data.WeekdaySchedulerAction; import de.unikassel.android.sdcframework.data.WeeklySchedule; import de.unikassel.android.sdcframework.data.independent.GlobalSerializer; import de.unikassel.android.sdcframework.preferences.ApplicationPreferenceManagerImpl; import de.unikassel.android.sdcframework.preferences.facade.ApplicationPreferenceManager; import de.unikassel.android.sdcframework.service.ServiceUtils; import de.unikassel.android.sdcframework.util.Logger; import de.unikassel.android.sdcframework.util.TimeProvider; import android.annotation.SuppressLint; import android.app.AlarmManager; import android.app.IntentService; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.util.Log; /** * The scheduler background process. * * @author Katy Hilgenberg * */ public class ScheduleService extends IntentService { /** * A simple structure to store the data for an alarm. * * @author Katy Hilgenberg * */ private class ScheduledAlarm { /** * The related weekday schedule entry */ public WeekdayScheduleEntry schedule; /** * The calculated alarm time */ public Calendar alarmTime; } /** * The action this service does process from intents. */ private static final String ACTION = WeekdaySchedulerAction.ACTION; /** * The preference manager */ private ApplicationPreferenceManager prefManager; /** * The alarm manager */ private AlarmManager alarmManager; /** * Date formatter for log messages. */ @SuppressLint( "SimpleDateFormat" ) private final SimpleDateFormat df = new SimpleDateFormat(); /** * The wake lock to keep CPU running while service is active. */ private PowerManager.WakeLock wakeLock; /** * Constructor */ public ScheduleService() { super( ScheduleService.class.getSimpleName() ); } /* * (non-Javadoc) * * @see android.app.IntentService#onCreate() */ @Override public void onCreate() { super.onCreate(); this.prefManager = new ApplicationPreferenceManagerImpl(); this.alarmManager = (AlarmManager) getSystemService( Context.ALARM_SERVICE ); acquireWakeLock(); } /* * (non-Javadoc) * * @see android.app.IntentService#onDestroy() */ @Override public void onDestroy() { releaseWakeLock(); prefManager.onDestroy(); prefManager = null; alarmManager = null; super.onDestroy(); } /* * (non-Javadoc) * * @see android.app.IntentService#onHandleIntent(android.content.Intent) */ @Override protected final void onHandleIntent( Intent intent ) { // get time stamp first Calendar now = Calendar.getInstance(); Logger.getInstance().info( this, "received intent at " + df.format( now.getTime() ) ); // ignore wrong ACTIONS if ( !ACTION.equals( intent.getAction() ) ) return; WeeklySchedule schedule = prefManager.getServicePreferences().getWeeklySchedulePreference().getConfiguration( prefManager.getSharedPreferences( getApplicationContext() ) ); // unparcel intent data first WeekdayScheduleEntry lastScheduledEntry = getEntryFromIntent( intent, schedule ); // cancel any pending intent alarmManager.cancel( createPendingIntent( createIntent( this ), this ) ); // create new alarm ScheduledAlarm alarm = getNextAlarm( schedule, now, lastScheduledEntry ); setAlarm( alarm ); WeekdaySchedulerAction currentAction = null; if ( lastScheduledEntry != null ) { // the event was raised by an alarm Logger.getInstance().info( this, "Execution of alarm event: " + lastScheduledEntry.toString() ); currentAction = lastScheduledEntry.getAction(); } else if ( alarm != null ) { // update to expected current state if necessary currentAction = alarm.schedule.getAction().getPreviousAction(); Logger.getInstance().info( this, "Update with previous action to force expected running state: " + currentAction.toString() ); } execute( currentAction ); } /** * Method to set a new alarm for next event. * * @param alarm * the alarm to create */ private final void setAlarm( ScheduledAlarm alarm ) { if ( alarm != null ) { Intent intent = createIntent( this ); String extra = alarm.schedule.toString(); intent.putExtra( WeekdayScheduleEntry.class.getSimpleName(), extra ); intent.putExtra( Weekday.class.getSimpleName(), alarm.schedule.getWeekday().name() ); PendingIntent pendingIntent = createPendingIntent( intent, this ); alarm.alarmTime.setTimeZone( TimeZone.getTimeZone( "GMT" ) ); alarmManager.set( AlarmManager.RTC_WAKEUP, alarm.alarmTime.getTimeInMillis(), pendingIntent ); Logger.getInstance().info( this, "scheduled next service state change for " + df.format( alarm.alarmTime.getTimeInMillis() ) + ": " + alarm.schedule ); } } /** * Does create a pending intent to start this service. * * @param intent * the intent to wrap * @param context * the context * @return a pending intent to start this service */ private final static PendingIntent createPendingIntent( Intent intent, Context context ) { return PendingIntent.getService( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT ); } /** * Does create an intent to start the schedule service * * @param applicationContext * the context * * @return an intent to start the schedule service */ public final static Intent createIntent( Context applicationContext ) { return new Intent( ACTION ).setClass( applicationContext, ScheduleService.class ); } /** * Method to calculate the next alarm based on time and a given schedule. * * @param schedule * the current schedule * @param now * the current time stamp * @param lastScheduledEntry * the last scheduled entry if known * @return the next scheduled alarm */ private final ScheduledAlarm getNextAlarm( WeeklySchedule schedule, Calendar now, WeekdayScheduleEntry lastScheduledEntry ) { if ( schedule.size() == 0 ) return null; ScheduledAlarm alarm = new ScheduledAlarm(); // initialize with date of todays weekday alarm.alarmTime = TimeProvider.getDayBegin( now ); alarm.schedule = null; int startTimeLimit = ( now.get( Calendar.MINUTE ) + now.get( Calendar.HOUR_OF_DAY ) * 60 ) * 60 + now.get( Calendar.SECOND ); // start search at todays weekday Weekday weekday = Weekday.valueOf( now ); // just avoid restarting same schedule if alarm was raised exact in time // (processing takes place in the minute window) if ( lastScheduledEntry != null && weekday.equals( lastScheduledEntry.getWeekday() ) && lastScheduledEntry.getSeconds() >= startTimeLimit ) { startTimeLimit = lastScheduledEntry.getSeconds() + 1; } while ( true ) { for ( WeekdayScheduleEntry entry : schedule.getScheduleForWeekday( weekday ).getEntries() ) { if ( entry.getSeconds() >= startTimeLimit ) { alarm.schedule = entry; alarm.alarmTime.add( Calendar.MILLISECOND, entry.getMilliseconds() ); break; } } // stop as soon as an activity was found if ( alarm.schedule != null ) break; alarm.alarmTime.add( Calendar.HOUR_OF_DAY, 24 ); startTimeLimit = 0; weekday = Weekday.next( weekday ); } return alarm; } /** * Does execute a scheduled action * * @param action * the action to execute */ protected final void execute( WeekdaySchedulerAction action ) { boolean isRunning = ServiceUtils.isServiceRunning( getApplicationContext(), ISDCService.class ); // execute action if ( WeekdaySchedulerAction.StopService.equals( action ) && isRunning ) { ServiceUtils.stopService( getApplicationContext(), ISDCService.class ); } else if ( WeekdaySchedulerAction.StartService.equals( action ) && !isRunning ) { ServiceUtils.startService( getApplicationContext(), ISDCService.class ); } } /** * Method to get a scheduled valid entry from an intent (valid according to * the given schedule ). * * @param intent * the intent to create scheduled entry from * @param schedule * the schedule to validate an existing entry for * @return the scheduled valid entry from this intent or null */ private final WeekdayScheduleEntry getEntryFromIntent( Intent intent, WeeklySchedule schedule ) { if ( intent.hasExtra( WeekdayScheduleEntry.class.getSimpleName() ) ) { try { // get scheduled entry and validate String extra = intent.getStringExtra( Weekday.class.getSimpleName() ); Weekday weekday = Weekday.valueOf( extra ); extra = intent.getStringExtra( WeekdayScheduleEntry.class.getSimpleName() ); WeekdayScheduleEntry entry; entry = GlobalSerializer.fromXML( WeekdayScheduleEntry.class, extra ); entry.setWeekdaySchedule( new WeekdaySchedule( weekday ) ); // test if entry has an assigned weekday and if it is still in the // schedule if ( weekday != null && schedule.getScheduleForWeekday( weekday ).getEntries().contains( entry ) ) { return entry; } } catch ( Exception e ) { Logger.getInstance().error( this, Log.getStackTraceString( e ) ); } } return null; } /** * Setter for the wake lock * * @param wakeLock * the wake lock to set */ private final void setWakeLock( PowerManager.WakeLock wakeLock ) { this.wakeLock = wakeLock; } /** * Getter for the wake lock * * @return the wake lock */ private final PowerManager.WakeLock getWakeLock() { if ( wakeLock == null ) { PowerManager powerManager = (PowerManager) getSystemService( Context.POWER_SERVICE ); setWakeLock( powerManager.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, getClass().getSimpleName() ) ); } return wakeLock; } /** * Does release the wake lock */ private final void releaseWakeLock() { WakeLock wakeLock = getWakeLock(); if ( wakeLock != null && wakeLock.isHeld() ) { wakeLock.release(); Logger.getInstance().info( this, "wake lock released" ); } } /** * Does acquire the wake lock */ private final void acquireWakeLock() { WakeLock wakeLock = getWakeLock(); if ( wakeLock != null ) { wakeLock.acquire(); Logger.getInstance().info( this, "wake lock aquired" ); } } }