Android/Java n00b encounters a bug I can't think my way around.

JoLLyRoGer

Diamond Member
Aug 24, 2000
4,154
4
81
Good Morning,

This is going to be a very long write-up so if you're curious/interested, I recommend grabbing a cup of coffee and carving out some time to dig through this with me.

Ok.. got your coffee? Great! Here we go!

I have been working on an Android app that is mostly for my own educational interests but has some applicability where I work.

It is essentially an application that once installed queries the phone's location sensor and reports back the location in the form of a speudo-XML string to a server I have written in LabView which then translates the string to KML and exports to Google Earth to display the phone's location on a map. The server side is done, but the mobile app is giving me hell!

The basic functions of the app are to 'Start' 'Stop' and 'Send' where start and stop trigger a timer loop which will periodically make the call to update the server with the phone's current location and derived GPS time, while Send can be pressed from the UI to perform this function manually.

The app can be controlled one of two ways. Either through the User Interface or by responding to coded SMS messages that trigger certain functions.

For the most part I have it all working except for a software bug that I can't seem to get past. What happens is this:

-- When the app is started from the UI, it creates a runnable that handles the periodic updating.
-- IF and ONLY IF the runnable is called from the UI (the start button) it will respond appropriately to the
Stop button and stop the runnable by removing the callback.

--When the app is triggered in response to an SMS command it will create a situation where the runnable cannot be stopped. That is, it will respond to the stop command either from the UI button press or a coded SMS, but only momentarily at which point the runnable automatically restarts. This behavior cannot be broken by removing callbacks and I have experimented with several approaches thus far.

I will post the relevant code, comments, and logcat output relevant to the different test cases. Hopefully somebody here much smarter than me can spot the issue.

Feel free to copy and compile this code if you wish to test it yourself. I used Android Studio but I'm sure Eclipse, etc. will do the job as well.

Android Manifest

Code:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="my.testapp">

    <!-- Declare permissions needed by LocationServices class -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <!-- Declare permissions needed by SendMessage class -->
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <!-- Declare permission needed by SmsButler class -->
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    <uses-permission android:name="android.permission.READ_SMS" />
    <uses-permission android:name="android.permission.SEND_SMS" />


    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity"
            android:configChanges="orientation|screenSize|keyboardHidden"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>


        <receiver android:name=".IncomingSms">
            <intent-filter>
                <action android:name="android.provider.Telephony.SMS_RECEIVED" />
            </intent-filter>
        </receiver>

    </application>
</manifest>



activity_main.xml
Code:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="my.testapp.MainActivity">



    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true"
        android:id="@+id/linearLayoutMain">

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_above="@+id/linearLayoutMain"
            android:layout_alignParentStart="true"
            android:background="#963F51B5"
            android:id="@+id/linearLayoutPlayer"
            android:focusableInTouchMode="true">

            <TextView
                android:layout_width="100dp"
                android:layout_height="wrap_content"
                android:text="Player ID:"
                android:id="@+id/tvPlayer"
                android:textColor="#AAFFFFFF"
                android:textSize="18dp" />

            <TextView
                android:layout_width="0dp"
                android:layout_height="40dp"
                android:id="@+id/tvPlayerInfo"
                android:textColor="#FFFFFF"
                android:textSize="22dp"
                android:layout_weight="1"
                android:hint="--ASSIGN NAME--"
                android:text="Joe's Phone" />

        </LinearLayout>

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_above="@+id/linearLayoutMain"
            android:layout_alignParentStart="true"
            android:background="#963F51B5"
            android:id="@+id/linearLayoutLatitude">

            <TextView
                android:layout_width="100dp"
                android:layout_height="wrap_content"
                android:text="Latitude:"
                android:id="@+id/tvLatitude"
                android:textColor="#AAFFFFFF"
                android:textSize="18dp" />

            <TextView
                android:layout_width="0dp"
                android:layout_height="40dp"
                android:id="@+id/tvLatInfo"
                android:textColor="#FFFFFF"
                android:textSize="22dp"
                android:layout_weight="1"
                android:hint="--null--" />


        </LinearLayout>

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_above="@+id/linearLayoutMain"
            android:layout_alignParentStart="true"
            android:background="#963F51B5"
            android:id="@+id/linearLayoutLongitude">

            <TextView
                android:layout_width="100dp"
                android:layout_height="wrap_content"
                android:text="Longitude:"
                android:id="@+id/tvLongitude"
                android:textColor="#AAFFFFFF"
                android:textSize="18dp" />

            <TextView
                android:layout_width="0dp"
                android:layout_height="40dp"
                android:id="@+id/tvLongInfo"
                android:textColor="#FFFFFF"
                android:textSize="22dp"
                android:layout_weight="1"
                android:hint="--null--" />

        </LinearLayout>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="80dp"
            android:id="@+id/tvTime"
            android:background="#963F51B5"
            android:hint="HH:MM:SS"
            android:gravity="center_vertical|center_horizontal"
            android:textColor="#88FFFFFF"
            android:textColorHint="#88FFFFFF"
            android:textIsSelectable="true"
            android:textSize="40dp"
            android:textStyle="italic" />

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <LinearLayout
                android:orientation="vertical"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center_vertical|center_horizontal">

                <Button
                    android:layout_width="120dp"
                    android:layout_height="wrap_content"
                    android:text="START"
                    android:id="@+id/bStart"
                    android:layout_weight="1" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center_vertical|center_horizontal"
                android:orientation="vertical">

                <Button
                    android:id="@+id/bStop"
                    android:layout_width="120dp"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:layout_weight="1"
                    android:text="STOP" />
            </LinearLayout>

            <LinearLayout
                android:orientation="vertical"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center_vertical|center_horizontal">

                <Button
                    android:id="@+id/bSend"
                    android:layout_width="120dp"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:layout_weight="1"
                    android:text="SEND" />
            </LinearLayout>

        </LinearLayout>

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/linearLayoutSatInfo">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:orientation="horizontal">

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_weight="1"
                    android:orientation="horizontal">

                    <TextView
                        android:id="@+id/tvAppStat"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="right"
                        android:layout_weight="1"
                        android:gravity="right"
                        android:text="App Status: "
                        android:textAppearance="?android:attr/textAppearanceSmall"
                        android:textColor="#88000000"
                        android:textStyle="bold" />
                </LinearLayout>

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_weight="1"
                    android:orientation="horizontal">

                    <TextView
                        android:id="@+id/tvAppStatInfo"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:hint="-Waiting-"
                        android:textAppearance="?android:attr/textAppearanceSmall"
                        android:textColor="#66000000"
                        android:textIsSelectable="true"
                        android:textStyle="normal" />
                </LinearLayout>

            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/tvSatSnr"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="left"
                    android:layout_weight="1"
                    android:gravity="right"
                    android:text="Sat SNR:  "
                    android:textAppearance="?android:attr/textAppearanceSmall"
                    android:textColor="#88000000"
                    android:textStyle="bold" />

                <TextView
                    android:id="@+id/tvSatSnrInfo"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:hint="--null--"
                    android:textAppearance="?android:attr/textAppearanceSmall"
                    android:textColor="#FFFFFFFF"
                    android:textIsSelectable="true" />

            </LinearLayout>

        </LinearLayout>

    </LinearLayout>

</android.support.constraint.ConstraintLayout>



MainActivity.java
Code:
package my.testapp;

import android.content.Context;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity
{
    /////////////////////////////////////////////////////////////
    //Declare a context for Android and set to this activity
    //  re-use this context when calling other constructors or
    //  methods that require it.
    /////////////////////////////////////////////////////////////
    public final Context c = this;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //Instantiate an object of LocationServices and call the
        //getLocationUpdates method.  Pass this context 'c' to the
        //method so it can update the TextViews.
        final LocationServices locationServices = new LocationServices();
        locationServices.getLocationUpdates(c);

        //Instantiate an object of SendMessage
        final SendMessage sendMessage = new SendMessage();

        //Instantiate and object of TimerButler and call
        //its startHandler method.
        final TimerButler timerButler = new TimerButler();

        //Pass this context to the ContextHandler
        ContextHandler.setC(c);

        ////////////////////////////////////////////////////////////
        //Set up the START button to perform some action
        ////////////////////////////////////////////////////////////
        final Button bStart = (Button) findViewById(R.id.bStart);
        bStart.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                Toast.makeText(c, "Starting Thread", Toast.LENGTH_SHORT).show();
                System.out.println("StartApp UI button clicked"); //Just some code for debugging with logcat
                timerButler.startHandler();    //startTimer();

            }
        });//End setOnClickListener


        ////////////////////////////////////////////////////////////
        //Set up the STOP button to perform some action
        ////////////////////////////////////////////////////////////
        final Button bStop = (Button) findViewById(R.id.bStop);
        bStop.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                Toast.makeText(c, "Stopping Thread", Toast.LENGTH_SHORT).show();
                System.out.println("StopApp UI button clicked"); //Just some code for debugging with logcat
                timerButler.stopHandler();        //stopTimer();

            }
        });//End setOnClickListener

        ////////////////////////////////////////////////////////////
        //Set up the SEND button to perform some action
        ////////////////////////////////////////////////////////////
        final Button bSend = (Button) findViewById(R.id.bSend);
        bSend.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                Toast.makeText(c, "Sending Location Information", Toast.LENGTH_SHORT).show();
                sendMessage.sendMessage();
                System.out.println(locationServices.getNmea()); //Just some code for debugging with logcat
            }
        });//End setOnClickListener
    }//End onCreate
}//End MainActivity

LocationServices.java
Code:
package my.testapp;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.GpsStatus;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v4.app.ActivityCompat;
import android.widget.TextView;

import java.text.SimpleDateFormat;


public class LocationServices  {

    //////////////////////////////////////////////////////////////////
    //Declare some variables for storing GPS information
    //We make these variable static so multiple objects of this class
    //  will reference the same instance of these variables
    //////////////////////////////////////////////////////////////////
    private static double dblLatInfo = 0.0, dblLongInfo = 0.0, dblElvInfo = 0.0;
    private static String sFormatTime, sNmea = "";


    //Declare some TextViews for displaying information
    private TextView tvLatInfo, tvLongInfo, tvTime;


    //Declare LocationManager and LocationListener for calling the GPS
    private LocationManager locationManager;
    private LocationListener locationListener;


    public void getLocationUpdates(Context c)
    {
        /////////////////////////////////////////////////////////////////////////////////
        //Initialize the TextView elements
        //Since this class is a non-activity class, we must cast to MainActivity class
        //  for these elements, then reference the context passed to the constructor
        //  from the activity class
        /////////////////////////////////////////////////////////////////////////////////
        tvLatInfo = (TextView) ((MainActivity)c).findViewById(R.id.tvLatInfo);
        tvLongInfo = (TextView) ((MainActivity)c).findViewById(R.id.tvLongInfo);
        tvTime = (TextView) ((MainActivity)c).findViewById(R.id.tvTime);



        ///////////////////////////////////////////////////
        //Perform permission checks for location services
        ///////////////////////////////////////////////////
        if (ActivityCompat.checkSelfPermission(c, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
                && ActivityCompat.checkSelfPermission(c, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
        {
            return;
        }



        ////////////////////////////
        //Initialize the GPS service
        ////////////////////////////
        locationManager = (LocationManager) c.getSystemService(Context.LOCATION_SERVICE);



        /////////////////////////////////////////////
        // Instantiate the location listener and
        //   get location sensor position data
        /////////////////////////////////////////////
        locationListener = new LocationListener()
        {
            @SuppressLint({"SimpleDateFormat", "SetTextI18n"})
            @Override

            //Update TextViews and store location information on Location Change
            public void onLocationChanged(Location location)
            {
                //Format GPS timestamp into human-readable format
                sFormatTime = new SimpleDateFormat("HH:mm:ss").format(location.getTime());

                //////////////////////////////////////////////////////////
                //We only update location and time data from a GPS source
                //  if the locationListener is using something
                //  other than GPS, display a warning message and
                //  nullify the displayed location but retain the
                //  last location coordinates stored in dbl variables
                //////////////////////////////////////////////////////////
                if (location.getProvider().equals(LocationManager.GPS_PROVIDER))
                {
                    //Update the textviews on the main screen
                    tvTime.setText("GPS Time:  "+sFormatTime);
                    tvLatInfo.setText(""+location.getLatitude());
                    tvLongInfo.setText(""+location.getLongitude());

                    //Store the current location information
                    dblLatInfo = location.getLatitude();
                    dblLongInfo = location.getLongitude();
                    dblElvInfo = location.getAltitude();
                }
                else
                {
                    //Update the textviews on the main screen
                    tvTime.setText("GPS SIGNAL LOST!!!");
                    tvLatInfo.setText("");
                    tvLongInfo.setText("");
                }


            }

            @Override
            public void onStatusChanged(String provider, int status, Bundle extras)
            {

            }

            @Override
            public void onProviderEnabled(String provider)
            {

            }

            @Override
            public void onProviderDisabled(String provider)
            {
                //Start the Location Provider if location is disabled
                //Use the context 'c' passed to the constructor for this class
                //--commented out-- Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
                // --commented out-- c.startActivity();
            }
        };



        //////////////////////////////////////////////////////////////////////////////
        //Set location update triggers
        //(FromSource, TimeInMilliseconds, DistanceInMeters, CastTo: locationListener)
        //////////////////////////////////////////////////////////////////////////////
        locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 1000, 0, locationListener);
        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 0, locationListener);



        /////////////////////////////////////////////
        //Get and store NMEA Sentence Information
        /////////////////////////////////////////////
        locationManager.addNmeaListener(new GpsStatus.NmeaListener()
        {
            @Override
            public void onNmeaReceived(long timestamp, String nmea)
            {
                sNmea = "NMEA Scentence: " + nmea;

            }
        });
    }//End getLocaitonUpdates method



    /////////////////////////////////////////////////////////////
    //Getter methods for accessing information from another class
    /////////////////////////////////////////////////////////////
    public String getNmea()
    {
        return sNmea;
    }

    public double getLat()
    {
        return dblLatInfo;
    }

    public double getLong()
    {
        return dblLongInfo;
    }

    public double getElv()
    {
        return dblElvInfo;
    }

    public String getTime()
    {
        return sFormatTime;
    }

}//End public class LocationServices


SendMessage.java
Code:
package my.testapp;

import android.content.Context;
import android.os.AsyncTask;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;



public class SendMessage
{
    //Declare a some variables for a formatted message
    private String sMessage, sTime;
    private String sNmea;
    private double dblLatInfo;
    private double dblLongInfo;
    private double dblElvInfo;


    //Instantiate LocationServices object
    LocationServices locationServices = new LocationServices();


    //////////////////////////////////////////////////////////
    //Support method declared private is visible only to this
    //class.  Calls getters from LocationServices to create
    //the XML formatted message
    //////////////////////////////////////////////////////////
    private void createMessage()
    {
        dblLatInfo = locationServices.getLat();
        dblLongInfo = locationServices.getLong();
        dblElvInfo = locationServices.getElv();
        sNmea = locationServices.getNmea();
        sTime = locationServices.getTime();

        sMessage = "<MESSAGE><ID>My ID</ID><LAT>"+dblLatInfo+"</LAT><LNG>"
                    +dblLongInfo+"</LNG><ELV>"+dblElvInfo+"</ELV><NMEA>"+sNmea
                    +"</NMEA><TIME>"+sTime+"</TIME></MESSAGE><EOF>";
    }


    /////////////////////////////////////////////////////////////
    //Calls the createMessage support method.
    //Calls the doInBackground method from private class SendThis
    //  and passes sMessage String as a parameter
    /////////////////////////////////////////////////////////////
    public void sendMessage()
    {
        createMessage();
        System.out.println("Executing sendMessage()"); //Debugging message
        new SendThis().execute(sMessage); //Pass this to doInBackground
    }

    ///////////////////////////////////////////////////////////////////////
    //Private Class SendThis sends sMessage via UDP as a background process
    ///////////////////////////////////////////////////////////////////////
    private class SendThis extends AsyncTask<String,Void,Void>
    {
        @Override

        ////////////////////////////////////////////////////////////////////
        //doInBackground method inherited from AsyncTask accepts one or more
        //    String objects into 'params' as an ArrayList.  Gets passed in
        ////////////////////////////////////////////////////////////////////
        protected Void doInBackground(String... params)
        {
            //Create instance String variable to hold the message and set value
            String sSendMe = params[0];

            //Created instance inetAddress variable and initialize
            InetAddress inetAddress = null;

            //Set the inetAddress value and handle any errors
            try
            {
                inetAddress = InetAddress.getByName("10.0.0.23");
            }
            catch (UnknownHostException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            //Create int and DatagramSocket variables and initialize
            int udpPort=55505;
            DatagramSocket udpSocket = null;

            //Instantiate the DatagramSocket and handle any errors
            try
            {
                udpSocket = new DatagramSocket();
            }
            catch (SocketException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            //Convert message into an array of bytes
            byte[] btArrData = sSendMe.getBytes();

            //Create the datagram packet from the byte array
            DatagramPacket dataPac = new DatagramPacket(btArrData, btArrData.length, inetAddress, udpPort);
            try
            {
                //Send the datagram packet
                udpSocket.send(dataPac);
            }
            catch (IOException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            return null;
        }//End doInBackground
    }//End private class SendThis
}//End public class SendMessage


IncomingSms.java
Code:
package my.testapp;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.telephony.SmsMessage;

///////////////////////////////////////////////////////////
//Create a lister for incoming SMS messages to set up SMS
//  control over certain application functions
///////////////////////////////////////////////////////////
public class IncomingSms extends BroadcastReceiver
{
    //Declare variable to hold the SMS Originator information and
    //  the received SMS body
    private static String sSmsOriginator;
    private static String sSmsBody;

    //Instantiate the smsButler object to perform some
    //   SMS driven functions
    private SmsButler smsButler = new SmsButler();

    @Override
    public void onReceive(Context context, Intent intent)
    {
        //Use a try-catch statement for error handling
        try
        {
            //Retrieves a map of extended SMS functionality
            // Get/Read current incoming SMS
            Bundle myBundle = intent.getExtras();
            SmsMessage [] messages = null;
            String strMessage = "";

            if (myBundle != null)
            {
                //////////////////////////////////////////////////
                //Create an object array of messages from 'pdus'
                //  == a pdu is a Protocol Data Unit, the
                //  the industry format for an SMS message.
                //  A large message might be broken into many,
                //  which is why it is an array of objects.
                //////////////////////////////////////////////////
                Object [] pdus = (Object[]) myBundle.get("pdus");

                ///////////////////////////////////////////////////////////////////////
                //Deconstruct "messages" objects array and pull the most recent message
                //  into the currentMessage object so we can deal with only the most
                //  recent segment of a larger SMS.
                ///////////////////////////////////////////////////////////////////////
                messages = new SmsMessage[pdus.length];
                for (int i = 0; i < messages.length; i++)
                {
                    ///////////////////////////////////////////////////////
                    //If message uses newer 3GPP2 formatting for
                    //  CDMA/LTE networks, use the first method to
                    //  handle the message format.
                    //
                    // Else use the legacy (depreciated) method for
                    //  handling 3GPP formatted messages
                    //  (GSM/UMTS)
                    ///////////////////////////////////////////////////////
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        String format = myBundle.getString("format");
                        messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i], format);
                    }
                    else {
                        //Line-through indicates depreciated method
                        //  used for handling legacy formats
                        messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
                    }

                    //A simple string builder to concatenate a message to display for debugging
                    strMessage += "SMS From: " + messages[i].getOriginatingAddress();
                    strMessage += " : ";
                    strMessage += messages[i].getMessageBody();
                    strMessage += "\n";

                    //Setter method to send a reply
                    sSmsOriginator = messages[i].getDisplayOriginatingAddress();
                    sSmsBody = messages[i].getMessageBody();

                }//End for loop

                System.out.println(strMessage); //Debugging message
                fireSmsSender();

                //Log.e("SMS", strMessage);
                //Toast.makeText(context, strMessage, Toast.LENGTH_SHORT).show();
            }//End if statement
        }//End try

        //Handle any errors that may occur
        catch (Exception e)
        {
            e.printStackTrace();
        }//End catch
    }//End onReceive method

    //Support method for passing information to SmsButler
    private void fireSmsSender()
    {
        smsButler.smsAction(sSmsOriginator,sSmsBody);
    }

}//End IncomingSms class

SmsButler.java
Code:
package my.testapp;


import android.content.Context;
import android.telephony.SmsManager;

public class SmsButler
{
    //Declare some String variables for this class
    private String sSmsOriginator, sSmsReceived, sSmsBody;

    //Instantiate an SmsManager to handle SMS responses
    private SmsManager smsManager = SmsManager.getDefault();

    //Instantiate these objects so we can call their methods.
    private TimerButler timerButler = new TimerButler();
    private LocationServices locationServices = new LocationServices();
    private SendMessage sendMessage = new SendMessage();

    ////////////////////////////////////////////////////////////////////////////
    //Main worker method for this class.  Accepts two string arguments from
    //  the IncomingSms class and makes a determination on what to do based
    //  on the contents of the message received.
    ////////////////////////////////////////////////////////////////////////////
    public void smsAction (String originator, String received)
    {
        //Accepts parameters passed from IncomingSms class
        sSmsOriginator = originator;
        sSmsReceived = received;


        //If "#getTime" SMS message is received, get GPS time from
        //  LocationServices and reply to sender
        if (sSmsReceived.compareTo("#getTime") == 0)
        {
            System.out.println("SMS: #getTime command received");//Debugging message
            sSmsBody = locationServices.getTime();
            smsManager.sendTextMessage(sSmsOriginator, null, sSmsBody, null, null);
            System.out.println("#getTime response sent to: "+sSmsOriginator);//Debugging message
        }

        //If "#getPos" SMS message is received, get position data
        // from LocationServices and reply to sender
        else if (sSmsReceived.compareTo("#getPos") == 0)
        {
            System.out.println("SMS: #getPos command received");//Debugging message
            sSmsBody = "\nLat: "+locationServices.getLat()
                    +"\nLong: "+locationServices.getLong()
                    +"\nElv: "+locationServices.getElv();
            smsManager.sendTextMessage(sSmsOriginator, null, sSmsBody, null, null);
            System.out.println("#getPos response sent to: "+sSmsOriginator);//Debugging message
        }

        //If "#sendPos" SMS message is received, get position data
        // from LocationServices and send to upstream server
        else if (sSmsReceived.compareTo("#sendPos") == 0)
        {
            System.out.println("SMS: #sendPos command received");//Debugging message
            sendMessage.sendMessage();
            smsManager.sendTextMessage(sSmsOriginator, null, "#sendPos acknowledged", null, null);
            System.out.println("#sendPos response sent to: "+sSmsOriginator);//Debugging message
        }

        //If "#startApp" SMS message is received, call timerButler to
        //  start the timerRunnable process to periodically update upstream server
        else if (sSmsReceived.compareTo("#startApp") == 0)
        {
            System.out.println("SMS: #startApp command received");//Debugging message
            timerButler.startHandler();
            smsManager.sendTextMessage(sSmsOriginator, null, "#startApp acknowledged", null, null);
            System.out.println("#startApp response sent to: "+sSmsOriginator);//Debugging message
        }

        //If "#stopApp" SMS message is received, call timerButler to
        //  stop the timerRunnable process
        else if (sSmsReceived.compareTo("#stopApp") == 0)
        {
            System.out.println("SMS: #stopApp command received");//Debugging message
            timerButler.stopHandler();
            smsManager.sendTextMessage(sSmsOriginator, null, "#stopApp acknowledged", null, null);
            System.out.println("#stopApp response sent to: "+sSmsOriginator);//Debugging message
        }

        //If SMS message does not match a special command string, ignore it
        else
        {
            System.out.println("SMS: Invalid command! -Ignoring");//Debugging message
        }
    }
}


TimerButler.java
Code:
package my.testapp;

import android.content.Context;
import android.os.AsyncTask;
import android.os.CountDownTimer;
import android.widget.TextView;
import android.os.Handler;
import android.os.Handler.Callback; //I have plans for this later.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import static java.lang.Thread.*;


public class TimerButler //--commented out-- extends AsyncTask <Void, Void, Void>
{
    //Declare some variables
    private static String sAppStarted = "Running";
    private static String sAppStopped = "Stopped!";
    private static TextView tvAppStatInfo;
    //--commented out-- private long startTime = 0;
    private int index = 0;
    private int index2 = 0;
    private static boolean isRunning = false;
    private boolean killMe;
    //--commented out-- private static Runnable thisRunnable;
    //--commented out-- private static String threadID;

    //Instantiate a sendMessage object
    private static SendMessage sendMessage = new SendMessage();

    //Instantiate a new handle for time based operations
    private Handler timerHandler = new Handler();

    //--commented out-- private ExecutorService executorService = Executors.newSingleThreadExecutor();

    //Instantiate and define a runnable thread
    private Runnable timerRunnable = new Runnable()
    {
        @Override
        public void run()
        {
                //Keep updating this value while thread is running
                //Prevents startHandler from calling multiple instances of this
                isRunning = true;

                //Update a TextView to indicate status
                if (index < 5)
                {
                    tvAppStatInfo.setText(sAppStarted);
                    sAppStarted = sAppStarted + ".";
                    index++;
                }
                else
                {
                    sAppStarted = "Running";
                    index = 0;
                }


                //Call the sendMessage method every 30 seconds to update the upstream server
                if (index2 < 29)
                {
                    index2++;
                }
                else
                {
                    sendMessage.sendMessage();
                    index2 = 0;
                }


                timerHandler.postDelayed(this, 1000);
            }
    };//End runnable


    //--commented out-- private Future timerRunnableFuture = executorService.submit(timerRunnable);

    //A handler method to start the runnable
    public void startHandler()
    {
        if (isRunning == false) {
            tvAppStatInfo = (TextView) ((MainActivity)ContextHandler.getC()).findViewById(R.id.tvAppStatInfo);
            timerHandler.postDelayed(timerRunnable, 0);
            //--commented out-- thisRunnable = timerRunnable;
            //--commented out-- threadID = thisRunnable.toString();
            System.out.println("Starting Application");
            System.out.println("Thread " + timerRunnable + " started.\n\n");

        } else {
            System.out.println("Application thread is already running");
        }
    }

    //A handler method to stop the runnable
    public void stopHandler()
    {
      // --commented out-- if (isRunning == true)
      //--commented out--  {
            tvAppStatInfo = (TextView) ((MainActivity)ContextHandler.getC()).findViewById(R.id.tvAppStatInfo);

            //--commented out-- thisThread.interrupt();
            timerHandler.removeCallbacks(timerRunnable);
            System.out.println("Stopping "+timerRunnable+"\n\n");


            tvAppStatInfo.setText(sAppStopped);
            isRunning = false;
      //--commented out--  }
     //--commented out--   else
      //--commented out--  {
          //--commented out--  System.out.println("Application thread is not running");
      //--commented out--  }

    }
}









/*===============Leftover code from Other Approaches - some snippets may be useful still======================*/

    /*===========================================Limited functionality using Async====================
    private static String sAppStarted = "Running";
    private static String sAppStopped = "Stopped!";
    private static TextView tvAppStatInfo;
    private ContextHandler contextHandler = new ContextHandler();
    private int index = 0;
    private int index2 = 0;
    private static boolean isRunning = false;
    private Thread thisThread;
    private long threadID;

    private Context c;

    //Instantiate a sendMessage object
    private static SendMessage sendMessage = new SendMessage();



    @Override
    protected Void doInBackground(Void... params)

    {
        if(!isCancelled())
        {
            c = contextHandler.getC();
            tvAppStatInfo = (TextView) ((MainActivity) c).findViewById(R.id.tvAppStatInfo);
            countDownA(c);
            thisThread = currentThread();
            threadID = thisThread.getId();
            System.out.println("Background Thread " + threadID + " started");


        }
        return null;
    }



    public void countDownA (Context c)
    {
        if (isRunning)
        {
            long lCountDownDuration = 30000;
            long lCountDownTick = 1000;
            new CountDownTimer(lCountDownDuration - 1000, lCountDownTick) {
                @Override
                public void onTick(long millisUntilFinished) {
                    if (index < 5) {
                        sAppStarted = sAppStarted + ".";
                        index++;
                    } else
                    {
                        sAppStarted = "Running";
                        index = 0;
                    }

                    tvAppStatInfo.setText(sAppStarted);

                }

                @Override
                public void onFinish() {
                    tvAppStatInfo.setText("Sending!");
                    sendMessage.sendMessage();
                    countDownKickStart();
                }
            }.start();
        }
    }


    private void countDownKickStart ()
    {

        long lCountDownKickstart = 1000;
        long lKickstartTick = 1000;
        new CountDownTimer(lCountDownKickstart, lKickstartTick)
        {
            @Override
            public void onTick(long millisUntilFinished)
            {
                //Method is inherited abstract so we must give it something to do
                byte aByte;
            }

            @Override
            public void onFinish()
            {
                if(isRunning) {
                    sAppStarted = "Running";
                    countDownA(c);
                }
                else
                {
                    tvAppStatInfo.setText("Stopped!");
                }
            }
        }.start();
    }

    public void startTimer()
    {
        isRunning = true;
        cancel(false);
        System.out.println("isCanceled "+isCancelled());
        doInBackground();
    }

    public void stopTimer()
    {
        cancel(true);
        isRunning = false;
        System.out.println("Stop Command received");
        System.out.println("icCanceled"+isCancelled());
    } */




    /*====================working with limited functionality... does NOT extend Async===========================


    //Declare some variables
    private static String sAppStarted = "Running";
    private static String sAppStopped = "Stopped!";
    private static TextView tvAppStatInfo;
    private long startTime = 0;
    private int index = 0;
    private int index2 = 0;
    private static boolean isRunning = false;
    private boolean killMe;
    private Thread thisThread;
    private static long threadID;

    private ContextHandler contextHandler = new ContextHandler();

    //Instantiate a sendMessage object
    private static SendMessage sendMessage = new SendMessage();

    //Instantiate a new handle for time based operations
    private Handler timerHandler = new Handler();

    //private ExecutorService executorService = Executors.newSingleThreadExecutor();

    //Instantiate and define a runnable thread
    private Runnable timerRunnable = new Runnable() {
        @Override
        public void run() {

            if (killMe = true);
            {
                timerHandler.removeCallbacks(timerRunnable);
                isRunning =false;
            }

            isRunning = true;


            //Update a TextView to indicate status
            if (index < 5) {
                tvAppStatInfo.setText(sAppStarted);
                sAppStarted = sAppStarted + ".";
                index++;
            } else {
                sAppStarted = "Running";
                index = 0;
            }


            //Call the sendMessage method every 30 seconds
            if (index2 < 29) {
                index2++;
            } else {
                sendMessage.sendMessage();
                index2 = 0;
            }


            timerHandler.postDelayed(this, 1000);
        }
    };//End runnable

   // private Future timerRunnableFuture = executorService.submit(timerRunnable);

    //A method to start the handler thread.  Accepts a Context passed from MainActivity call
    //   so that this non-activity class can update the TextViews
    public void startHandler(Context c)
    {
        killMe = false;


        if (isRunning == false)
        {
            tvAppStatInfo = (TextView) ((MainActivity)c).findViewById(R.id.tvAppStatInfo);
            timerHandler.postDelayed(timerRunnable, 0);
            thisThread = currentThread();
            threadID = thisThread.getId();
            System.out.println("Thread "+threadID+" started.");

            System.out.println("Starting Application");
        }
        else
        {
            System.out.println ("Application thread is already running");
        }
    }



    public void stopHandler(Context c)
    {
        if (isRunning == true)
        {
            tvAppStatInfo = (TextView) ((MainActivity)c).findViewById(R.id.tvAppStatInfo);

            //thisThread.interrupt();
            timerHandler.removeCallbacks(timerRunnable);
            System.out.println ("Stopping Application");

            tvAppStatInfo.setText(sAppStopped);
            isRunning = false;
            killMe = true;
        }
        else
        {
            System.out.println ("Applicaiton thread is not running");
        }

    }



=======================================end limited functionality solutions =============*/

ContextHandler.java
Code:
package my.testapp;

import android.content.Context;


//A simple class for passing the context from Main in situations where
//  it would be impossible to pass the context in directly
public class ContextHandler
{
    //This is declared 'static' since we will be passing the context in during
    //  the call from MainActivity and passing the context out to other non-activity classes.
    private static Context C;

    public static Context getC()
    {
        return C;
    }

    public static void setC(Context c)
    {
        C = c;
    }
}


****LogCat Output****
05-09 08:05:42.731 5037-5037/my.testapp I/Adreno-EGL: <qeglDrvAPI_eglInitialize:320>: EGL 1.4 QUALCOMM build: (CL3776187)
OpenGL ES Shader Compiler Version:
Build Date: 10/15/13 Tue
Local Branch:
Remote Branch: partner/upstream
Local Patches:
Reconstruct Branch:
05-09 08:05:42.771 5037-5037/my.testapp D/OpenGLRenderer: Enabling debug mode 0
05-09 08:05:42.922 5037-5037/my.testapp I/ActivityManager: Timeline: Activity_idle id: android.os.BinderProxy@418a9048 time:330054410

==I started the runnable by clicking the UI button=
05-09 08:05:58.377 5037-5037/my.testapp I/System.out: StartApp UI button clicked
05-09 08:05:58.377 5037-5037/my.testapp I/System.out: Starting Application
05-09 08:05:58.387 5037-5037/my.testapp I/System.out: Thread my.testapp.TimerButler$1@41915678 started.

==I clicked the UI start button a second time to verify some code I wrote to prevent multiple
instances of the runnable==

05-09 08:06:03.121 5037-5037/my.testapp I/System.out: StartApp UI button clicked
05-09 08:06:03.121 5037-5037/my.testapp I/System.out: Application thread is already running

==I clicked the UI stop button to stop the runnable==
==Just take my work for it this worked as expected
given the order of events thus far==
05-09 08:06:08.066 5037-5037/my.testapp I/System.out: StopApp UI button clicked
05-09 08:06:08.066 5037-5037/my.testapp I/System.out: Stopping my.testapp.TimerButler$1@41915678

==I started the runnable this time using an SMS command==
05-09 08:06:51.118 5037-5037/my.testapp I/System.out: SMS From: 5552221005 : #startApp
05-09 08:06:51.118 5037-5037/my.testapp I/System.out: SMS: #startApp command received
05-09 08:06:51.118 5037-5037/my.testapp I/System.out: Starting Application

==The app runnable thread started as expected and the app appears to function normally==
05-09 08:06:51.118 5037-5037/my.testapp I/System.out: Thread my.testapp.TimerButler$1@41aa23e8 started.
05-09 08:06:51.118 5037-5037/my.testapp I/System.out: #startApp response sent to: 5552221005

==This is another debug statement that indicates the sentMessage method was called==
==In this case it was triggered by they runnable. Evidence the program is executing==

05-09 08:07:20.196 5037-5037/my.testapp I/System.out: Executing sendMessage()

==Here I sent another SMS command to stop the runnable==
==It responds and for about a split second the indicator on the UI I have set up
shows the app stopped, however within no-time, it retarts itself and the app keeps on running==

05-09 08:07:25.822 5037-5037/my.testapp I/System.out: SMS From: 5552221005 : #stopApp
05-09 08:07:25.822 5037-5037/my.testapp I/System.out: SMS: #stopApp command received
05-09 08:07:25.822 5037-5037/my.testapp I/System.out: Stopping my.testapp.TimerButler$1@41baaf20
05-09 08:07:25.832 5037-5037/my.testapp I/System.out: #stopApp response sent to: 5552221005

==The screen went to sleep here triggering this message==
05-09 08:07:41.357 5037-5037/my.testapp I/ActivityManager: Timeline: Activity_idle id: android.os.BinderProxy@418a9048 time:330172848

==Remember the last thing I did was try to stop the runnable thread that was initially
started with an SMS command. Here I press the UI start button==
==The app responds with the appropriate response considering the runnable never
actually stopped==

05-09 08:07:47.653 5037-5037/my.testapp I/System.out: StartApp UI button clicked
05-09 08:07:47.653 5037-5037/my.testapp I/System.out: Application thread is already running

==Every 30 seconds the runnable calls the sendMessage==
05-09 08:07:50.346 5037-5037/my.testapp I/System.out: Executing sendMessage()

==This is an attempt to stop the runnable using the UI however when
runnable is stared in response to an SMS command this becomes uneffective as well==
==Remember: This works when the runnable is trigger by the UI start button - no problems==

05-09 08:07:55.090 5037-5037/my.testapp I/System.out: StopApp UI button clicked
05-09 08:07:55.090 5037-5037/my.testapp I/System.out: Stopping my.testapp.TimerButler$1@41915678

==Just to verify the runnable is running in spite of being instructed to stop==
05-09 08:08:08.303 5037-5037/my.testapp I/System.out: StartApp UI button clicked
==Note the reponse==
05-09 08:08:08.303 5037-5037/my.testapp I/System.out: Application thread is already running

==And the fact the method continues to be called every 30 seconds==
05-09 08:08:20.445 5037-5037/my.testapp I/System.out: Executing sendMessage()
05-09 08:08:50.545 5037-5037/my.testapp I/System.out: Executing sendMessage()

==An additional SMS start command encounters the same logic and returns
an appropriate response for when runnable is running==

05-09 08:09:16.009 5037-5037/my.testapp I/System.out: SMS From: 5552221005 : #startApp
05-09 08:09:16.009 5037-5037/my.testapp I/System.out: SMS: #startApp command received
==Note the response
05-09 08:09:16.009 5037-5037/my.testapp I/System.out: Application thread is already running
05-09 08:09:16.009 5037-5037/my.testapp I/System.out: #startApp response sent to: 5552221005

==And again the app continues to run==
05-09 08:09:20.704 5037-5037/my.testapp I/System.out: Executing sendMessage()


Whew!!! That was alot and yes I know I'm a shitty programmer but I'm trying to learn so cut me some slack. ;)

If someone can nudge me in the right direction here I'll buy you an e-Beer!:beer:

Thanks!!
-JR
 
Last edited:

Mr Evil

Senior member
Jul 24, 2015
464
187
116
mrevil.asvachin.com
There's a lot of code there, and I don't know anything about Android, but the problem of not being able to stop the thread after it has been started by SMS probably comes down to each SMS instantiating a new instance of IncomingSms, which will then instantiate a new SmsButler, then TimerButler, which isn't the same one that started the thread in the first place, so it can't stop it because it doesn't have a reference to it.

Without wanting to put too much thought into what a good design for this would be, a quick fix would be to make TimerButler a singleton, so there is only one thread to start and stop.
 
  • Like
Reactions: JoLLyRoGer

purbeast0

No Lifer
Sep 13, 2001
52,856
5,729
126
I don't have anything to help your problem here, but I do have some advice to help your coding - remove a boatload of those comments. They take up way too much vertical space and most of them are completely useless. You literally have comments that take up 2 lines of code that say "initializing variable to 1234 and the other to null" and then your next 2 lines of code do that. Those types of comments are completely unnecessary. If you make your code readable, comments aren't even necessary at all.
 

smackababy

Lifer
Oct 30, 2008
27,024
79
86
I don't have anything to help your problem here, but I do have some advice to help your coding - remove a boatload of those comments. They take up way too much vertical space and most of them are completely useless. You literally have comments that take up 2 lines of code that say "initializing variable to 1234 and the other to null" and then your next 2 lines of code do that. Those types of comments are completely unnecessary. If you make your code readable, comments aren't even necessary at all.
Yeah, unless this is homework and you're required to comment.
 

JoLLyRoGer

Diamond Member
Aug 24, 2000
4,154
4
81
Gents,

Thanks for the replies so far (and the comments on my comments!)

Typically I don't comment as much but since Android is new to me most of what you are reading is for my own edification. No its not homework, but six months from now I don't want to have to go back through and try and remember why I did something a particular way. Maybe it's overboard, but I'm not an every-day programmer so it's just something I do to keep myself straight mostly.

I'm going to try and follow Mr. Evil's suggestion and see what comes of it. I'll post back with the results for sure.

Keep em coming!:)
-JR
 
Sep 29, 2004
18,665
67
91
If you make your code readable, comments aren't even necessary at all.

Accept in large sequential methods. Then they are useful in order to break the method into chunks visually. The only other time I add comments is when I have engineering notes to add like (method X in API Y does not function as javadoc describes and actually does this: bla bla bla". Or if I spend half a day trying to figure out something, I'll leave notes behind for anyone maintaining the code.

OP: Also, use standard javadoc conventions for method comments. /** ......
 
  • Like
Reactions: JoLLyRoGer
Sep 29, 2004
18,665
67
91
And on threads, don't actually call Thread.stop(). I think the javadoc even says to avoid that. You should set a flag that lets the run method exit cleanly when the thread needs to be shut down.
 
  • Like
Reactions: JoLLyRoGer

purbeast0

No Lifer
Sep 13, 2001
52,856
5,729
126
Accept in large sequential methods. Then they are useful in order to break the method into chunks visually. The only other time I add comments is when I have engineering notes to add like (method X in API Y does not function as javadoc describes and actually does this: bla bla bla". Or if I spend half a day trying to figure out something, I'll leave notes behind for anyone maintaining the code.

OP: Also, use standard javadoc conventions for method comments. /** ......
I disagree with your example. If you have a function that is long enough for making comments to explain these "chunks" then you should break the function up into multiple smaller functions that each do a "chunk" of functionality.

I'm all for comments when necessary, just not when they are unnecessary.
 

JoLLyRoGer

Diamond Member
Aug 24, 2000
4,154
4
81
Well I tried the singleton approach and nothing seems to change the behavior.
So I figured out a work-around that gets me to the desired result and now I can't believe I spent three days chasing my tail when the solution is so simple!

Ultimately the problem revolves around the fact that calling the timerButler.startHandler() from SmsButler results in creating a thread in a memory address that the MainActivity buttons cannot address, I decided to just click the MainActivity Start and Stop buttons programmatically using .performClick().

So what I did was set up access to the MainActivity button objects inside of SmsButler like this:
Code:
private final Button bStart = (Button) ((MainActivity)ContextHandler.getC()).findViewById(R.id.bStart);
private final Button bStop = (Button) ((MainActivity)ContextHandler.getC()).findViewById(R.id.bStop);
private final Button bSend = (Button) ((MainActivity)ContextHandler.getC()).findViewById(R.id.bSend);

And then rewrote this for each event that can be button driven:
Code:
else if (sSmsReceived.compareTo("#startApp") == 0)
       {
           System.out.println("SMS: #startApp command received");//Debugging message
           timerButler.startHandler();
           smsManager.sendTextMessage(sSmsOriginator, null, "#startApp acknowledged", null, null);
           System.out.println("#startApp response sent to: "+sSmsOriginator);//Debugging message
       }

To read this:
Code:
else if (sSmsReceived.compareTo("#startApp") == 0)
{
    System.out.println("SMS: #startApp command received");//Debugging message
    bStart.performClick();
    smsManager.sendTextMessage(sSmsOriginator, null, "#startApp acknowledged", null, null);
    System.out.println("#startApp response sent to: "+sSmsOriginator);//Debugging message
}

Might not be the sexiest approach but it works like a champ!



-JR