Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Post
  • Reply
hooah
Feb 6, 2006
WTF?
I'm still having a hard time figuring out how to sync up polling (or at least recording) of two different motion sensors. I've tried only updating when the difference between curTime and the last update is exactly 100 (which I know shouldn't work, and it didn't), when the difference is within 5 ms of 100, and also when the difference between the last update time for each sensor is less than 100. None of these showed constant updates for both sensors; most of the time one would update, and the other would never show anything. What other ways can I do this? Or should I just dump all the data with timestamps and sort it out later?

Adbot
ADBOT LOVES YOU

baka kaba
Jul 19, 2003

PLEASE ASK ME, THE SELF-PROFESSED NO #1 PAUL CATTERMOLE FAN IN THE SOMETHING AWFUL S-CLUB 7 MEGATHREAD, TO NAME A SINGLE SONG BY HIS EXCELLENT NU-METAL SIDE PROJECT, SKUA, AND IF I CAN'T PLEASE TELL ME TO
EAT SHIT

From the sound of things you want to basically take a reading every 100ms or so? In that case you need to be able to poll something that gives values on demand. The thing about the sensors is they fire events when they want, so if you're expecting them to come on a particular schedule that's probably not going to work.

The way I've done it is to have a SensorWrangler object that basically registers sensors and handles the sensor event callbacks. When you get a particular event, you update some internal data in the object - it can be as simple as the last value you got, a running average, some complex filtering thing, whatever you like. Then you have other methods to poll the Wrangler, whenever you need to, and those can just pull out the current value held in the object. So the two sides are running on different update schedules, but you always have some current data when it's needed

The sensors should update whenever they have an event (which depends on the rate you set too), so if one isn't working then it might be your handling logic ignoring it. Also make sure you understand the sensors you're polling, they might be a bit weird and not providing data the way you expect. If you have some code that might help!

hooah
Feb 6, 2006
WTF?
Ok, I think I can figure most of that out, except where to put the Wrangler object. If I were doing some command-line thing (all I've ever done really), it'd be straight-forward, but should I put this class in one of the existing activities? Or is there a way to make some sort of thing that sits by itself and doesn't have an associated activity?

Here's the onSensorChanged method; this configuration has the acceleration updated less frequently than the rotation:
Java code:
public void onSensorChanged(SensorEvent event) {
    Sensor mySensor = event.sensor;

    if(mySensor.getType() == Sensor.TYPE_LINEAR_ACCELERATION){
        float x = event.values[0];
        float y = event.values[1];
        float z = event.values[2];

        long curTime = System.currentTimeMillis();
        long timeDiff = curTime - mAccelLastUpdate;
        if(timeDiff > 100) {

            float xAvg = Math.abs(x - mAccelLastX) / timeDiff;
            float yAvg = Math.abs(y - mAccelLastY) / timeDiff;
            float zAvg = Math.abs(z - mAccelLastZ) / timeDiff;

            mAccelLastUpdate = curTime;

            mLinXText.setText(Float.toString(xAvg));
            mLinYText.setText(Float.toString(yAvg));
            mLinZText.setText(Float.toString(zAvg));
        }
    }
    
    // Second if for the rotation vector sensor
}

baka kaba
Jul 19, 2003

PLEASE ASK ME, THE SELF-PROFESSED NO #1 PAUL CATTERMOLE FAN IN THE SOMETHING AWFUL S-CLUB 7 MEGATHREAD, TO NAME A SINGLE SONG BY HIS EXCELLENT NU-METAL SIDE PROJECT, SKUA, AND IF I CAN'T PLEASE TELL ME TO
EAT SHIT

Ohhh ok - see now you're getting into the more complicated area, which is having events and scheduled tasks running to provide you with updates. It doesn't have to be difficult, but there are a lot of different ways of doing this kind of thing, and some are probably better than others in your situation. There are people with way more experience than me who'll have better suggestions, but hey I've moved on from AsyncTask, let's have a look!

Basically with Android your code (by default) runs in the main thread, which is also the one that handles all the UI stuff - updating views and handling touch events, which is important. You don't want to do too much work on this thread, which means if you're going to be polling for sensor data updates, you can't be constantly checking it in a while loop or anything. You need to either schedule some kind of 'check the data and update the views' task every so often, or have it triggered externally by something running on another thread, that can just poke the main thread when it's time to update. I'm being intentionally vague but hopefully you get the general idea of what you need to do, and what the basic options look like

I think for what you're doing, you probably just want an update task that runs every 100ms on the main thread, which asks the Wrangler for some data and updates the display. Using the main thread's Handler is a simple way of doing it - you create a task and then give it to the Handler, which puts it on a queue to be dealt with at the appropriate time. Make the task add itself back to the queue at the end of its code, and you end up with a repeating scheduled job.

So you could do something like this:
Java code:
public class SensorWrangler implements SensorEventListener {

        private float[] currentData = new float[3];
        private SensorManager sensorManager;

        public SensorWrangler(Context context) {
            sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
            // register your sensors - really you want to register/unregister in a separate method,
	    // that you can call from onResume/onPause to pause and restart the sensors as required
            Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
            sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL);
        }


        @Override
        public void onSensorChanged(SensorEvent event) {
            // doot doot
            if (event.sensor.getType() == Sensor.TYPE_LINEAR_ACCELERATION) {
                currentData = Arrays.copyOf(event.values, event.values.length);
            }
        }


        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) { }


        public float[] getCurrentData() {
            return currentData;
        }

}
Java code:
public class MainActivity extends Activity {

    // call this on the main thread and it gets the main thread's Handler
    private Handler handler = new Handler();
    private SensorWrangler wrangler;

    private static final int UPDATE_DELAY = 100;
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView) findViewById(R.id.textview);
        wrangler = new SensorWrangler(getApplicationContext());
        // post the updating task so it will fire - giving it the usual 100ms delay here
        handler.postDelayed(updateTask, UPDATE_DELAY);
    }


    private Runnable updateTask = new Runnable() {
        public void run() {
            // poll for data and do whatever with it
            float[] data = wrangler.getCurrentData();
            textView.setText(String.format("X:%.4f\nY:%.4f\nZ:%.4f", data[0], data[1], data[2]));
            // throw the task back on the conveyor belt
            handler.postDelayed(this, UPDATE_DELAY);
        }
    };
}
(not tested at allruns on Androids for your pleasure )

That way, because you're registering the SensorManager on the main thread (it's initialised as part of your normal code with the Activity starting up), it'll send sensor update events to the main thread too. So your main thread is doing its thing, and every so often it gets a task to do - a sensor event to mess with, or your update task. You don't have to encapsulate the sensor stuff in its own object, I just feel like it's neater to work with :shobon: The advantage with doing it all in the activity is you can put your sensor registration/dereg straight in onPause and onResume. Whatever works for you basically

This might be totes cool and fine, but if you end up with too much work happening on the main thread (or you need much better timing accuracy for the polling, without possibly having to wait for other tasks to be dealt with) you might want to spin off some worker threads to handle those things separately. That makes things a bit trickier, there's a few more things to consider and more tools in the box to work with, but it's an option if you get to that point



e- I got (rightly) shouted at for promoting AsyncTasks and not knowing squat about concurrency and synchronisation before, so caveat emptor and all that, but if this is bad advice it'll be a learning experience for us all

edit again- decided to stop being lazy and wrote one that runs

baka kaba fucked around with this message at 01:42 on Jul 14, 2015

hooah
Feb 6, 2006
WTF?
Wow, that works great! Thank you so much! My only concern with this part is that I had unregistered the sensor listener when the user hit the stop button, since I'd read somewhere you should unregister your listeners whenever your view might go away. Do I need to worry about that now?

Moving on, I also had a question about the activity stack behavior (right terminology?). Currently, I have the main activity which just has the start button. That kicks off the DisplayData activity, which has a stop button, which creates a new MainActivity. I think creating this new MainActivity is what's causing my back stack to send the user back and forth between the DisplayData and MainActivity screens however many times the user has started/stopped "recording". How should I do this so the stop has the same behavior as the back button?

baka kaba
Jul 19, 2003

PLEASE ASK ME, THE SELF-PROFESSED NO #1 PAUL CATTERMOLE FAN IN THE SOMETHING AWFUL S-CLUB 7 MEGATHREAD, TO NAME A SINGLE SONG BY HIS EXCELLENT NU-METAL SIDE PROJECT, SKUA, AND IF I CAN'T PLEASE TELL ME TO
EAT SHIT

Yeah you need to explicitly stop the sensors when you're not using them, since they draw power. If you only need them running when the activity is visible, the easiest way is to start it from the activity's onResume method, and stop them in onPause. If you've encapsulated all your sensor business in that separate class, you can write wake/sleep methods that register and deregister everything it's using, and just call those from the activity's lifecycle methods. That way your wrangler object can deal with the details of what needs turning on and off, it's just a bit neater.

I left it out of the example (apart from the comment) but yeah, you'd do something like set up the wrangler in the activity's onCreate - you initialise the object but it's still 'off' (i.e. don't register a listener in the constructor). Then the activity's onResume fires, and there you can wake up the sensors by registering them. When your activity is going invisible/destroying, it'll call onPause and that's where you turn the stuff off.

(If you aren't familiar with the activity lifecycle it's a good idea to get an overview - onResume and onPaused are important ones because they're always called when the activity is 'appearing' and 'disappearing', which means creation/deletion and becoming visible)


There's a method on Activity you can call if you want to 'hit the back button', which is basically what the back button ends up triggering
http://developer.android.com/reference/android/app/Activity.html#onBackPressed()
Ok that link doesn't go to the method for some reason, but it did remind me that the lifecycle documentation on that page is way better

baka kaba fucked around with this message at 19:32 on Jul 14, 2015

hooah
Feb 6, 2006
WTF?
Alright, that seems to be working fine. Now I'm moving into the "write to file" portion of the app. I think I figured that out fine:

Java code:
String filename = "motion_file.txt";
FileOutputStream outputStream;
try {
    Log.d(TAG, this.getFilesDir().toString());
    outputStream = openFileOutput(filename, Context.MODE_WORLD_READABLE);
    StringBuilder myBuilder = new StringBuilder();
    myBuilder.append(System.currentTimeMillis());
    myBuilder.append(',');
    myBuilder.append(accelData[0]);
    myBuilder.append(',');
    myBuilder.append(accelData[1]);
    myBuilder.append(',');
    myBuilder.append(accelData[2]);
    myBuilder.append(',');
    myBuilder.append(rotData[0]);
    myBuilder.append(',');
    myBuilder.append(rotData[1]);
    myBuilder.append(',');
    myBuilder.append(rotData[2]);
    myBuilder.append('\n');

    outputStream.write(myBuilder.toString().getBytes());
    outputStream.close();

} catch (Exception e) {
    e.printStackTrace();
}
The reported file location is /data/data/my_app_name/files, but when I try to browse there with EStrongs, /data is empty. Where the hell is my file?

Volmarias
Dec 31, 2002

EMAIL... THE INTERNET... SEARCH ENGINES...
Hiding in app private storage. You don't have permissions to view /data. Write your file to external storage instead.

Fergus Mac Roich
Nov 5, 2008

Soiled Meat
This might be wishful thinking, but is there a tool that can audit my source code and find out what API calls I use from a given Android API level?

hooah
Feb 6, 2006
WTF?
I think I don't quite understand something about files in Java. I revised my code to create an appropriately-named folder in the Downloads folder (only one that seemed reasonable), but there's no file there:
Java code:
FileOutputStream outputStream;
try {
    if(isExternalStorageWritable()) {
        File myFile = getDataStorageDir("MotionCapture");
        String filename = myFile.getName();

        outputStream = openFileOutput(filename, Context.MODE_WORLD_READABLE);
        StringBuilder myBuilder = new StringBuilder();
        myBuilder.append(System.currentTimeMillis());
        myBuilder.append(',');
        myBuilder.append(accelData[0]);
        myBuilder.append(',');
        myBuilder.append(accelData[1]);
        myBuilder.append(',');
        myBuilder.append(accelData[2]);
        myBuilder.append(',');
        myBuilder.append(rotData[0]);
        myBuilder.append(',');
        myBuilder.append(rotData[1]);
        myBuilder.append(',');
        myBuilder.append(rotData[2]);
        myBuilder.append('\n');

        outputStream.write(myBuilder.toString().getBytes());
        outputStream.close();
    }
} catch (Exception e) {
    e.printStackTrace();
}

Volmarias
Dec 31, 2002

EMAIL... THE INTERNET... SEARCH ENGINES...
Several things.

1) Without knowing what directory getDataStorageDir is putting your file, we can't really help you with that.
2) openFileOutput opens a file in app private storage. Lets assume that getDataStorageDir is providing an externally visible directory.

code:
File myFile = getDataStorageDir("MotionCapture");
You're then getting the name of the file, not the path

code:
String filename = myFile.getName();
And opening an outputStream to a file with that name in app private storage.

code:
outputStream = openFileOutput(filename, Context.MODE_WORLD_READABLE);
WORLD_READABLE doesn't help if you don't have the absolute path. If you do, you may actually be able to pull it directly via adb pull. Regardless, I strongly advice you to use getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) in this particular case as your root directory.

To open the file for writing, I'd recommend using FileOutputStream with a file argument directly, e.g.

code:
File myFile = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS);
FileOutputStream fos = new FileOutputStream(myFIle);
...

hooah
Feb 6, 2006
WTF?
I was lazy and dumb and didn't include getDataStorageDir(), which calls Environment.getExternalStoragePublicDirectory(). I think the FileOutputStream(myFile) will be what I need though, thanks.

hooah
Feb 6, 2006
WTF?
I'm confused now. The Environment.getExternalStoragePublicDirectory() method returns a directory. How do I make a new file there (or write to an existing one once I've created the desired file)?

Edit: figured that out by looking at the documentation; I needed to create a file by giving the path and a file name to the File constructor. Now my question is, how do I append to that file once it's created? It seems that currently the data just gets overwritten each time.

hooah fucked around with this message at 23:45 on Jul 17, 2015

baka kaba
Jul 19, 2003

PLEASE ASK ME, THE SELF-PROFESSED NO #1 PAUL CATTERMOLE FAN IN THE SOMETHING AWFUL S-CLUB 7 MEGATHREAD, TO NAME A SINGLE SONG BY HIS EXCELLENT NU-METAL SIDE PROJECT, SKUA, AND IF I CAN'T PLEASE TELL ME TO
EAT SHIT

A File isn't actually a file, it describes where a hypothetical file might be (amongst other things). Point is the File object is more of a destination signpost, the FileOutputStream is the business end of putting the bits on the disk.

http://developer.android.com/reference/java/io/FileOutputStream.html

Check the constructors out!

hooah
Feb 6, 2006
WTF?
Ah, didn't read the documentation on that! This is going great. The only thing now is that it wrote the same motion data several times at the end of the file, but that's not a large concern. Thanks for all the help!

Volmarias
Dec 31, 2002

EMAIL... THE INTERNET... SEARCH ENGINES...

hooah posted:

Ah, didn't read the documentation on that! This is going great. The only thing now is that it wrote the same motion data several times at the end of the file, but that's not a large concern. Thanks for all the help!

Did you register your listener multiple times? Make sure you mirror your registrations with deregistrations.

hooah
Feb 6, 2006
WTF?
I don't think multiple registrations is the issue. It's just the last set of data repeated, with only the timestamp changing.

baka kaba
Jul 19, 2003

PLEASE ASK ME, THE SELF-PROFESSED NO #1 PAUL CATTERMOLE FAN IN THE SOMETHING AWFUL S-CLUB 7 MEGATHREAD, TO NAME A SINGLE SONG BY HIS EXCELLENT NU-METAL SIDE PROJECT, SKUA, AND IF I CAN'T PLEASE TELL ME TO
EAT SHIT

Maybe you're still polling the sensor handler object? The way I wrote it it'll keep giving out the last received data if you keep asking it. Your timestamps are from your polling activity, and not the timestamps of the actual sensor events, right?

To get a clean record you probably want to just stop polling when you want to stop getting data, since the polling is really your data acquisition behaviour as far as your app's concerned - the sensor object is really a data source. Just make sure you turn it off after you stop polling

hooah
Feb 6, 2006
WTF?

baka kaba posted:

Maybe you're still polling the sensor handler object? The way I wrote it it'll keep giving out the last received data if you keep asking it. Your timestamps are from your polling activity, and not the timestamps of the actual sensor events, right?

The timestamp is generated in updateViews, which is called by the updateTask Runnable you showed me. So I believe they're from the polling activity, if I understand correctly.

quote:

To get a clean record you probably want to just stop polling when you want to stop getting data, since the polling is really your data acquisition behaviour as far as your app's concerned - the sensor object is really a data source. Just make sure you turn it off after you stop polling

So I need to change the Runnable object? What do I need to read about to learn how to do that?

baka kaba
Jul 19, 2003

PLEASE ASK ME, THE SELF-PROFESSED NO #1 PAUL CATTERMOLE FAN IN THE SOMETHING AWFUL S-CLUB 7 MEGATHREAD, TO NAME A SINGLE SONG BY HIS EXCELLENT NU-METAL SIDE PROJECT, SKUA, AND IF I CAN'T PLEASE TELL ME TO
EAT SHIT

A Runnable is just some object that implements the Runnable interface. All it means is there's a run() method to call, so things like Threads can execute code just by calling run() on something they've been given. It's that easy!

Read up on the Looper if you want to know what's happening in the background, but this is basically how your program is working:
  • When onCreate is called you put the runnable on the message queue with a delayed timestamp
  • The main thread checks the message queue every so often to see if it has any tasks it needs to run
  • If your task is next, and its timestamp says it's due to run, the thread calls run() on the Runnable that was passed in
  • It executes the code that's in run() until that method returns. Part of that code is reposting the same runnable object on the message queue, with a delay.
  • Later the queue hits your runnable again and it's time to run it...
(I've glossed over some of the technical handling in the background, but that's basically all you need to think about)

So your 'polling task' is just that the runnable gets run every so often (and reschedules itself for the next run), and part of its code involves asking the sensor wrangler for data, and then updating views. (And writing to a file?) So stopping the polling really means stopping that task from running and rescheduling itself.

Generally you might want to implement some kind of cancellation system - it can be as simple as a 'cancelled' flag in the runnable, that it checks at the start of run() and decides whether to just return immediately (without rescheduling). If your task is more involved and can potentially get stuck waiting on something, you might want to have ways of interrupting it, but that's getting more into multithreading. This is one approach though - have the runnable monitor its own status somehow, and decide what to execute

The other possibility is in whatever you're using for the scheduling - since it's responsible for actually running your code, it might have a way of not running it anymore. And luckily, Handler provides
http://developer.android.com/reference/android/os/Handler.html#removeCallbacks(java.lang.Runnable)
You can basically pass your runnable task into that, it'll look through and see if it has that object on the queue, and if so it'll kick it off wherever it finds it. So no more of that task, until you drop it on the queue again

The code where you stop the polling would be a good place to close your FileOutputStream too I think

hooah
Feb 6, 2006
WTF?
Ok, that's a lot of good information. I basically understood how the Runnable got in the queue and what it did there, but I didn't understand the role of the Handler. The only experience with multi-threaded things I've had is making a toy shell for a class using execvp. Currently, I'm closing the FileOutputStream object after each thing is written. Should I move this to the exit method instead?

As a follow-up, I'm now noticing that the file continually gets appended to. I guess this is fine for now, but eventually I'd like to write to a new file each time the user presses the start button. What do I need to learn to go about doing that?

hooah fucked around with this message at 21:28 on Jul 18, 2015

baka kaba
Jul 19, 2003

PLEASE ASK ME, THE SELF-PROFESSED NO #1 PAUL CATTERMOLE FAN IN THE SOMETHING AWFUL S-CLUB 7 MEGATHREAD, TO NAME A SINGLE SONG BY HIS EXCELLENT NU-METAL SIDE PROJECT, SKUA, AND IF I CAN'T PLEASE TELL ME TO
EAT SHIT

Well right now you're only using the one main thread - it just constantly loops waiting for something to do, and it has a queue you can put tasks and stuff on. You can access that handler from another thread, and hand it stuff for the main thread to run, but right now you're just letting the main thread put stuff on its own queue. Having a looper is sort of necessary for apps - you don't want them to run code and stop, they need to be looping waiting for events to happen, like touches and menu selections and whatever. You're just adding more events for it to handle (your runnable tasks)

Someone else should probably chime in on this, but I/O is fairly slow, and constantly opening new streams is probably pretty inefficient, especially if you're doing it regularly. I'd expect it's probably better to hold a stream open for the duration, and just write to it as you get new polled data. You're not really meant to do I/O on the main thread at all, since it can take a while and make the thread slower and less responsive - it's probably fine for small amounts of data, but anything problematic and you'd need to do your I/O on another thread

As for your file thing, just create a new File and open the stream with that? Just come up with a way to name the file so there's no collisions - having a start timestamp in the name is probably useful

kitten smoothie
Dec 29, 2001

Yeah, doing I/O on the main thread is a no-no, so don't do that.

What I would do is instantiate a HandlerThread, get its Looper, and create a Handler from that. Save that.

Then when you get a new measurement, you post a Runnable onto the Handler, and that Runnable's work is to trigger writing the line to the file so it happens off the main thread.

hooah
Feb 6, 2006
WTF?
I'm not clear on how to make a Handler from a Looper. When I'm doing all this business with a HandlerThread, a Looper, and a(nother?) Handler, should these all be member variables of a class, or do some not need to be?

baka kaba
Jul 19, 2003

PLEASE ASK ME, THE SELF-PROFESSED NO #1 PAUL CATTERMOLE FAN IN THE SOMETHING AWFUL S-CLUB 7 MEGATHREAD, TO NAME A SINGLE SONG BY HIS EXCELLENT NU-METAL SIDE PROJECT, SKUA, AND IF I CAN'T PLEASE TELL ME TO
EAT SHIT

That's something I've wondered about - there's a few options for this kind of thing, like making an Executor with a single thread, or even spinning up a raw thread and just dropping data into a blocking queue. Is there a preferred way to handle simple, repetitive tasks like this?

baka kaba
Jul 19, 2003

PLEASE ASK ME, THE SELF-PROFESSED NO #1 PAUL CATTERMOLE FAN IN THE SOMETHING AWFUL S-CLUB 7 MEGATHREAD, TO NAME A SINGLE SONG BY HIS EXCELLENT NU-METAL SIDE PROJECT, SKUA, AND IF I CAN'T PLEASE TELL ME TO
EAT SHIT

hooah posted:

I'm not clear on how to make a Handler from a Looper. When I'm doing all this business with a HandlerThread, a Looper, and a(nother?) Handler, should these all be member variables of a class, or do some not need to be?

A looper is effectively a thing that runs forever on a thread, checking its message queue. The main thread has one created by default, and there's a command to get a reference to it. A Handler is really just an object associated with a particular looper, which allows you to post messages to it, which is what you're doing in your app.

Setting up a looper and Handler on a new thread is a bit of a faff - a HandlerThread is just taking care of the boilerplate for you and getting that all running from the get-go.

It's unlikely you'll need to hold a reference to an actual looper, but you might want to keep a handler reference so you can post stuff to a thread easily. You can get it if you have a reference to the thread itself, but you may as well keep a reference to the Handler if you're going to access it a bunch. Just make sure you shut it down when you're done with it

kitten smoothie
Dec 29, 2001

baka kaba posted:

That's something I've wondered about - there's a few options for this kind of thing, like making an Executor with a single thread, or even spinning up a raw thread and just dropping data into a blocking queue. Is there a preferred way to handle simple, repetitive tasks like this?

Truth be told if I were really to implement this, I'd probably just implement the sensor stuff as an rxjava observable, and then observe on Schedulers.io() so you get a thread pool for free that's reserved for I/O-bound work.

hooah
Feb 6, 2006
WTF?
I found the Handler constructor kitten smoothie was talking about, but suddenly I'm getting an error when I try to run my app over ADB: "Error: The activity 'MainActivity' is not declared in AndroidManifest.xml". I haven't changed that file in at least a couple days, and here it is:

XML code:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.user.accel_demo" >

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/title_activity_main"
            android:exported="true"
            android:screenOrientation="portrait" >
        </activity>
        <activity
            android:name=".DisplayData"
            android:label="@string/title_activity_display_message"
            android:parentActivityName=".MainActivity" >
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="com.example.user.accel_demo.MainActivity" />
        </activity>
    </application>

</manifest>
What got borked? Or is it something else?

Volmarias
Dec 31, 2002

EMAIL... THE INTERNET... SEARCH ENGINES...

hooah posted:

I found the Handler constructor kitten smoothie was talking about, but suddenly I'm getting an error when I try to run my app over ADB: "Error: The activity 'MainActivity' is not declared in AndroidManifest.xml". I haven't changed that file in at least a couple days, and here it is:

XML code:

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

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/title_activity_main"
            android:exported="true"
            android:screenOrientation="portrait" >
        </activity>
        <activity
            android:name=".DisplayData"
            android:label="@string/title_activity_display_message"
            android:parentActivityName=".MainActivity" >
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="com.example.user.accel_demo.MainActivity" />
        </activity>
    </application>

</manifest>

What got borked? Or is it something else?

Something else broke. Uninstall your package, clean your build, and restart your device.

kitten smoothie
Dec 29, 2001

Yeah, I've had this happen before and it usually seems to be a hosed up build in some regard. Cleaning and rebuilding has usually solved that for me.

hooah
Feb 6, 2006
WTF?
Ok, yeah, cleaning fixed it. Whew, thanks!

I'm still not getting this handler/looper stuff, even after doing some reading. My DisplayData class is currently like this:
Java code:
// Displays the motion data
public class DisplayData extends Activity {
    private SensorWrangler mWrangler;
    private HandlerThread mHandlerThread = new HandlerThread("dataThread");
    private Looper mLooper;
    private Handler mHandler;

    private static final long UPDATE_DELAY = 250L;
    private static final String TAG = "DisplayData";

    private Runnable updateTask = new Runnable() {
        public void run() {
            // poll for data and do whatever with it
            float[][] data = mWrangler.getCurrentData();
            updateViews(data);
            // throw the task back on the conveyor belt
            mHandler.postDelayed(this, UPDATE_DELAY);
        }
    };

    private TextView mLinXText;
    private TextView mLinYText;
    private TextView mLinZText;
    private TextView mRotXText;
    private TextView mRotYText;
    private TextView mRotZText;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // initialise the Wrangler (with the context it needs to get a SensorManager)
        mWrangler = new SensorWrangler(getApplicationContext());

        // Set up the and Looper and Handler
        mHandlerThread.start();
        mLooper = mHandlerThread.getLooper();
        mHandler = new Handler(mLooper);

        // post the updating task so it will fire - giving it the usual 100ms delay here
        mHandler.postDelayed(updateTask, UPDATE_DELAY);

        setContentView(R.layout.activity_display_data);
        mLinXText = (TextView) findViewById(R.id.LinXCoord);
        mLinYText = (TextView) findViewById(R.id.LinYCoord);
        mLinZText = (TextView) findViewById(R.id.LinZCoord);
        mRotXText = (TextView) findViewById(R.id.RotXCoord);
        mRotYText = (TextView) findViewById(R.id.RotYCoord);
        mRotZText = (TextView) findViewById(R.id.RotZCoord);

    }

    @Override
    protected void onPause(){
        super.onPause();
        mWrangler.sleep();
    }

    protected void onResume(){
        super.onResume();
        mWrangler.wake();
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    /** Called when the user clicks the Stop button */
    public void stopCollect(View view) {
        mHandler.removeCallbacks(updateTask);
        Intent intent = new Intent(this, MainActivity.class);
        startActivity(intent);
    }

    /* Checks if external storage is available for read and write */
    public boolean isExternalStorageWritable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            return true;
        }
        return false;
    }

    public File getDataStorageDir(String dataName) {
        // Get the directory for the user's public pictures directory.
        File file = new File(Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_DOWNLOADS), dataName);
//        if (!file.mkdirs()) {
//            Log.e(TAG, "Directory not created");
//        }
        return file;
    }

    /** Display the updated information */
    public void updateViews(float[][] data) {
        float[] accelData = data[0];
        float[] rotData = data[1];

        mLinXText.setText(Float.toString(accelData[0]));
        mLinYText.setText(Float.toString(accelData[1]));
        mLinZText.setText(Float.toString(accelData[2]));

        mRotXText.setText(Float.toString(rotData[0]));
        mRotYText.setText(Float.toString(rotData[1]));
        mRotZText.setText(Float.toString(rotData[2]));

        FileOutputStream outputStream;
        try {
            if(isExternalStorageWritable()) {
                File path = getDataStorageDir("MotionCapture");
                File file = new File(path, "data.txt");
                outputStream = new FileOutputStream(file, true);
                StringBuilder myBuilder = new StringBuilder();
                myBuilder.append(System.currentTimeMillis());
                myBuilder.append(',');
                myBuilder.append(accelData[0]);
                myBuilder.append(',');
                myBuilder.append(accelData[1]);
                myBuilder.append(',');
                myBuilder.append(accelData[2]);
                myBuilder.append(',');
                myBuilder.append(rotData[0]);
                myBuilder.append(',');
                myBuilder.append(rotData[1]);
                myBuilder.append(',');
                myBuilder.append(rotData[2]);
                myBuilder.append('\n');

                outputStream.write(myBuilder.toString().getBytes());
                outputStream.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
When I try to run this, I get an error saying "Only the original thread that created a view hierarchy can touch its views." This happens at the first line where I try to update the TextViews. I think this means that the HandlerThread is trying to update the TextViews which are owned by the UI thread, right? If that's so, then I need to make setters in the DisplayData class that change the TextViews, yes?

hooah fucked around with this message at 14:26 on Jul 20, 2015

kitten smoothie
Dec 29, 2001

Setters won't do it because you'll still be invoking those on the secondary thread.

code:
Handler mainHandler = new Handler(context.getMainLooper());

Get yourself a Handler for the main thread like that and then post a runnable for the UI update.

hooah
Feb 6, 2006
WTF?
Where does that mainHandler line go? I tried as a member variable and as a local variable in my DisplayData class's onCreate, but in both locations Android Studio told me it couldn't resolve context.

Also, could you elaborate a little on "post a runnable for the UI update"? The only runnable I have currently is the one baka kaba helped me with, which resides in a member variable for DisplayData and looks like this:

Java code:
private Runnable updateTask = new Runnable() {
    public void run() {
        // poll for data and do whatever with it
        float[][] data = mWrangler.getCurrentData();
        updateViews(data);
        // throw the task back on the conveyor belt
        mHandler.postDelayed(this, UPDATE_DELAY);
    }
};
Should I be modifying this Runnable or making a new one?

baka kaba
Jul 19, 2003

PLEASE ASK ME, THE SELF-PROFESSED NO #1 PAUL CATTERMOLE FAN IN THE SOMETHING AWFUL S-CLUB 7 MEGATHREAD, TO NAME A SINGLE SONG BY HIS EXCELLENT NU-METAL SIDE PROJECT, SKUA, AND IF I CAN'T PLEASE TELL ME TO
EAT SHIT

kitten smoothie posted:

Truth be told if I were really to implement this, I'd probably just implement the sensor stuff as an rxjava observable, and then observe on Schedulers.io() so you get a thread pool for free that's reserved for I/O-bound work.

http://reactivex.io/RxJava/javadoc/rx/Observable.html

:catstare:

Not something I'd casually bundle in for a task or anything, but this is pretty drat interesting. Reading an overview on it makes me hope Java 8 lambdas get integrated with Android somehow too, because drat is that a lot less boilerplate


hooah posted:

Where does that mainHandler line go? I tried as a member variable and as a local variable in my DisplayData class's onCreate, but in both locations Android Studio told me it couldn't resolve context.

Also, could you elaborate a little on "post a runnable for the UI update"? The only runnable I have currently is the one baka kaba helped me with, which resides in a member variable for DisplayData and looks like this:

Code isn't tied to a particular thread - if a thread is told to run some code, that code runs on that thread. It's like having a bunch of written instructions, and a worker to carry them out. You can get another worker in to help out, and you can give them the same instructions, and they'll do the same thing. But worker 2 might not be allowed to do certain things worker 1 can, like in this case, messing with the UI. Any code that tries to do that will only run on the main thread (which is why it's called the UI thread too)

Handlers and loopers work in a pair, and they're just a way of getting code to run on a particular thread. If you have a thread with a looper running on it, and you have a reference to that looper's handler, then you can hand over runnables and the like via the handler, and the looper will eventually run it. Like an in-tray for the thread, if you like. So to run stuff on the UI thread, you can get its handler and post to it. That's what the code I originally posted did - it was already running on the main thread anyway (when it started up the activity), and then it posts a runnable... to the main thread. That single thread was effectively sending itself work to do later

Your new code is creating a new thread (the HandlerThread), and posting tasks to that by using its handler. So all the code in the runnable you're posting is being executed by the new thread, not the UI thread. If you want to touch those views, the code doing that needs to be executed by the UI thread. So you could hold a reference to the main thread's handler, and post to that. Or since you have a reference to an Activity (your code is part of one, it's this) you can just call runOnUiThread which is basically just a convenience method.

You just need to get a handle on your code, and which thread is running it at any given time. Right now you're sending a runnable to your "data thread", so that thread will run the runnable's code. If there's code in there that needs to be run on the main thread, post it as a runnable to the UI thread. "Hey UI thread, run the stuff in here cheers"

code:
public class Wow implements Runnable {
	public void run() {
		// post this runnable to the DANGER THREAD's handler, all this runs on DANGER THREAD
		doSomething();
		doOtherThing();
		runOnUiThread(new Runnable() {
			public void run() {
				// This is only getting run on the UI thread, so it's safe!
				pokeUiElements();
			}
		});
	}
}
That kinda thing
e- although if this is firing a lot you probably just want to create that internal runnable once (Runnable viewUpdateRunnable = new ViewUpdateRunnable()) and just keep passing it, like you're doing with the other on

baka kaba fucked around with this message at 20:58 on Jul 20, 2015

hooah
Feb 6, 2006
WTF?
It's starting to make a little more sense now, thanks.

I'm trying to go for the second method you mentioned - create a Runnable class and instantiate it. But, how do I get the data (returned by SensorWrangler.getCurrentData()) into this new, unrelated(?) runnable?

Java code:
private Runnable updateFile = new Runnable() {
    // This guy has no idea what "data" is
    private UIUpdateRunnable UIUpdater = new UIUpdateRunnable();
    public void run() {
        // poll for data and do whatever with it
        float[][] data = mWrangler.getCurrentData();
        writeToFile(data);

        // How do I get data in here?
        runOnUiThread(UIUpdater);

        // throw the task back on the conveyor belt
        mDataHandler.postDelayed(this, UPDATE_DELAY);
    }
};

Jabor
Jul 16, 2010

#1 Loser at SpaceChem
Essentially, you want to encapsulate that data inside the Runnable you're sending back to the UI thread. You could do this in a few different ways, for example by making a custom subclass of Runnable that takes some parameters:

code:
class UpdateUiRunnable implements Runnable {
  private final int value;
  public UpdateUiRunnable(int value) {
    this.value = value;
  }
  public void run() {
    // do stuff with value
  }
}

But by far the easiest way is to let the language do all that heavy lifting for you, by simply capturing those values inside an anonymous class.

code:
public void backgroundThreadStuff() {
  final int value = computeTheValue();
  context.runOnUiThread(new Runnable() {
    public void run() {
      // because it was declared 'final', you can
      // just use 'value' here like normal.
    }
  }
}

hooah
Feb 6, 2006
WTF?

Jabor posted:

Essentially, you want to encapsulate that data inside the Runnable you're sending back to the UI thread. You could do this in a few different ways, for example by making a custom subclass of Runnable that takes some parameters:

code:
class UpdateUiRunnable implements Runnable {
  private final int value;
  public UpdateUiRunnable(int value) {
    this.value = value;
  }
  public void run() {
    // do stuff with value
  }
}
But by far the easiest way is to let the language do all that heavy lifting for you, by simply capturing those values inside an anonymous class.

code:
public void backgroundThreadStuff() {
  final int value = computeTheValue();
  context.runOnUiThread(new Runnable() {
    public void run() {
      // because it was declared 'final', you can
      // just use 'value' here like normal.
    }
  }
}

But the second way creates a new object every time, right? Is the language ease worth the overhead in this case?

kitten smoothie
Dec 29, 2001

If you're using the constructor to pass forward the values to put in the UI, then that approach requires a new object to be created too because you're going to have different values every time. So it's moot really, the approaches just differ in terms of convenience for the developer.

Don't sweat it.

Edit: and unless you want strange and hard to repro concurrency bugs that'll make you rip your hair out, don't even think of trying to reuse that runnable and add accessors for the value if you take the first approach.

kitten smoothie fucked around with this message at 15:55 on Jul 21, 2015

baka kaba
Jul 19, 2003

PLEASE ASK ME, THE SELF-PROFESSED NO #1 PAUL CATTERMOLE FAN IN THE SOMETHING AWFUL S-CLUB 7 MEGATHREAD, TO NAME A SINGLE SONG BY HIS EXCELLENT NU-METAL SIDE PROJECT, SKUA, AND IF I CAN'T PLEASE TELL ME TO
EAT SHIT

Yeah sorry, I forgot about the state you're passing in this case. I keep talking generally, just to give you an idea of how things can work.

There's probably no noticeable overhead at all in creating the objects - it's just that you'll be doing a lot of allocations, memory use will gradually creep up and it's probably best to avoid it if you can. It depends on your use case really - if the app is being used short-term, it won't really matter

Anyway this might be a good time to look at the Handler class and check out the other thing you can pass, Messages. It's basically a way of wrapping up some data that the destination thread can do something with, and the system keeps a pool of Message objects that it reuses, so you're not constantly allocating new objects that get used once

https://developer.android.com/training/multiple-threads/communicate-ui.html

I don't think it's the easiest example to follow but them's our docs!

baka kaba fucked around with this message at 16:01 on Jul 21, 2015

Adbot
ADBOT LOVES YOU

hooah
Feb 6, 2006
WTF?
Ok, I think I understand passing the data around passably. I'm going the constructor route since it makes the most sense to me, but I'm a little confused about how to update the text views in the UI. I initially thought to pass in the DisplayData object and grab its text views that way, but ran into a couple problems. One, since my updateFile runnable is nested in DisplayData, this doesn't refer to a DisplayData object. Two, the text views haven't been set to anything yet, since this is all happening before DisplayData's onCreate.

  • 1
  • 2
  • 3
  • 4
  • 5
  • Post
  • Reply