diff --git a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/services/sending/MessagePersistenceManager.java b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/services/sending/MessagePersistenceManager.java index 77c3a57d131..87043f35400 100644 --- a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/services/sending/MessagePersistenceManager.java +++ b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/services/sending/MessagePersistenceManager.java @@ -1,36 +1,41 @@ package com.sap.sailing.android.shared.services.sending; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URLDecoder; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; -import android.content.ContentResolver; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; import android.os.Bundle; import com.sap.sailing.android.shared.logging.ExLog; -import com.sap.sailing.android.shared.provider.AnalyticsContract; +import com.sap.sailing.android.shared.util.FileHandlerUtils; public class MessagePersistenceManager { - + private final static String TAG = MessagePersistenceManager.class.getName(); - protected Context context; - protected ContentResolver resolver; + private final static String delayedMessagesFileName = "delayedMessages.txt"; - private final MessageRestorer restorer; + protected Context context; + protected List persistedMessages; + + private final MessageRestorer messageRestorer; public MessagePersistenceManager(Context context, MessageRestorer messageRestorer) { this.context = context; - this.restorer = messageRestorer; - this.resolver = context.getContentResolver(); + persistedMessages = new ArrayList(); + this.messageRestorer = messageRestorer; + initializeFileAndPersistedMessages(); } public boolean areIntentsDelayed() { - return getMessageCount() != 0; + return !persistedMessages.isEmpty(); } public void persistIntent(Intent intent) { @@ -43,15 +48,23 @@ public class MessagePersistenceManager { } private void persistMessage(String url, String callbackPayload, String payload, String callbackClass) { - ContentValues values = new ContentValues(); - values.put(AnalyticsContract.Message.MESSAGE_URL, url); - values.put(AnalyticsContract.Message.MESSAGE_CALLBACK_PAYLOAD, callbackPayload); - values.put(AnalyticsContract.Message.MESSAGE_PAYLOAD, payload); - values.put(AnalyticsContract.Message.MESSAGE_CALLBACK_CLASS_STRING, callbackClass); - Uri result = resolver.insert(AnalyticsContract.Message.CONTENT_URI, values); - if (result != null) { - ExLog.i(context, TAG, "Message saved."); + String messageLine = getSerializedIntentForPersistence(url, callbackPayload, payload, callbackClass); + ExLog.i(context, TAG, String.format("Persisting message \"%s\".", messageLine)); + if (persistedMessages.contains(messageLine)) { + ExLog.i(context, TAG, "The message already exists. Ignoring."); + return; } + saveMessage(messageLine); + } + + /** + * @param payload will be URL-encoded to ensure that the resulting string does not contain newlines + */ + private String getSerializedIntentForPersistence(String url, String callbackPayload, + String payload, String callbackClass) { + String messageLine = String.format("%s;%s;%s;%s", callbackPayload, URLEncoder.encode(payload), + url, callbackClass); + return messageLine; } public void removeIntent(Intent intent) { @@ -64,81 +77,142 @@ public class MessagePersistenceManager { } private void removeMessage(String url, String callbackPayload, String payload, String callbackClass) { - ExLog.i(context, TAG, String.format("Removing message \"%s\".", payload)); - String where = AnalyticsContract.Message.MESSAGE_URL + " = ? AND " - + AnalyticsContract.Message.MESSAGE_CALLBACK_PAYLOAD + " = ? AND " - + AnalyticsContract.Message.MESSAGE_PAYLOAD + " = ? AND " - + AnalyticsContract.Message.MESSAGE_CALLBACK_CLASS_STRING + " = ?"; - int count = resolver.delete(AnalyticsContract.Message.CONTENT_URI, where, new String[] { url, callbackPayload, - payload, callbackClass }); - if (count != 0) { + if (!persistedMessages.isEmpty()) { + ExLog.i(context, TAG, String.format("Removing message \"%s\".", payload)); + String messageLine = getSerializedIntentForPersistence(url, callbackPayload, payload, callbackClass); + removePersistedMessage(messageLine); + } + } + + /** + * Removes all pending messages and clears the persistence file. + */ + public synchronized void removeAllMessages() { + persistedMessages.clear(); + writePersistedMessagesToFile(); + } + + private void removePersistedMessage(String messageLine) { + if (persistedMessages.contains(messageLine)) { + persistedMessages.remove(messageLine); + writePersistedMessagesToFile(); ExLog.i(context, TAG, "Message removed."); } } - public synchronized void removeAllMessages() { - resolver.delete(AnalyticsContract.Message.CONTENT_URI, null, null); - } - public int getMessageCount() { - int result = 0; - Cursor cursor = resolver.query(AnalyticsContract.Message.CONTENT_URI, null, null, null, null); - if (cursor != null) { - cursor.moveToFirst(); - result = cursor.getCount(); - cursor.close(); - } - return result; + return persistedMessages.size(); } - - public Cursor getContent() { - return resolver.query(AnalyticsContract.Message.CONTENT_URI, null, null, null, null); + + public List getContent() { + return persistedMessages; } - + public static interface MessageRestorer { void restoreMessage(Context context, Intent messageIntent); } public List restoreMessages() { List delayedIntents = new ArrayList(); - Cursor cursor = resolver.query(AnalyticsContract.Message.CONTENT_URI, null, null, null, null); - if (cursor != null) { - for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { - String url = cursor.getString(cursor.getColumnIndex(AnalyticsContract.Message.MESSAGE_URL)); - String callbackPayload = cursor.getString(cursor - .getColumnIndex(AnalyticsContract.Message.MESSAGE_CALLBACK_PAYLOAD)); - String payload = cursor.getString(cursor.getColumnIndex(AnalyticsContract.Message.MESSAGE_PAYLOAD)); - String callbackClassString = cursor.getString(cursor - .getColumnIndex(AnalyticsContract.Message.MESSAGE_CALLBACK_CLASS_STRING)); + for (String persistedMessage : persistedMessages) { + String[] lineParts = persistedMessage.split(";"); + String url = lineParts[2]; + String callbackPayload = lineParts[0]; + String payload = URLDecoder.decode(lineParts[1]); + String callbackClassString = lineParts[3]; - Class callbackClass = null; - if (callbackClassString != null) { - try { - @SuppressWarnings("unchecked") - Class tmp = (Class) Class - .forName(callbackClassString); - callbackClass = tmp; - } catch (ClassNotFoundException e) { - ExLog.e(context, TAG, "Could not find class for callback name: " + callbackClassString); - } - } - - // We are passing no message id, because we know it used to suppress message sending and - // we want this message to be sent. - Intent messageIntent = MessageSendingService.createMessageIntent(context, url, callbackPayload, null, - payload, callbackClass); - - if (restorer != null) { - restorer.restoreMessage(context, messageIntent); - } - - if (messageIntent != null) { - delayedIntents.add(messageIntent); + Class callbackClass = null; + if (! "null".equals(callbackClassString)) { + try { + @SuppressWarnings("unchecked") + Class tmp = + (Class) Class.forName(callbackClassString); + callbackClass = tmp; + } catch (ClassNotFoundException e) { + ExLog.e(context, TAG, "Could not find class for callback name: " + callbackClassString); } } - cursor.close(); + + // We are passing no message id, because we know it used to suppress message sending and + // we want this message to be sent. + Intent messageIntent = MessageSendingService.createMessageIntent(context, url, callbackPayload, + null, payload, callbackClass); + + if (messageRestorer != null) { + messageRestorer.restoreMessage(context, messageIntent); + } + + if (messageIntent != null) { + delayedIntents.add(messageIntent); + } } ExLog.i(context, TAG, "Restored " + delayedIntents.size() + " messages"); return delayedIntents; } + + /** + * @return + * @throws FileNotFoundException + */ + private String getFileContent() throws FileNotFoundException { + String fileContent = ""; + FileInputStream inputStream; + try { + inputStream = context.openFileInput(delayedMessagesFileName); + + fileContent = FileHandlerUtils.convertStreamToString(inputStream, context); + inputStream.close(); + } catch (IOException e) { + ExLog.w(context, TAG, "In Method getFileContent(): " + e.getMessage() + " fileContent is empty"); + } + return fileContent; + } + + private void saveMessage(String messageLine) { + persistedMessages.add(messageLine); + writePersistedMessagesToFile(); + ExLog.i(context, TAG, "Wrote message to file: " + messageLine); + } + + private void writePersistedMessagesToFile() { + StringBuilder newFileContent = new StringBuilder(); + for (String persistedMessage : persistedMessages) { + newFileContent.append(persistedMessage); + newFileContent.append('\n'); + } + writeToFile(newFileContent.toString(), Context.MODE_PRIVATE); + ExLog.i(context, TAG, "Wrote file content to file: " + newFileContent); + } + + private void initializeFileAndPersistedMessages() { + try { + String fileContent = getFileContent(); + String[] messageLines = fileContent.split("\n"); + for (String messageLine : messageLines) { + if (!messageLine.isEmpty()) { + persistedMessages.add(messageLine); + } + } + } catch (FileNotFoundException e) { + ExLog.w(context, TAG, "persistence file not found in internal storage. The file will be created."); + clearPersistedMessages(); + } + ExLog.i(context, TAG, "Initialized file"); + } + + private void clearPersistedMessages() { + persistedMessages.clear(); + writePersistedMessagesToFile(); + } + + private void writeToFile(String content, int mode) { + try { + FileOutputStream outputStream = context.openFileOutput(delayedMessagesFileName, mode); + outputStream.write(content.getBytes()); + outputStream.close(); + } catch (IOException e) { + ExLog.e(context, TAG, "In Method writeToFile: " + e.getMessage() + " with content " + content + " and mode " + mode); + } + } + } diff --git a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/services/sending/MessageSendingService.java b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/services/sending/MessageSendingService.java index 6df851c143e..4c01d0cb326 100644 --- a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/services/sending/MessageSendingService.java +++ b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/services/sending/MessageSendingService.java @@ -15,7 +15,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Binder; @@ -31,34 +30,30 @@ import com.sap.sailing.android.shared.util.PrefUtils; import com.sap.sailing.domain.common.racelog.RaceLogServletConstants; /** - * Service that handles sending messages to a webservice. Deals with an offline setting by buffering the messages in a - * file, so that they can be sent when the connection is re-established.
+ * Service that handles sending messages to a webservice. Deals with an offline setting + * by buffering the messages in a file, so that they can be sent when the connection is + * re-established.
* - * Use in the following way: Add the service declaration to your {@code AndroidManifest.xml}, and also specify - * your class implementing the {@link MessagePersistenceManager.MessageRestorer} as a meta-data tag with the key - * {@code com.sap.sailing.android.shared.services.sending.messageRestorer}. Also refer to - * {@link ConnectivityChangedReceiver}, which has to be registered as well. For example: - * - *
- * {@code
+ * Use in the following way: Add the service declaration to your {@code AndroidManifest.xml},
+ * and also specify your class implementing the {@link MessagePersistenceManager.MessageRestorer}
+ * as a meta-data tag with the key {@code com.sap.sailing.android.shared.services.sending.messageRestorer}.
+ * Also refer to {@link ConnectivityChangedReceiver}, which has to be registered as well.
+ * For example:
+ * 
{@code
  * 
  *   
  * 
- * }
- * 
+ * }
* * - * Message sending example: - * - *
- * {@code
+ * Message sending example: 
+ * 
{@code
  * context.startService(MessageSendingService.createMessageIntent(
  *      context, url, race.getId(), eventId, serializedEventAsJson, callbackClass));
- * }
- * 
+ * }
*/ public class MessageSendingService extends Service implements MessageSendingListener { public final static String URL = "url"; @@ -74,9 +69,9 @@ public class MessageSendingService extends Service implements MessageSendingList private final IBinder mBinder = new MessageSendingBinder(); private MessagePersistenceManager persistenceManager; private boolean isHandlerSet; - + private Set suppressedMessageIds = new HashSet(); - + public void registerMessageForSuppression(Serializable messageId) { suppressedMessageIds.add(messageId); } @@ -113,8 +108,8 @@ public class MessageSendingService extends Service implements MessageSendingList } private Date lastSuccessfulSend; - - public Cursor getDelayedIntensContent() { + + public List getDelayedIntensContent() { return persistenceManager.getContent(); } @@ -125,15 +120,15 @@ public class MessageSendingService extends Service implements MessageSendingList public Date getLastSuccessfulSend() { return lastSuccessfulSend; } - + public void clearDelayedIntents() { persistenceManager.removeAllMessages(); // let's fake a successful send! serviceLogger.onMessageSentSuccessful(); } - public static Intent createMessageIntent(Context context, String url, Serializable callbackPayload, - Serializable messageId, String payload, Class callbackClass) { + public static Intent createMessageIntent(Context context, String url, Serializable callbackPayload, Serializable messageId, String payload, + Class callbackClass) { Intent messageIntent = new Intent(context, MessageSendingService.class); messageIntent.setAction(context.getString(R.string.intent_send_message)); messageIntent.putExtra(CALLBACK_PAYLOAD, callbackPayload); @@ -143,7 +138,7 @@ public class MessageSendingService extends Service implements MessageSendingList messageIntent.putExtra(CALLBACK_CLASS, callbackClass == null ? null : callbackClass.getName()); return messageIntent; } - + public static Intent createSendDelayedIntent(Context context) { Intent intent = new Intent(context, MessageSendingService.class); intent.setAction(context.getString(R.string.intent_send_saved_intents)); @@ -158,7 +153,7 @@ public class MessageSendingService extends Service implements MessageSendingList ExLog.w(this, TAG, "Unable to extract message identifier from message intent."); return null; } - + private MessagePersistenceManager getPersistenceManager() { ComponentName thisService = new ComponentName(this, this.getClass()); MessageRestorer restorer = null; @@ -166,11 +161,10 @@ public class MessageSendingService extends Service implements MessageSendingList Bundle data = getPackageManager().getServiceInfo(thisService, PackageManager.GET_META_DATA).metaData; String className = data.getString("com.sap.sailing.android.shared.services.sending.messageRestorer"); Class clazz = Class.forName(className); - if (!MessageRestorer.class.isAssignableFrom(clazz)) { + if (! MessageRestorer.class.isAssignableFrom(clazz)) { throw new Exception("Class does not conform to expected type."); } - @SuppressWarnings("unchecked") - // checked above + @SuppressWarnings("unchecked") //checked above Class castedClass = (Class) clazz; restorer = castedClass.getConstructor().newInstance(); } catch (Exception e) { @@ -253,7 +247,7 @@ public class MessageSendingService extends Service implements MessageSendingList private void sendMessage(Intent intent) { boolean sendingActive = PrefUtils.getBoolean(this, R.string.preference_isSendingActive_key, R.bool.preference_isSendingActive_default); - if (!sendingActive) { + if (! sendingActive) { ExLog.i(this, TAG, "Sending deactivated. Message will not be sent to server."); } else { Serializable messageId = getMessageId(intent); @@ -287,7 +281,7 @@ public class MessageSendingService extends Service implements MessageSendingList } lastSuccessfulSend = Calendar.getInstance().getTime(); serviceLogger.onMessageSentSuccessful(); - + String callbackClassString = intent.getStringExtra(CALLBACK_CLASS); ServerReplyCallback callback = null; if (callbackClassString != null) { @@ -317,21 +311,22 @@ public class MessageSendingService extends Service implements MessageSendingList } return activeNetwork.isConnected(); } - + /** - * a UUID that identifies this client session; can be used, e.g., to let the server identify subsequent requests - * coming from the same client + * a UUID that identifies this client session; can be used, e.g., to let the server identify subsequent requests coming from the same client */ public final static UUID uuid = UUID.randomUUID(); - + public static String getRaceLogEventSendAndReceiveUrl(Context context, final String raceGroupName, final String raceName, final String fleetName) { - String url = String.format("%s/sailingserver/rc/racelog?" + RaceLogServletConstants.PARAMS_LEADERBOARD_NAME - + "=%s&" + RaceLogServletConstants.PARAMS_RACE_COLUMN_NAME + "=%s&" - + RaceLogServletConstants.PARAMS_RACE_FLEET_NAME + "=%s&" + RaceLogServletConstants.PARAMS_CLIENT_UUID - + "=%s", PrefUtils.getString(context, R.string.preference_server_url_key, - R.string.preference_server_url_default), URLEncoder.encode(raceGroupName), URLEncoder.encode(raceName), - URLEncoder.encode(fleetName), uuid); + String url = String.format("%s/sailingserver/rc/racelog?"+ + RaceLogServletConstants.PARAMS_LEADERBOARD_NAME+"=%s&"+ + RaceLogServletConstants.PARAMS_RACE_COLUMN_NAME+"=%s&"+ + RaceLogServletConstants.PARAMS_RACE_FLEET_NAME+"=%s&"+ + RaceLogServletConstants.PARAMS_CLIENT_UUID+"=%s", + PrefUtils.getString(context, R.string.preference_server_url_key, R.string.preference_server_url_default), + URLEncoder.encode(raceGroupName), + URLEncoder.encode(raceName), URLEncoder.encode(fleetName), uuid); return url; } }