/* * The MIT License (MIT) * * Copyright (c) 2013 Curt Binder * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package info.curtbinder.reefangel.service; import info.curtbinder.reefangel.controller.Controller; import info.curtbinder.reefangel.db.ErrorTable; import info.curtbinder.reefangel.db.NotificationTable; import info.curtbinder.reefangel.db.StatusProvider; import info.curtbinder.reefangel.db.StatusTable; import info.curtbinder.reefangel.phone.Globals; import info.curtbinder.reefangel.phone.Permissions; import info.curtbinder.reefangel.phone.R; import info.curtbinder.reefangel.phone.RAApplication; import info.curtbinder.reefangel.phone.MainActivity; import java.util.Locale; import android.app.IntentService; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Build; import android.support.v4.app.NotificationCompat; // TODO update notification service public class NotificationService extends IntentService { private static final String TAG = NotificationService.class.getSimpleName(); private static RAApplication rapp; private String errorMessage; private String paramPrecision; private String[] parameters; public NotificationService () { super( TAG ); } @Override protected void onHandleIntent ( Intent intent ) { rapp = (RAApplication) getApplication(); String action = intent.getAction(); if ( action.equals( MessageCommands.NOTIFICATION_CLEAR_INTENT ) ) { clearNotifications(); } else if ( action.equals( MessageCommands.NOTIFICATION_LAUNCH_INTENT ) ) { clearNotifications(); Intent i = getStatusActivity(); getApplication().startActivity( i ); } else if ( action.equals( MessageCommands.NOTIFICATION_INTENT ) ) { processNotifications(); } else if ( action.equals( MessageCommands.NOTIFICATION_ERROR_INTENT ) ) { processError(); } } private void processError ( ) { boolean fDisplayUpdate = true; if ( rapp.raprefs.isNotificationEnabled() ) { // Only proceed if notifications are enabled // check if can retry on errors if ( rapp.raprefs.isErrorRetryEnabled() ) { // increase error count as soon as we know it's an error rapp.increaseErrorCount(); // we are to retry connection before displaying an error if ( rapp.canErrorRetry() ) { // if error count is less than the max, // we need to retry the communication String s = rapp.getString( R.string.messageErrorRetry, rapp.errorCount ); broadcastUpdateStatus( s ); fDisplayUpdate = false; try { Thread.sleep( rapp.raprefs .getNotificationErrorRetryInterval() ); } catch ( InterruptedException e ) { } Intent i = new Intent( rapp, UpdateService.class ); i.setAction( MessageCommands.QUERY_STATUS_INTENT ); startService( i ); } // otherwise if we have exceeded the max count, then we // display the error } } if ( fDisplayUpdate ) { // log the error in the error table String errorMessage = rapp.getErrorMessage(); insertErrorMessage( errorMessage ); // notify the user notifyUser(); // update the app text broadcastUpdateStatus( errorMessage ); } } private void broadcastUpdateStatus ( String status ) { Intent i = new Intent( MessageCommands.UPDATE_STATUS_INTENT ); i.putExtra( MessageCommands.UPDATE_STATUS_ID, -1 ); i.putExtra( MessageCommands.UPDATE_STATUS_STRING, status ); rapp.sendBroadcast( i, Permissions.QUERY_STATUS ); } private void processNotifications ( ) { Uri uri = Uri.parse( StatusProvider.CONTENT_URI + "/" + StatusProvider.PATH_NOTIFICATION ); // get all notifications Cursor c = getContentResolver().query( uri, null, null, null, NotificationTable.COL_ID + " ASC" ); int notifyCount = 0; if ( c.moveToFirst() ) { int param = 0, cond = 0; float value = (float) 0.0; parameters = rapp.getResources() .getStringArray( R.array.deviceParameters ); // grab the latest parameters to compare against Uri uriLatest = Uri.parse( StatusProvider.CONTENT_URI + "/" + StatusProvider.PATH_LATEST ); // get all notifications Cursor l = getContentResolver().query( uriLatest, null, null, null, StatusTable.COL_ID + " DESC" ); l.moveToFirst(); do { param = c.getInt( c .getColumnIndex( NotificationTable.COL_PARAM ) ); cond = c.getInt( c .getColumnIndex( NotificationTable.COL_CONDITION ) ); value = c.getFloat( c .getColumnIndex( NotificationTable.COL_VALUE ) ); errorMessage = ""; if ( isNotifyTriggered( param, cond, value, l ) ) { // notification triggered // add to list of notifications insertErrorMessage( errorMessage ); notifyCount++; } } while ( c.moveToNext() ); l.close(); } // else no notifications c.close(); // launch the notification after we process the messages if ( notifyCount > 0 ) { notifyUser(); } } private boolean isNotifyTriggered ( int param, int cond, float rvalue, Cursor latest ) { boolean fRet = false; float lvalue = getLeftValue( param, latest ); String condLabel = ""; switch ( cond ) { case Globals.condEqual: { if ( lvalue == rvalue ) { condLabel = "="; fRet = true; } break; } case Globals.condGreaterThan: { if ( lvalue > rvalue ) { condLabel = ">"; fRet = true; } break; } case Globals.condGreaterThanOrEqualTo: { if ( lvalue >= rvalue ) { condLabel = ">="; fRet = true; } break; } case Globals.condLessThan: { if ( lvalue < rvalue ) { condLabel = "<"; fRet = true; } break; } case Globals.condLessThanOrEqualTo: { if ( lvalue <= rvalue ) { condLabel = "<="; fRet = true; } break; } case Globals.condNotEqual: { if ( lvalue != rvalue ) { condLabel = "!="; fRet = true; } break; } } if ( fRet ) { String formatString = String.format( Locale.US, "%%s: %s %s %s", paramPrecision, condLabel, paramPrecision ); errorMessage = String.format( Locale.US, formatString, parameters[param], lvalue, rvalue ); } return fRet; } // TODO add in 16ch dimming values private float getLeftValue ( int id, Cursor l ) { float f; paramPrecision = "%.0f"; switch ( id ) { case Globals.paramT1: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_T1 ) ); paramPrecision = "%.1f"; break; } case Globals.paramT2: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_T2 ) ); paramPrecision = "%.1f"; break; } case Globals.paramT3: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_T3 ) ); paramPrecision = "%.1f"; break; } case Globals.paramPH: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_PH ) ); paramPrecision = "%.2f"; break; } case Globals.paramPHExpansion: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_PHE ) ); paramPrecision = "%.2f"; break; } case Globals.paramDaylightPWM: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_DP ) ); break; } case Globals.paramActinicPWM: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_AP ) ); break; } case Globals.paramSalinity: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_SAL ) ); paramPrecision = "%.1f"; break; } case Globals.paramORP: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_ORP ) ); break; } case Globals.paramWaterLevel: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_WL ) ); break; } case Globals.paramATOHigh: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_ATOHI ) ); break; } case Globals.paramATOLow: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_ATOLO ) ); break; } case Globals.paramPWMExp0: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_PWME0 ) ); break; } case Globals.paramPWMExp1: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_PWME1 ) ); break; } case Globals.paramPWMExp2: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_PWME2 ) ); break; } case Globals.paramPWMExp3: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_PWME3 ) ); break; } case Globals.paramPWMExp4: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_PWME4 ) ); break; } case Globals.paramPWMExp5: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_PWME5 ) ); break; } case Globals.paramAIWhite: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_AIW ) ); break; } case Globals.paramAIBlue: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_AIB ) ); break; } case Globals.paramAIRoyalBlue: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_AIRB ) ); break; } case Globals.paramVortechMode: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_RFM ) ); break; } case Globals.paramVortechSpeed: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_RFS ) ); break; } case Globals.paramVortechDuration: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_RFD ) ); break; } case Globals.paramRadionWhite: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_RFW ) ); break; } case Globals.paramRadionRoyalBlue: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_RFRB ) ); break; } case Globals.paramRadionRed: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_RFR ) ); break; } case Globals.paramRadionGreen: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_RFG ) ); break; } case Globals.paramRadionBlue: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_RFB ) ); break; } case Globals.paramRadionIntensity: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_RFI ) ); break; } case Globals.paramIOCh0: case Globals.paramIOCh1: case Globals.paramIOCh2: case Globals.paramIOCh3: case Globals.paramIOCh4: case Globals.paramIOCh5: { short io = l.getShort( l.getColumnIndex( StatusTable.COL_IO ) ); byte ch = (byte) (id - Globals.paramIOCh0); // getIOChannel returns TRUE if the value is 1 // and FALSE if the value is 0 if ( Controller.getIOChannel( io, ch ) ) { f = 1; } else { f = 0; } break; } case Globals.paramCustom0: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_C0 ) ); break; } case Globals.paramCustom1: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_C1 ) ); break; } case Globals.paramCustom2: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_C2 ) ); break; } case Globals.paramCustom3: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_C3 ) ); break; } case Globals.paramCustom4: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_C4 ) ); break; } case Globals.paramCustom5: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_C5 ) ); break; } case Globals.paramCustom6: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_C6 ) ); break; } case Globals.paramCustom7: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_C7 ) ); break; } case Globals.paramWaterLevel1: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_WL1 ) ); break; } case Globals.paramWaterLevel2: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_WL2 ) ); break; } case Globals.paramWaterLevel3: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_WL3 ) ); break; } case Globals.paramWaterLevel4: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_WL4 ) ); break; } case Globals.paramHumidity: { f = l.getFloat( l.getColumnIndex( StatusTable.COL_HUM ) ); break; } default: { f = 0; break; } } return f; } private PendingIntent getNotificationLaunchIntent ( boolean fClearOnly ) { // Create notification intent // Will launch the service to clear the notifications // and launch the main activity unless clear only is set Intent i = new Intent( this, NotificationService.class ); if ( fClearOnly ) { i.setAction( MessageCommands.NOTIFICATION_CLEAR_INTENT ); } else { i.setAction( MessageCommands.NOTIFICATION_LAUNCH_INTENT ); } PendingIntent pi = PendingIntent.getService( this, -1, i, 0 ); return pi; } private NotificationCompat.Builder buildNormalNotification ( String msg, long when, int count ) { Bitmap icon = BitmapFactory.decodeResource( getResources(), R.drawable.ic_launcher ); NotificationCompat.Builder b = new NotificationCompat.Builder( this ) .setAutoCancel( true ) .setSmallIcon( R.drawable.st_notify ) .setLargeIcon( icon ) .setContentTitle( getString( R.string.app_name ) ) .setContentText( msg ) .setTicker( msg ) .setWhen( when ) .setSound( rapp.raprefs.getNotificationSound() ) .setDeleteIntent( getNotificationLaunchIntent( true ) ) .setContentIntent( getNotificationLaunchIntent( false ) ); if ( count > 1 ) { b.setNumber( count ); if ( Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1 ) { String msgGB = String.format( Locale.US, getString( R.string.messageGBMoreErrors ), msg, count ); b.setContentText( msgGB ); } } return b; } private String getInboxStyleMessage ( String msg, long when ) { String extraMessage = String.format( Locale.getDefault(), "%s - %s", msg, RAApplication.getFancyDate( when ) ); return extraMessage; } public void notifyUser ( ) { Uri uri = Uri.parse( StatusProvider.CONTENT_URI + "/" + StatusProvider.PATH_ERROR ); Cursor c = getContentResolver().query( uri, null, ErrorTable.COL_READ + "=?", new String[] { "0" }, ErrorTable.COL_ID + " DESC" ); String firstMessage = null; long firstWhen = 0; int numCount = 0; String[] summaryLines = new String[5]; String summaryText = ""; if ( c.moveToFirst() ) { int msgIndex = c.getColumnIndex( ErrorTable.COL_MESSAGE ); int whenIndex = c.getColumnIndex( ErrorTable.COL_TIME ); // grab the most recent error first firstMessage = c.getString( msgIndex ); firstWhen = c.getLong( whenIndex ); numCount = c.getCount(); // handle looping through the rest of the messages // in order to create the big notification // InboxStyle only allows for up to 5 lines int extraCount = 1; summaryLines[0] = getInboxStyleMessage( firstMessage, firstWhen ); while ( c.moveToNext() && extraCount < 5 ) { summaryLines[extraCount] = getInboxStyleMessage( c.getString( msgIndex ), c.getLong( whenIndex ) ); extraCount++; } // when multiple items shown, the first item is the content title // so a max of items can be shown (content title plus 5 extra // lines) if ( extraCount < numCount ) { summaryText = String.format( Locale.US, getString( R.string.messageMoreErrors ), numCount - extraCount ); } } c.close(); NotificationManager nm = (NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE ); int mNotificationId = 001; NotificationCompat.Builder normal = buildNormalNotification( firstMessage, firstWhen, numCount ); if ( numCount > 1 ) { NotificationCompat.InboxStyle inbox = new NotificationCompat.InboxStyle( normal ); // inbox.setBigContentTitle( firstMessage ); for ( String s : summaryLines ) { inbox.addLine( s ); } inbox.setSummaryText( summaryText ); nm.notify( mNotificationId, inbox.build() ); } else { nm.notify( mNotificationId, normal.build() ); } } public void insertErrorMessage ( String msg ) { // inserts the given error message into the database // message is the parameter for expandability with notifications ContentValues v = new ContentValues(); v.put( ErrorTable.COL_TIME, System.currentTimeMillis() ); v.put( ErrorTable.COL_MESSAGE, msg ); v.put( ErrorTable.COL_READ, false ); getContentResolver() .insert( Uri.parse( StatusProvider.CONTENT_URI + "/" + StatusProvider.PATH_ERROR ), v ); } private Intent getStatusActivity ( ) { Intent si = new Intent( this, MainActivity.class ); si.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK ); si.addFlags( Intent.FLAG_ACTIVITY_SINGLE_TOP ); si.addFlags( Intent.FLAG_ACTIVITY_REORDER_TO_FRONT ); return si; } private void clearNotifications ( ) { Uri uri = Uri.parse( StatusProvider.CONTENT_URI + "/" + StatusProvider.PATH_ERROR ); ContentValues v = new ContentValues(); v.put( ErrorTable.COL_READ, true ); // ignore the number of rows updated getContentResolver().update( uri, v, ErrorTable.COL_READ + "=?", new String[] { "0" } ); } }