Skip to content

Instantly share code, notes, and snippets.

@frontrangerider2004
Last active June 13, 2016 19:33
Show Gist options
  • Save frontrangerider2004/d9b0f2807f80dfbfc56c07055a2bb33a to your computer and use it in GitHub Desktop.
Save frontrangerider2004/d9b0f2807f80dfbfc56c07055a2bb33a to your computer and use it in GitHub Desktop.
Shows a robust way to get work off the UI thread in Android. Runs a Linux cmd in another thread.
package com.frontrangerider.SubProcess;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
import com.frontrangerider.intents.MyIntentActions;
import com.frontrangerider.intents.MyIntentExtras;
import com.frontrangerider.util.Logger;
/**
IntentServiceStatusReceiver
Allows the UI or another app component to receive updates from the
ProcessIntentService which is completing work on a background thread.
Typically and instance of this would be created and registered with
an intent-filter in the UI.
*/
public class IntentServiceStatusReceiver extends BroadcastReceiver {
private static final String TAG = "IntentServiceStatusReceiver";
private Logger logger = new Logger(TAG);
public IntentServiceStatusReceiver(){
logger.d("Constructor");
}
@Override
public void onReceive(Context context, Intent intent) {
logger.d("onReceive");
MyIntentActions action = MyIntentActions.lookupByValue(intent.getAction());
switch (action){
case SUB_PROCESS_UPDATE:
//Parse the extra for the status and make a toast
int status = intent.getIntExtra(MyIntentExtras.SUB_PROCESS_STATUS.getValue(), SubProcessStatusEnum.FAILED.getKey());
logger.d("onReceive", "Sub-Process Update::STATUS ", SubProcessStatusEnum.lookupByKey(status).getValue());
String msg;
if(status == SubProcessStatusEnum.FAILED.getKey()){
msg = "Failed to complete Sub-Process!";
}
else {
msg = "Sub-Process success.";
}
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
break;
case NONE:
logger.w("onReceive", "Received default Intent Action, this is probably due to the action not being defined.");
break;
default:
logger.e("onReceive", "Received unknown Intent Action");
break;
}
}
}//End Class
public class MainActivity extends Activity{
private IntentServiceStatusReceiver intentServiceStatusReceiver;
@Override
protected void onResume() {
super.onResume();
registerListeners();
}
@Override
protected void onPause() {
super.onPause();
removeListeners();
}
/**
This will start the Intent Service.
NOTE: You can replace 'null' with a linux cmd
*/
public void onClickStartService(){
Intent subProcIntent = new Intent(getApplicationContext(), ProcessIntentService.class);
subProcIntent.setData(null);
getApplicationContext().startService(subProcIntent);
}
/**
* Register for the SubProcess Intent Service events
*/
private void registerListeners(){
logger.d("registerListeners");
intentServiceStatusReceiver = new IntentServiceStatusReceiver();
IntentFilter subProcessIntentFilter = new IntentFilter(MyIntentActions.SUB_PROCESS_UPDATE.getValue());
LocalBroadcastManager.getInstance(this).registerReceiver(intentServiceStatusReceiver, subProcessIntentFilter);
}
/**
* Unregisters the SubProcess Intent Service events
*/
private void removeListeners(){
logger.d("removeListeners");
LocalBroadcastManager.getInstance(this).unregisterReceiver(intentServiceStatusReceiver);
}
}//End Class
package com.frontrangerider.SubProcess;
import android.app.IntentService;
import android.content.Intent;
import android.support.v4.content.LocalBroadcastManager;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import com.frontrangerider.intents.MyIntentActions;
import com.frontrangerider.intents.MyIntentExtras;
import com.frontrangerider.util.Logger;
/*
ProcessIntentService
Runs a Linux command in another thread (other than the UI)
Provides a great way to offload long running background tasks to another
thread. This is also safer than an AsyncTask in terms of ensuring the work
gets completed.
This service will spin up when called via an intent and it will
create it's own thread to perform work. This service will then privately
notify components of this package that work has been completed, and then tear
itself down.
*/
public class ProcessIntentService extends IntentService {
private final String TAG = "ProcessIntentService";
private final Logger logger = new Logger(TAG);
private static final boolean DEBUG_SHELL = false; //Verbose sub-process debug
private static final String SHELL_CMD = "sh ";
public ProcessIntentService() {
super("ProcessIntentService");
}
/**
* sendLocalPrivateBroadcast:
* Sends a local private broadcast that can only be received by this application
* or processes using the application's UID.
* @param localIntent
*/
private void sendLocalPrivateBroadcast(Intent localIntent){
logger.d("sendLocalPrivateBroadcast");
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(localIntent);
}
/**
* updateUiWithFailure:
* Creates a Local Private Broadcast for the sub-process failure event using the
* supplied failure message as the intent extra.
* @param message
*/
private void updateUiWithFailure(String message) {
logger.d("updateUiWithFailure");
//Uses actions and events from Enums holding those constants
Intent i = new Intent(MyIntentActions.SUB_PROCESS_UPDATE.getValue());
i.putExtra(MyIntentExtras.SUB_PROCESS_STATUS.getValue(), SubProcessStatusEnum.FAILED.getKey());
i.putExtra(MyIntentExtras.SUB_PROCESS_STATUS_DETAIL.getValue(), message);
sendLocalPrivateBroadcast(i);
}
/**
* updateUiWithSuccess
* Creates a Local Private Broadcast for the sub-process Success event.
*/
private void updateUiWithSuccess(){
logger.d("updateUiWithSuccess");
//Uses actions and events from Enums holding those constants
Intent i = new Intent(MyIntentActions.SUB_PROCESS_UPDATE.getValue());
i.putExtra(MyIntentExtras.SUB_PROCESS_STATUS.getValue(), SubProcessStatusEnum.SUCCESS.getKey());
sendLocalPrivateBroadcast(i);
}
/**
* handleProcessExitCode
* Parses the exit code and determines if the script failed.
* Will also send a UI update for each case.
* @param exitCode
*/
private void handleProcessExitCode(String cmd, int exitCode){
logger.d("onPostexecSubProcess", cmd + " exited with code:", exitCode);
switch (exitCode){
case 0:
updateUiWithSuccess();
break;
default:
//TODO Decide if we should pass more details to the intent
updateUiWithFailure(SubProcessStatusEnum.NONE.getValue());
break;
}
}
/**
* execSubProcess:
*
* Creates a Linux process to run the supplied command.
* @param cmd
*/
private void execSubProcess(String cmd){
logger.d("execSubProcess", cmd);
Process process = null;
BufferedReader stdIn = null;
BufferedReader stdErr = null;
//Run the command via Linux Shell
try {
process = Runtime.getRuntime().exec(cmd);
stdIn = new BufferedReader(new InputStreamReader(process.getInputStream()));
stdErr = new BufferedReader(new InputStreamReader(process.getErrorStream()));
//Read the STDIO from the shell if we care
if(DEBUG_SHELL){
// read the output from the command
logger.d("Here is the standard output of the command:\n");
String s = null;
while ((s = stdIn.readLine()) != null) {
logger.d(SubProcessStatusEnum.STD_IN_MSG_PREFIX.getValue() + s);
}
// read any errors from the command
logger.d("Here is the standard error of the command (if any):\n");
while ((s = stdErr.readLine()) != null) {
logger.d(SubProcessStatusEnum.STD_IN_MSG_PREFIX.getValue() + s);
}
}
//Must call wait in order to get the exit code, otherwise we get Exception.
process.waitFor();
handleProcessExitCode(cmd, process.exitValue());
}
//For the process
catch (IOException e) {
logger.e("execSubProcess", "ERROR: IOException while executing sub-process\n" + e.getMessage());
updateUiWithFailure(e.getMessage());
}
//For the process.waitFor()
catch (InterruptedException e) {
logger.e("execSubProcess", "ERROR: InterruptedException while executing sub-process\n" + e.getMessage());
updateUiWithFailure(e.getMessage());
}
/*
Close and cleanup our STDIO buffers from the shell:
You might think that having another try/catch inside the
finally defeats the point of a try/catch, but,
apparently this is the accepted pattern and the correct way to do it.
NOTE: We need one try catch for each buffer.
*/
finally{
try{
if(stdErr != null) stdErr.close();
}
catch (IOException e){
//This should never happen so we don't care about logging
e.printStackTrace();
}
try{
if(stdIn != null) stdIn.close();
}
catch(IOException e){
//This should never happen so we don't care about logging
e.printStackTrace();
}
}
}//End Exec
/**
* onHandleIntent:
*
* Parses the supplied intent for a string command corresponding to a Linux
* command and then executes the command via Linux sub-process.
* @param intent - The Android intent to start this service
*/
@Override
protected void onHandleIntent(Intent intent) {
logger.d("onHandleIntent", intent);
String cmd;
if(intent.getDataString() != null){
cmd = SHELL_CMD + intent.getDataString();
logger.d("onHandleIntent", "Received intent data:", cmd);
}
else {
cmd = SHELL_CMD + getString(R.string.default_cmd_path);
}
logger.d("onHandleIntent", "Set Linux CMD:", cmd);
execSubProcess(cmd);
}
}//End Class
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment