ActivityRecording::Session - rapid short recordings corrupt files, crash app

Sorry for reposting this, but I sort of made a mess of the other thread. This time I have a store app, some sample code and a test case. And I have audited my code to make sure I check return codes where necessary and avoid calling methods that shouldn't be called for a given state.

SDK: CIQ 3.0.6
Device: FR935 (ROW), FW11.00

Reproduction procedure
Download the "Stop and Go" device app from the store.
https://apps.garmin.com/en-US/apps/3...8-f45584b0a117

This is a stopwatch that can record activities. (I liked the 935 stopwatch so much I thought I'd try to recreate it >_>)

1) Install app on 935.
2) Open app.
3) Press DOWN to enable recording. (The onscreen button prompt will change to "Record: On")
4) Press START twice, quickly. You should have about 250 ms on the stopwatch
5) Press BACK to save recording. The app will display a progressbar for half a second (more on this later)
6) As soon as you can, repeat Steps 4 and 5, as quickly as possible.
7) Exit app

You should now have two recorded activities of about 250 ms. Sometimes both activities will be okay. Other times, one activity will be corrupt (cannot open/delete it). Or one activity will be missing. Or the app will crash. You will probably have O_FILE.ERR in the root of the device file system.

The reason I have a hardcoded 500ms delay after saving is because I need it to avoid full device crashes. Without it, I can crash the device by doing the same "quick recording" test case as above. As explained in the other thread, I also want to avoid the case where I am unable to start the timer/recording immediately when the user presses START.

I can give you the full source code upon request, but I will excerpt it in the post below (since I'm sure the forum won't let me type it in this post.)

Thanks for reading!
  • var session; // the ActivityRecorder.Session object

    function initializeSession()
    {
    if (Toybox has :ActivityRecording) {
    if (session == null) {
    session = ActivityRecording.createSession(
    {
    :name=>"Stopwatch"//,
    //:sport=>ActivityRecording.SPORT_GENERIC, // set sport type
    //:subSport=>ActivityRecording.SUB_SPORT_GENERIC // set sub sport type
    }
    );
    }
    }
    }

    // called when timer is stopped/reset, and user presses START
    function start()
    {
    initializeSession();

    if (session)
    {
    if (!session.isRecording())
    {
    var r = session.start();

    //System.println("Session.start() returned " + r );
    if (!r)
    {
    // never happens, due to hardcoded 500ms delay between save and start
    return false;
    }
    }
    }
    // do some stopwatch-related stuff here
    // ...
    return true;
    }

    // called when timer is running, and user presses START
    function stop()
    {
    // ...
    if (session != null && session.isRecording())
    {
    var s = session.stop();
    //System.println("Session.stop() returned " + s);
    }
    // I don't return anything here because
    // I don't want to do anything different in the UI
    // if the recording fails to stop
    //
    (A stopwatch that won't stop is problematic)
    }

    // called when timer is stopped, and user presses BACK/LAP
    function save()
    {
    var success = true;
    if (session)
    {
    if (session.isRecording())
    {
    success = session.stop();
    //System.println("Session.stop() (2) returned " + success);
    }
    if (success)
    {
    success = session.save();
    //System.println("Session.save() returned " + success);
    if (success)
    {
    session = null;
    // used to call initializeSession() here,
    // but it seems to make little difference
    }
    }
    }
    // Have never seen this be anything but true
    return success;
    }


    When I uncomment the debug printlns and run in the simulator, start(), stop() and save() always return true. (Because of my hardcoded 500ms delay between save() and start()).

    Unfortunately I can't use them in the test case, because they make the problem go away.

    - But I am sure that start() returns true because if it didn't, I would display a "Wait..." progressbar for 2 seconds

    - And I am sure that save() return true because I have never seen it return false. (I have code which repeatedly tries to call save() if save() returns false. A previous version of the app had no hardcoded wait, and it would've displayed a "Save" progressbar only if save() failed, but I had never seen that progressbar under any circumstances. even when saving long recordings and/or when the subsequent start() would fail.)

    Either way, if save() returns false, the app will try for up to ten seconds to keep calling save() until it returns true, but I have never seen save() return false, so I assume it never does (in the kinds of situations I test). When I've crashed the device (without 500ms wait), save() still didn't return false.

    - And I'm sure that at least the second of two possible calls to stop() is succeeding because it's part of the save() check above.
    To clarify, I call stop() at most twice. The first time when the user stops the timer with START, at which point I ignore the return value, because there's no sensible action to take in the UI if stopping the timer fails.

    The second possible time is when the user press BACK to save the recording -- in this case if sessionIsRecording() returns true, I'll call stop() again, before calling save().

    Note, I used to call initializeSession() immediately after session.save(), but I don't anymore. I thought this might be part of the problem, but when I put the call back, I could still recreate the problem. It might've been a little harder to do.

    I'll also reiterate that I wish Session.save() would return false, while the activity is saving, as opposed to the next Session.start() returning false if you call it too soon. It would really be nice to be able to properly block the UI while the session is saving, but I don't see how it's possible.
  • Thanks for the additional info. I've linked this new thread to the ticket tracking this issue. Just so you know, based on the info in your previous post, I was able to reproduce most of the issues you brought up. I did see a device crash only once, but I got Logs and we will look into it. I did get the O_FILE.ERR in the root of the device, and I did see the corrupted FIT files that I cannot open. As for your request for an API like Session.canStart[] or Session.isBusy[], I think that sounds reasonable so I have made a request for that.
  • Stephen.ConnectIQ thanks! I was a little worried that I had actually reported some random issue in my own code instead of actual bugs. I guess I thought that my previous attempted workaround of trying to poll Session.start() was causing some of the problems.

    Okay, so I guess my workaround of waiting 500ms is def necessary for now, which is what I thought. Without that workaround, I had been able to crash the device very easily, but only with the one iteration of my code that I didn't save....

    Thanks for reading my walls 'o text.