/*
* 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.util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import android.content.Context;
import android.os.Environment;
import de.unikassel.android.sdcframework.preferences.facade.TransmissionProtocolConfiguration;
import de.unikassel.android.sdcframework.transmission.BasicAuthHttpProtocol;
import de.unikassel.android.sdcframework.transmission.ConnectionStrategyBuilder;
import de.unikassel.android.sdcframework.transmission.UnknownProtocol;
import de.unikassel.android.sdcframework.transmission.facade.ConnectionStrategy;
import de.unikassel.android.sdcframework.transmission.facade.ProtocolStrategy;
import de.unikassel.android.sdcframework.transmission.facade.UpdatableTransmissionComponent;
import de.unikassel.android.sdcframework.util.facade.EventObserver;
import de.unikassel.android.sdcframework.util.facade.ObservableEventSource;
/**
* A worker thread to realize a the global log file handling.
*
* @author Katy Hilgenberg
*
*/
public final class LogfileManager
extends AbstractWorkerThread
implements
UpdatableTransmissionComponent< TransmissionProtocolConfiguration >,
EventObserver< AlarmEvent >
{
/**
* The delay for the automatic instance release ( to allow final log
* information be still logged)
*/
private static final long TERMINATION_DELAY = 30000L;
/**
* The log file name
*/
private static final String FILE_NAME = File.separatorChar + "sdcf.log.";
/**
* Relative file path on the external storage media
*/
public static final String RELATIVE_PATH =
File.separatorChar + "sdcframework";
/**
* The environmental storage directory
*/
private static final String STORAGE_DIR =
Environment.getExternalStorageDirectory().getAbsolutePath()
+ RELATIVE_PATH;
/**
* The queue for log messages
*/
private final ConcurrentLinkedQueue< LogEvent > logEvents;
/**
* The queue for files to transfer
*/
private final ConcurrentLinkedQueue< String > transferFiles;
/**
* The strategy implementing the protocol
*/
protected ProtocolStrategy protocolStrategy;
/**
* The connection strategy
*/
private ConnectionStrategy connectionStrategy;
/**
* The wake lock holder instance
*/
private final WakeLockHolder wakeLockHolder;
/**
* The uuid of the device
*/
private final UUID uuid;
/**
* The observable wake up alarm
*/
private final ObservableAlarm alarm;
/**
* The context
*/
private final Context context;
/**
* The singleton instance
*/
private static LogfileManager instance = null;
/**
* Getter for the instance
*
* @return the log file transfer task instance
*/
public final static synchronized LogfileManager getInstance()
{
return instance;
}
/**
* Method to create the global instance
*
* @param context
* the context
* @param uuid
* the unique SDC installation identifier for this device
*/
public final static synchronized void createInstance( Context context,
UUID uuid )
{
releaseInstance();
instance = new LogfileManager( context, uuid );
instance.startWork();
}
/**
* Method to destroy the global instance
*/
private final static synchronized void releaseInstance()
{
if ( instance != null )
{
instance.doTerminate();
instance = null;
}
}
/**
* Constructor
*
* @param context
* the context
* @param uuid
* the unique SDC installation identifier for this device
*/
private LogfileManager( Context context, UUID uuid )
{
super();
setLogging( false );
this.context = context;
this.wakeLockHolder = new WakeLockHolder( context );
Logger.getInstance().debug( this,
wakeLockHolder.hashCode() + ": wake lock holder created" );
this.uuid = uuid;
this.logEvents = new ConcurrentLinkedQueue< LogEvent >();
transferFiles = new ConcurrentLinkedQueue< String >();
this.protocolStrategy = null;
this.connectionStrategy = null;
this.alarm = AlarmBuilder.createAlarm( this, context );
this.alarm.onCreate( context );
this.alarm.onResume( context );
alarm.registerEventObserver( this );
}
/**
* Method to prepare delayed instance release
*/
public static synchronized void prepareReleaseInstance()
{
if ( instance != null )
{
instance.alarm.setAlarm( TERMINATION_DELAY );
}
}
/**
* Method to test for a valid configuration
*
* @return true if upload configuration is available
*/
protected final boolean isConfigured()
{
return protocolStrategy != null && protocolStrategy.getHost() != null;
}
/**
* Method to add a log event
*
* @param event
* the log event to add to the queue
*/
public final void addLogEvent( LogEvent event )
{
logEvents.offer( event );
}
/*
* (non-Javadoc)
*
* @see
* de.unikassel.android.sdcframework.util.AbstractWorkerThread#doCleanUp()
*/
@Override
protected final void doCleanUp()
{
alarm.cancelAlarm();
alarm.unregisterEventObserver( this );
alarm.onPause( context );
alarm.onDestroy( context );
}
/*
* (non-Javadoc)
*
* @see de.unikassel.android.sdcframework.util.AbstractWorkerThread#doWork()
*/
@Override
protected final void doWork()
{
// process log event if available
LogEvent event = logEvents.poll();
if ( event != null )
{
// just empty queue as long as external storage is not available
while ( !isExternalStorageAvailable() && !logEvents.isEmpty() )
{
event = logEvents.remove();
}
if ( !saveLogEvent( event ) )
{
Logger.getInstance().error( this,
"Failed to save log event: " + event.getLongMessage() );
}
if ( !transferFiles.isEmpty() && isConfigured() )
{
String file = transferFiles.peek();
if ( uploadFile( file ) )
{
transferFiles.poll();
Logger.getInstance().debug( this,
"Successful log file transfer: \"" + file + "\"" );
}
}
}
else
{
try
{
Thread.sleep( 2000 );
}
catch ( InterruptedException e )
{}
}
}
/**
* Me6thod to save a log event
*
* @param event
* the log event to save to file
* @return true if successful, false otherwise
*/
private boolean saveLogEvent( LogEvent event )
{
String msg = event.getLongMessage() + '\n';
FileOutputStream fos = null;
try
{
File logFile =
getLogFile( TimeProvider.getUTCDayTimeMillis( event.getTimeStamp() ) );
fos = new FileOutputStream( logFile, true );
fos.write( msg.getBytes() );
fos.flush();
return true;
}
catch ( Exception e )
{
Logger.getInstance().error( this,
"Exception in saveLogEvent: " + e.getMessage() );
}
finally
{
if ( fos != null )
{
try
{
fos.close();
}
catch ( IOException e )
{}
}
}
return false;
}
/**
* Getter for the actual log file
*
* @param ts
* the time stamp
* @return the current log file if accessible
* @throws IOException
*/
protected final File getLogFile( long ts ) throws IOException
{
if ( !isExternalStorageAvailable() )
{
throw new IOException( "External storage not availale!" );
}
File dir = FileUtils.fileFromPath( STORAGE_DIR );
if ( ( dir.exists() || dir.mkdirs() ) && dir.isDirectory() )
{
// try to access the expected log file
File file =
FileUtils.fileFromPath( new StringBuffer( STORAGE_DIR ).append(
FILE_NAME ).append(
TimeProvider.toUTCDate( ts ) ).toString() );
if ( !file.exists() )
{
// new log file will be created -> mark older files for transfer
for ( File oldFile : dir.listFiles() )
{
transferFiles.offer( oldFile.getAbsolutePath() );
}
}
return file;
}
throw new IOException( "Failed to create directory "
+ dir.getAbsolutePath() );
}
/**
* Test method for availability of the external storage device
*
* @return true if available, false otherwise
*/
private boolean isExternalStorageAvailable()
{
return Environment.MEDIA_MOUNTED.equals( Environment.getExternalStorageState() );
}
/**
* Does upload a file to the configured remote server using the given
* authentication data
*
* @param fileName
* the file to upload
* @return true if successful, false otherwise
*/
private synchronized final boolean uploadFile( String fileName )
{
if ( !isConfigured() )
return false;
boolean result = true;
File file = FileUtils.fileFromPath( fileName );
// simply ignore not existing files
if ( file.exists() )
{
result = false;
String currentFile =
new StringBuffer( STORAGE_DIR ).append( FILE_NAME ).append(
TimeProvider.toUTCDate( TimeProvider.getUTCDayTimeMillis() ) ).toString();
// wake up device for time sync
wakeLockHolder.acquireWakeLock();
try
{
// wait a bit for connectivity
try
{
Thread.sleep( 5000 );
}
catch ( InterruptedException e )
{}
protocolStrategy.setFileName( fileName );
result = connectionStrategy.doWork( protocolStrategy );
if ( result )
{
if ( !currentFile.equals( file.getAbsolutePath() ) )
{
file.delete();
}
}
}
finally
{
wakeLockHolder.releaseWakeLock();
}
}
return result;
}
/*
* (non-Javadoc)
*
* @see de.unikassel.android.sdcframework.transmission.facade.
* UpdatableTransmissionComponent#updateConfiguration(android.content.Context,
* de
* .unikassel.android.sdcframework.preferences.facade.UpdatableConfiguration)
*/
@Override
public final synchronized void updateConfiguration( Context context,
TransmissionProtocolConfiguration config )
{
this.connectionStrategy = ConnectionStrategyBuilder.buildStrategy( config );
protocolStrategy = null;
try
{
// determine protocol type
URL url = new URL( config.getURL() );
String protocol = url.getProtocol();
if ( "http".equals( protocol ) )
{
// HTTP the only protocol we do support right now
protocolStrategy = new BasicAuthHttpProtocol( context, uuid, config );
}
}
catch ( MalformedURLException e )
{}
if ( protocolStrategy == null )
{
// unknown protocol
protocolStrategy = new UnknownProtocol( context, uuid, config );
}
}
/**
* Static method to clear any available log file on the external storage media
*/
public final static void clearAllLogs()
{
LogfileManager instance = LogfileManager.getInstance();
File dir = FileUtils.fileFromPath( STORAGE_DIR );
if ( dir.exists() && dir.isDirectory() )
{
for ( File file : dir.listFiles() )
{
if ( instance != null && instance.isConfigured() )
{
instance.transferFiles.offer( file.getAbsolutePath() );
}
else
{
file.delete();
}
}
}
}
/*
* (non-Javadoc)
*
* @see
* de.unikassel.android.sdcframework.util.facade.EventObserver#onEvent(de.
* unikassel.android.sdcframework.util.facade.ObservableEventSource,
* de.unikassel.android.sdcframework.util.facade.ObservableEvent)
*/
@Override
public void onEvent(
ObservableEventSource< ? extends AlarmEvent > eventSource,
AlarmEvent observedEvent )
{
releaseInstance();
}
}