package org.bitseal.services; import info.guardianproject.cacheword.CacheWordHandler; import info.guardianproject.cacheword.ICacheWordSubscriber; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import org.bitseal.R; import org.bitseal.controllers.TaskController; import org.bitseal.core.App; import org.bitseal.core.ObjectProcessor; import org.bitseal.core.QueueRecordProcessor; import org.bitseal.data.Address; import org.bitseal.data.Message; import org.bitseal.data.Payload; import org.bitseal.data.Pubkey; import org.bitseal.data.QueueRecord; import org.bitseal.database.AddressProvider; import org.bitseal.database.DatabaseContentProvider; import org.bitseal.database.MessageProvider; import org.bitseal.database.PayloadProvider; import org.bitseal.database.PayloadsTable; import org.bitseal.database.PubkeyProvider; import org.bitseal.database.PubkeysTable; import org.bitseal.database.QueueRecordProvider; import org.bitseal.database.QueueRecordsTable; import org.bitseal.network.NetworkHelper; import org.bitseal.util.ByteUtils; import org.bitseal.util.TimeUtils; import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.SystemClock; import android.preference.PreferenceManager; import android.util.Base64; import android.util.Log; import com.commonsware.cwac.wakeful.WakefulIntentService; /** * This class handles all the long-running processing required * by the application. * * @author Jonathan Coe */ public class BackgroundService extends WakefulIntentService implements ICacheWordSubscriber { /** * This constant determines whether or not the app will do * proof of work for pubkeys and messages that it creates. * If not, it will expect servers to do the proof of work. */ public static final boolean DO_POW = true; /** * The 'time to live' value (in seconds) that we use in the 'first attempt' to create * and send some types of objects (such as msgs sent by us). This is done * because in protocol version 3, objects with a lower time to live require less * proof of work for the network to relay them. <br><br> * * Therefore in some situations it is advantageous to use a low time to live * when creating and sending an object, for example when you are sending a * msg and the recipient is online and therefore able to receive and acknowledge * it immediately. */ public static final long FIRST_ATTEMPT_TTL = 3600; // Currently set to 1 hour /** * The 'time to live' value (in seconds) that we use in all attempts after the * first attempt to create and send some types of objects (such as msgs sent * by us). <br><br> * * If we create and send out an object using a low time to live and the first attempt is * not successful (e.g. we do not receive an acknowledgement for a sent msg) then we can * re-create and re-send the object with a longer time to live. That time to live is * determined by this constant. */ public static final long SUBSEQUENT_ATTEMPTS_TTL = 86400; // Currently set to 1 day /** * The minimum amount of time (in seconds) which a Bitmessage Object we are going to send out * must have until its expiration time. If there is less than this much time between now and * the Object's expiration time, we will discard it and create a new Object with an updated * time to live and new proof of work. */ public static final long MINIMUM_TIME_TO_LIVE = 120; // Currently set to 2 minutes /** * This 'maximum attempts' constant determines the number of times * that a task will be attempted before it is abandoned and deleted * from the queue. */ public static final int MAXIMUM_ATTEMPTS = 500; /** Determines how often the database cleaning routine should be run, in seconds. */ private static final long TIME_BETWEEN_DATABASE_CLEANING = 3600; // Constants to identify requests from the UI public static final String UI_REQUEST = "uiRequest"; public static final String UI_REQUEST_SEND_MESSAGE = "sendMessage"; public static final String UI_REQUEST_CREATE_IDENTITY = "createIdentity"; // Used when broadcasting Intents to the UI so that it can refresh the data it is displaying public static final String UI_NOTIFICATION = "uiNotification"; // Constants that identify request for periodic background processing public static final String PERIODIC_BACKGROUND_PROCESSING_REQUEST = "periodicBackgroundProcessingReqest"; public static final String BACKGROUND_PROCESSING_REQUEST = "doBackgroundProcessing"; // Constants to identify data sent to this Service from the UI public static final String MESSAGE_ID = "messageId"; public static final String ADDRESS_ID = "addressId"; // The tasks for performing the first major function of the application: creating a new identity public static final String TASK_CREATE_IDENTITY = "createIdentity"; public static final String TASK_DISSEMINATE_PUBKEY = "disseminatePubkey"; // The tasks for performing the second major function of the application: sending messages public static final String TASK_SEND_MESSAGE = "sendMessage"; public static final String TASK_PROCESS_OUTGOING_MESSAGE = "processOutgoingMessage"; public static final String TASK_DISSEMINATE_MESSAGE = "disseminateMessage"; private CacheWordHandler mCacheWordHandler; private static final String TAG = "BACKGROUND_SERVICE"; public BackgroundService() { super("BackgroundService"); // Set up the uncaught exception handler for this thread Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler()); } /** * Handles requests sent to the BackgroundService via Intents * * @param i - An Intent object that has been received by the * BackgroundService */ @SuppressLint("InlinedApi") @Override protected void doWakefulWork(Intent i) { try { Log.i(TAG, "BackgroundService.doWakefulWork() called"); // Connect to the CacheWordService and check whether it is locked mCacheWordHandler = new CacheWordHandler(this); mCacheWordHandler.connectToService(); SystemClock.sleep(5000); // We need to allow some extra time to connect to the CacheWordService if (mCacheWordHandler.isLocked()) { scheduleRestart(); closeDatabaseIfLocked(); return; } // Determine whether the intent came from a request for periodic // background processing or from a UI request if (i.hasExtra(PERIODIC_BACKGROUND_PROCESSING_REQUEST)) { processTasks(); } else if (i.hasExtra(UI_REQUEST)) { String uiRequest = i.getStringExtra(UI_REQUEST); TaskController taskController = new TaskController(); if (uiRequest.equals(UI_REQUEST_SEND_MESSAGE)) { Log.i(TAG, "Responding to UI request to run the 'send message' task"); // Get the ID of the Message object from the intent Bundle extras = i.getExtras(); long messageID = extras.getLong(MESSAGE_ID); // Attempt to retrieve the Message from the database. If it has been deleted by the user // then we should abort the sending process. Message messageToSend = null; try { MessageProvider msgProv = MessageProvider.get(getApplicationContext()); messageToSend = msgProv.searchForSingleRecord(messageID); } catch (RuntimeException e) { Log.i(TAG, "While running BackgroundService.onHandleIntent() and attempting to process a UI request of type\n" + UI_REQUEST_SEND_MESSAGE + ", the attempt to retrieve the Message object from the database failed.\n" + "The message sending process will therefore be aborted."); return; } // Create a new QueueRecord for the 'send message' task and save it to the database QueueRecordProcessor queueProc = new QueueRecordProcessor(); QueueRecord queueRecord = queueProc.createAndSaveQueueRecord(TASK_SEND_MESSAGE, TimeUtils.getUnixTime(), 0, messageToSend, null, null); // Also create a new QueueRecord for re-sending this msg in the event that we do not receive an acknowledgement for it // before its time to live expires. If we do receive the acknowledgement before then, this QueueRecord will be deleted long currentTime = System.currentTimeMillis() / 1000; queueProc.createAndSaveQueueRecord(TASK_SEND_MESSAGE, currentTime + FIRST_ATTEMPT_TTL, 1, messageToSend, null, null); // Attempt to send the message taskController.sendMessage(queueRecord, messageToSend, DO_POW, FIRST_ATTEMPT_TTL, FIRST_ATTEMPT_TTL); } else if (uiRequest.equals(UI_REQUEST_CREATE_IDENTITY)) { Log.i(TAG, "Responding to UI request to run the 'create new identity' task"); // Get the ID of the Address object from the intent Bundle extras = i.getExtras(); long addressId = extras.getLong(ADDRESS_ID); // Attempt to retrieve the Address from the database. If it has been deleted by the user // then we should abort the sending process. Address address = null; try { AddressProvider addProv = AddressProvider.get(getApplicationContext()); address = addProv.searchForSingleRecord(addressId); } catch (RuntimeException e) { Log.i(TAG, "While running BackgroundService.onHandleIntent() and attempting to process a UI request of type\n" + UI_REQUEST_CREATE_IDENTITY + ", the attempt to retrieve the Address object from the database failed.\n" + "The identity creation process will therefore be aborted."); return; } // Create a new QueueRecord for the create identity task and save it to the database QueueRecordProcessor queueProc = new QueueRecordProcessor(); QueueRecord queueRecord = queueProc.createAndSaveQueueRecord(TASK_CREATE_IDENTITY, TimeUtils.getUnixTime(), 0, address, null, null); // Attempt to complete the create identity task taskController.createIdentity(queueRecord, DO_POW); } } else { Log.e(TAG, "BackgroundService.onHandleIntent() was called without a valid extra to specify what the service should do."); } scheduleRestart(); closeDatabaseIfLocked(); } catch (Exception e) { Log.e(TAG, "Exception occurred in BackgroundService.doWakefulWork(). The exception message was:\n" + e.getMessage()); scheduleRestart(); closeDatabaseIfLocked(); } } /** * Schedules a restart of the BackgroundService */ private void scheduleRestart() { WakefulIntentService.scheduleAlarms(new AlarmScheduler(), getApplicationContext(), true); } /** * Checks whether CacheWord is locked. If yes, this routine closes * the database. */ private void closeDatabaseIfLocked() { if (mCacheWordHandler != null && mCacheWordHandler.isLocked()) { DatabaseContentProvider.closeDatabase(); } } /** * Runs periodic background processing. <br><br> * * This method will first check whether there are any QueueRecord objects saved * in the database. If there are, it will attempt to complete the task recorded * by each of those QueueRecords in turn. After that, it will run the 'check for * messages' task. If no QueueRecords are found in the database, it will run the * 'check for messages' task. */ private void processTasks() { Log.i(TAG, "BackgroundService.processTasks() called"); TaskController taskController = new TaskController(); // Check the database TaskQueue table for any queued tasks QueueRecordProvider queueProv = QueueRecordProvider.get(getApplicationContext()); QueueRecordProcessor queueProc = new QueueRecordProcessor(); ArrayList<QueueRecord> queueRecords = queueProv.getAllQueueRecords(); Log.i(TAG, "Number of QueueRecords found: " + queueRecords.size()); if (queueRecords.size() > 0) { // Sort the queue records so that we will process the records with the earliest 'last attempt time' first Collections.sort(queueRecords); // Process each queued task in turn, removing them from the database if completed successfully for (QueueRecord q : queueRecords) { try { Log.i(TAG, "Found a QueueRecord with ID " + q.getId() + ", task " + q.getTask() + ", and number of attempts " + q.getAttempts()); // First check how many times the task recorded by this QueueRecord has been attempted. // If it has been attempted a very high number of times (all without success) then we // will delete it. int attempts = q.getAttempts(); String task = q.getTask(); if (attempts > MAXIMUM_ATTEMPTS) { Log.d(TAG, "Deleting a QueueRecord for a task of type " + task + " because it has been attempted " + attempts + " times without success."); if (task.equals(TASK_SEND_MESSAGE)) { // Update the status of the Message we were trying to send to indicate that sending has failed MessageProvider msgProv = MessageProvider.get(getApplicationContext()); Message messageToSend = msgProv.searchForSingleRecord(q.getObject0Id()); String messageStatus = App.getContext().getString(R.string.message_status_sending_failed); MessageStatusHandler.updateMessageStatus(messageToSend, messageStatus); } queueProc.deleteQueueRecord(q); continue; } else if (task.equals(TASK_SEND_MESSAGE)) { // Attempt to retrieve the Message from the database. If it has been deleted by the user // then we should delete this QueueRecord and abort the sending process. Message messageToSend = null; try { MessageProvider msgProv = MessageProvider.get(getApplicationContext()); messageToSend = msgProv.searchForSingleRecord(q.getObject0Id()); } catch (RuntimeException e) { Log.i(TAG, "While running BackgroundService.processTasks() and attempting to process a task of type\n" + TASK_SEND_MESSAGE + ", the attempt to retrieve the Message object from the database failed.\n" + "The message sending process will therefore be aborted."); queueProv.deleteQueueRecord(q); continue; } // Check whether there are any existing QueueRecords which should be processed before this one. // If there are, this method will push the trigger time of this QueueRecord further into the future and // any duplicates will be deleted. if (checkAndAdjustQueueRecords(q)) { Log.i(TAG, "Ignoring QueueRecord with ID " + q.getId() + " and task " + q.getTask() + " because there is another QueueRecord for " + "the same task which should be processed first."); continue; } // Ignore any QueueRecords that have a 'trigger time' in the future long currentTime = System.currentTimeMillis() / 1000; if (q.getTriggerTime() > currentTime) { Log.i(TAG, "Ignoring a QueueRecord for a " + q.getTask() + " task because its trigger time has not been reached yet. " + "Its trigger time will be reached in roughly " + TimeUtils.getTimeMessage(q.getTriggerTime() - currentTime) + "."); continue; } // Work out which TTL value we should use, then attempt to send the message if (q.getRecordCount() == 0) // This is the first attempt to send this message, so use the 'first attempt' TTL value { // Attempt to send the message taskController.sendMessage(q, messageToSend, DO_POW, FIRST_ATTEMPT_TTL, FIRST_ATTEMPT_TTL); } else // This is not the first attempt to send this message, so use the 'subsequent attempts' TTL value { // Unless we have already done so, we need to create a new QueueRecord for re-sending this msg in the event that we do not receive // an acknowledgement for it before its time to live expires. If we do receive the acknowledgement before then, this // QueueRecord will be deleted. if (checkForMatchingSendMsgQueueRecords(q) == false) { Log.i(TAG, "Creating a QueueRecord to re-send message with ID " + messageToSend.getId()); currentTime = System.currentTimeMillis() / 1000; queueProc.createAndSaveQueueRecord(TASK_SEND_MESSAGE, currentTime + SUBSEQUENT_ATTEMPTS_TTL, q.getRecordCount() + 1, messageToSend, null, null); } // Attempt to send the message taskController.sendMessage(q, messageToSend, DO_POW, SUBSEQUENT_ATTEMPTS_TTL, SUBSEQUENT_ATTEMPTS_TTL); } } else if (task.equals(TASK_PROCESS_OUTGOING_MESSAGE)) { // Attempt to retrieve the Message from the database. If it has been deleted by the user // then we should abort the sending process. Message messageToSend = null; try { MessageProvider msgProv = MessageProvider.get(getApplicationContext()); messageToSend = msgProv.searchForSingleRecord(q.getObject0Id()); } catch (RuntimeException e) { Log.i(TAG, "While running BackgroundService.processTasks() and attempting to process a task of type\n" + TASK_PROCESS_OUTGOING_MESSAGE + ", the attempt to retrieve the Message object from the database failed.\n" + "The message sending process will therefore be aborted."); queueProv.deleteQueueRecord(q); continue; } // Now retrieve the pubkey for the address we are sending the message to PubkeyProvider pubProv = PubkeyProvider.get(App.getContext()); Pubkey toPubkey = pubProv.searchForSingleRecord(q.getObject1Id()); // Attempt to process and send the message if (q.getRecordCount() == 0) { taskController.processOutgoingMessage(q, messageToSend, toPubkey, DO_POW, FIRST_ATTEMPT_TTL); } else { taskController.processOutgoingMessage(q, messageToSend, toPubkey, DO_POW, SUBSEQUENT_ATTEMPTS_TTL); } } else if (task.equals(TASK_DISSEMINATE_MESSAGE)) { // Check whether the msg payload is still valid (its time to live pay have expired) PayloadProvider payProv = PayloadProvider.get(getApplicationContext()); Payload msgPayload = payProv.searchForSingleRecord(q.getObject1Id()); boolean msgValid = new ObjectProcessor().validateObject(msgPayload.getPayload()); if (msgValid == false) { Log.d(TAG, "Found a QueueRecord for a 'disseminate message' task with a msg payload which is due to expire soon.\n" + "We will now delete this QueueRecord and msg and create a new 'process outgoing message' QueueRecord."); // Delete the msg Payload from the database payProv.deletePayload(msgPayload); // Delete this QueueRecord from the database queueProv.deleteQueueRecord(q); // Retrieve the original Message that we are sending MessageProvider msgProv = MessageProvider.get(getApplicationContext()); Message messageToSend = msgProv.searchForSingleRecord(q.getObject0Id()); // Retrieve the pubkey for the address we are sending the message to PubkeyProvider pubProv = PubkeyProvider.get(App.getContext()); Pubkey toPubkey = pubProv.searchForSingleRecord(q.getObject2Id()); // Create a new QueueRecord for the 'process outgoing message' task. This will give us a new // msg with an updated expiration time and proof of work queueProc.createAndSaveQueueRecord(BackgroundService.TASK_PROCESS_OUTGOING_MESSAGE, TimeUtils.getUnixTime(), q.getRecordCount(), messageToSend, toPubkey, null); // Move on to the next QueueRecord continue; } // Check whether an Internet connection is available. If not, move on to the next QueueRecord if (NetworkHelper.checkInternetAvailability() == true) { // Retrieve the pubkey for the address we are sending the message to PubkeyProvider pubProv = PubkeyProvider.get(App.getContext()); Pubkey toPubkey = pubProv.searchForSingleRecord(q.getObject2Id()); // Attempt to send the msg taskController.disseminateMessage(q, msgPayload, toPubkey, DO_POW); } else { MessageProvider messageProv = MessageProvider.get(getApplicationContext()); Message messageToSend = messageProv.searchForSingleRecord(q.getObject0Id()); MessageStatusHandler.updateMessageStatus(messageToSend, getApplicationContext().getString(R.string.message_status_waiting_for_connection)); } } else if (task.equals(TASK_CREATE_IDENTITY)) { taskController.createIdentity(q, DO_POW); } else if (task.equals(TASK_DISSEMINATE_PUBKEY)) { // Check whether the pubkey payload is still valid (its time to live may have expired) PayloadProvider payProv = PayloadProvider.get(getApplicationContext()); Payload pubkeyPayload = payProv.searchForSingleRecord(q.getObject0Id()); boolean pubkeyValid = new ObjectProcessor().validateObject(pubkeyPayload.getPayload()); if (pubkeyValid) { // Check whether an Internet connection is available. If not, move on to the next QueueRecord if (NetworkHelper.checkInternetAvailability() == true) { // Attempt to disseminate the pubkey payload taskController.disseminatePubkey(q, pubkeyPayload, DO_POW); } } else { Log.d(TAG, "Found a QueueRecord for a 'disseminate pubkey' task with a pubkey payload which has expired or is invalid.\n" + "We will now delete this QueueRecord and pubkey and create a new 'create identity' QueueRecord."); // Delete this QueueRecord from the database queueProv.deleteQueueRecord(q); // Retrieve the original address for which we are trying to create and disseminate a pubkey AddressProvider addProv = AddressProvider.get(getApplicationContext()); Address address = addProv.searchForSingleRecord(pubkeyPayload.getRelatedAddressId()); // Create a new QueueRecord for the 'create identity' task. This will give us a new // pubkey with an updated expiration time and proof of work queueProc.createAndSaveQueueRecord(TASK_CREATE_IDENTITY, TimeUtils.getUnixTime(), q.getRecordCount(), address, null, null); } } else { Log.e(TAG, "While running BackgroundService.processTasks(), a QueueRecord with an invalid task " + "field was found. The invalid task field was : " + task); } } catch (Exception e) { Log.e(TAG, "Exception occurred in BackgroundService.processTasks(). The exception message was:\n" + e.getMessage()); // Delete this QueueRecord from the database queueProv.deleteQueueRecord(q); } } runPeriodicTasks(); } else // If there are no other tasks that we need to do { runPeriodicTasks(); // Check whether it is time to run the 'clean database' routine. If yes then run it. if (checkIfDatabaseCleaningIsRequired()) { Intent intent = new Intent(getBaseContext(), DatabaseCleaningService.class); intent.putExtra(DatabaseCleaningService.EXTRA_RUN_DATABASE_CLEANING_ROUTINE, true); startService(intent); } } } /** * Checks whether there is already an existing QueueRecord for sending this msg * with a lower trigger time than this QueueRecord. If there is, we will push the * trigger time of this QueueRecord further into the future. This is required because * sometimes the task of a QueueRecord may not be completed for a long time, for * example when there is no internet connection available. * * @param q - The QueueRecord to be checked * * @return A boolean indicating whether a QueueRecord of greater precedence was found */ private boolean checkAndAdjustQueueRecords(QueueRecord q) { ArrayList<QueueRecord> matchingRecords = getMatchingSendMsgQueueRecords(q); // If there is more than 1 matching record, delete all but the one with the earliest trigger time. // There should never be more than 2 QueueRecords for sending a given message. if (matchingRecords.size() > 1) { matchingRecords = deleteDuplicateSendMsgQueueRecords(matchingRecords); } for (QueueRecord match : matchingRecords) { // Check whether this matching record has a trigger time earlier than the current QueueRecord if (match.getTriggerTime() < q.getTriggerTime()) { // Push the trigger time of the current QueueRecord further into the future if (match.getRecordCount() == 0) // If the QueueRecord with the earlier trigger time is the first QueueRecord for attempting to send this message { q.setTriggerTime(match.getTriggerTime() + FIRST_ATTEMPT_TTL); } else // If the QueueRecord with the earlier trigger time is not the first QueueRecord for attempting to send this message { q.setTriggerTime(match.getTriggerTime() + SUBSEQUENT_ATTEMPTS_TTL); } long timeTillTriggerTime = q.getTriggerTime() - (System.currentTimeMillis() / 1000); Log.i(TAG, "Updating the trigger time of a QueueRecord for a " + q.getTask() + " task because there is another QueueRecord for sending the same " + "message which has an earlier trigger time. The updated trigger time of this QueueRecord is " + TimeUtils.getTimeMessage(timeTillTriggerTime) + " from now.");; QueueRecordProvider.get(getApplicationContext()).updateQueueRecord(q); return true; } } return false; } /** * Returns a boolean indicating whether there are any other QueueRecords for * sending the same message as the one referred to by the supplied QueueRecord. */ private boolean checkForMatchingSendMsgQueueRecords (QueueRecord q) { return getMatchingSendMsgQueueRecords(q).size() > 0; } /** Returns an ArrayList<QueueRecord> containing any other QueueRecords for * sending the same message as the one referred to by the supplied QueueRecord. */ private ArrayList<QueueRecord> getMatchingSendMsgQueueRecords(QueueRecord q) { // First we need to get any QueueRecords which also refer to the msg in question QueueRecordProvider queueProv = QueueRecordProvider.get(getApplicationContext()); ArrayList<QueueRecord> matchingRecords = queueProv.searchQueueRecords(QueueRecordsTable.COLUMN_OBJECT_0_ID, String.valueOf(q.getObject0Id())); // Now iterate through the list and remove any QueueRecords which are either // i) The same as the current record // ii) Not QueueRecord for one of the three 'send message' tasks Iterator<QueueRecord> iterator = matchingRecords.iterator(); while(iterator.hasNext()) { QueueRecord match = iterator.next(); // Remove the current QueueRecord from the list of 'matching' QueueRecods if(match.getId() == (q.getId())) { iterator.remove(); } // Filter the list of QueueRecords so that we only consider two QueueRecords to match if they refer to the same task else if (match.getTask().equals(q.getTask()) == false) { iterator.remove(); } } return matchingRecords; } /** * Takes an ArrayList<QueueRecord> of duplicate QueueRecords for sending a single message * and returns only the one with the earliest trigger time */ private ArrayList<QueueRecord> deleteDuplicateSendMsgQueueRecords (ArrayList<QueueRecord> matchingRecords) { // Find the QueueRecord with the earliest trigger time long earliestTriggerTime = 0; long earliestRecordId = 0; for (QueueRecord q : matchingRecords) { if (earliestTriggerTime == 0) { earliestTriggerTime = q.getTriggerTime(); earliestRecordId = q.getId(); } else if (q.getTriggerTime() < earliestTriggerTime) { earliestTriggerTime = q.getTriggerTime(); earliestRecordId = q.getId(); } } // Return only the QueueRecord with the earliest trigger time ArrayList<QueueRecord> recordToReturn = new ArrayList<QueueRecord>(); for (QueueRecord q : matchingRecords) { if (q.getId() == earliestRecordId) { recordToReturn.add(q); } else { new QueueRecordProcessor().deleteQueueRecord(q); } } return recordToReturn; } /** * Runs the tasks that must be done periodically, e.g. checking for new msgs. */ private void runPeriodicTasks() { try { Log.i(TAG, "BackgroundService.runPeriodicTasks() called"); runCheckForMessagesTask(); runCheckIfPubkeyReDisseminationIsDueTask(); } catch (Exception e) { Log.e(TAG, "Exception occurred in BackgroundService.runPeriodicTasks(). The exception message was:\n" + e.getMessage()); } } /** * This method runs the 'check for messages and send acks' task, via * the TaskController. <br><br> * * Note that we do NOT create QueueRecords for this task, because it * is a default action that will be carried out regularly anyway. */ private void runCheckForMessagesTask() { Log.i(TAG, "BackgroundService.runCheckForMessagesTask() called"); // First check whether an Internet connection is available. If not, we cannot proceed. if (NetworkHelper.checkInternetAvailability() == true) { // Only run this task if we have at least one Address! AddressProvider addProv = AddressProvider.get(getApplicationContext()); ArrayList<Address> myAddresses = addProv.getAllAddresses(); if (myAddresses.size() > 0) { // Attempt to complete the task TaskController taskController = new TaskController(); taskController.checkForMessagesAndSendAcks(); } else { Log.i(TAG, "No Addresses were found in the application database, so we will not run the 'Check for messages' task"); } } } /** * This method runs the 'check if pubkey re-dissemination is due' task, via * the TaskController. <br><br> * * Note that we do NOT create QueueRecords for this task, because it is a * default action that will be carried out regularly anyway. */ private void runCheckIfPubkeyReDisseminationIsDueTask() { Log.i(TAG, "BackgroundService.runCheckIfPubkeyReDisseminationIsDueTask() called"); // Only run this task if we have at least one Address! AddressProvider addProv = AddressProvider.get(getApplicationContext()); ArrayList<Address> myAddresses = addProv.getAllAddresses(); if (myAddresses.size() > 0) { // First delete any duplicate pubkeys and corresponding objects deleteDuplicatePubkeys(myAddresses); // Attempt to complete the task TaskController taskController = new TaskController(); taskController.checkIfPubkeyDisseminationIsDue(DO_POW); } else { Log.i(TAG, "No Addresses were found in the application database, so we will not run the 'Check if pubkey re-dissemination is due' task"); } } /** * Deletes any duplicate pubkeys and any Payloads or QueueRecords that * correspond to them. */ private void deleteDuplicatePubkeys(ArrayList<Address> myAddresses) { try { // First find any duplicate Pubkeys PubkeyProvider pubProv = PubkeyProvider.get(getApplicationContext()); ArrayList<Pubkey> duplicatePubkeys = new ArrayList<Pubkey>(); for (Address a : myAddresses) { // Find any duplicate pubkeys ArrayList<Pubkey> correspondingPubkeys = pubProv.searchPubkeys(PubkeysTable.COLUMN_RIPE_HASH, Base64.encodeToString(ByteUtils.stripLeadingZeros(a.getRipeHash()), Base64.DEFAULT)); if (correspondingPubkeys.size() > 1) { // Mark all the pubkeys as potential duplicates for (Pubkey p : correspondingPubkeys) { duplicatePubkeys.add(p); } // Remove the pubkey with the latest expiration time - this is the one to keep long latestTimeValue = 0; for (Pubkey p : correspondingPubkeys) { if (latestTimeValue == 0) { latestTimeValue = p.getExpirationTime(); } else if (p.getExpirationTime() > latestTimeValue) { latestTimeValue = p.getExpirationTime(); } } for (Pubkey p : correspondingPubkeys) { if (p.getExpirationTime() == latestTimeValue) { duplicatePubkeys.remove(p); } } } } Log.i(TAG, "Found " + duplicatePubkeys.size() + " duplicate Pubkey(s)"); for (Pubkey p : duplicatePubkeys) { try { // Get the corresponding address AddressProvider addProv = AddressProvider.get(getApplicationContext()); Address correspondingAddress = addProv.searchForSingleRecord(p.getCorrespondingAddressId()); // Delete their corresponding pubkey Payloads and any QueueRecords for disseminating those payloads PayloadProvider payProv = PayloadProvider.get(getApplicationContext()); ArrayList<Payload> correspondingPayloads = payProv.searchPayloads(PayloadsTable.COLUMN_RELATED_ADDRESS_ID, String.valueOf(correspondingAddress.getId())); for (Payload payload : correspondingPayloads) { payProv.deletePayload(payload); // Delete any QueueRecords for disseminating this Payload QueueRecordProvider queueProv = QueueRecordProvider.get(getApplicationContext()); ArrayList<QueueRecord> allQueueRecords = queueProv.getAllQueueRecords(); for (QueueRecord q : allQueueRecords) { if (q.getTask().equals(QueueRecordProcessor.TASK_DISSEMINATE_PUBKEY) && q.getObject0Id() == payload.getId()) { queueProv.deleteQueueRecord(q); } } } // Delete the duplicate Pubkey pubProv.deletePubkey(p); } catch (Exception e) { Log.e(TAG, "Exception occurred while processing duplicate pubkeys in BackgroundService.deleteDuplicatePubkeys(). The exception message was:\n" + e.getMessage() + "\n Moving on to the next duplicate pubkey."); continue; } } } catch (Exception e) { Log.e(TAG, "Exception occurred in BackgroundService.deleteDuplicatePubkeys(). The exception message was:\n" + e.getMessage()); } } /** * Determines whether it is time to run the 'clean database' routine, * which deletes defunct data. This is based on the period of time since * this routine was last run. * * @return A boolean indicating whether or not the 'clean database' routine * should be run. */ private boolean checkIfDatabaseCleaningIsRequired() { Log.i(TAG, "BackgroundService.checkIfDatabaseCleaningIsRequired() called"); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); long currentTime = System.currentTimeMillis() / 1000; long lastDataCleanTime = prefs.getLong(DatabaseCleaningService.LAST_DATABASE_CLEAN_TIME, 0); if (lastDataCleanTime == 0) { return true; } else { long timeSinceLastDataClean = currentTime - lastDataCleanTime; if (timeSinceLastDataClean > TIME_BETWEEN_DATABASE_CLEANING) { return true; } else { long timeTillNextDatabaseClean = TIME_BETWEEN_DATABASE_CLEANING - timeSinceLastDataClean; Log.i(TAG, "The database cleaning service was last run " + TimeUtils.getTimeMessage(timeSinceLastDataClean) + " ago\n" + "The database cleaning service will be run again in " + TimeUtils.getTimeMessage(timeTillNextDatabaseClean)); return false; } } } @Override public void onDestroy() { super.onDestroy(); if (mCacheWordHandler != null) { mCacheWordHandler.disconnectFromService(); } } @SuppressLint("InlinedApi") @Override public void onCacheWordLocked() { Log.i(TAG, "BackgroundService.onCacheWordLocked() called."); // Nothing to do here currently } @Override public void onCacheWordOpened() { Log.i(TAG, "BackgroundService.onCacheWordOpened() called."); // Nothing to do here currently } @Override public void onCacheWordUninitialized() { Log.i(TAG, "BackgroundService.onCacheWordUninitialized() called."); // Nothing to do here currently } }