android-intentsharedpreferencesextrasintermittentstartactivityforresult

Android getSharedPreferences startActivityForResult intermittent error


I have a game whose main activity calls three other activities using startActivityForResult -- the first (SignInActivity) returns a user name, or allows the creation of a new one; the second (LevelChooser) uses getSharedPreferences to find a preferences file with that user's name, or creates a new one, displays the user's progress so far (levels unlocked, stars earned), and allows the user to choose to play any unlocked level; the third (GameActivity) updates the user's preferences file if the level is completed successfully before going back (via main) to LevelChooser. In LevelChooser I have overridden onBackPressed to return you to the SignInActivity; in GameActivity it is onFinish that is overridden, so that you go back to the LevelChooser regardless of how it happens.

Now, nine times out of ten this is all working exactly as intended, but sometimes it doesn't: sometimes, instead of seeing the user's actual stars and levels, the LevelChooser shows a set of incorrect and theoretically impossible values (e.g. one level shown as locked, but completed with three stars). This often (but not always) happens if you choose a level and then back out of it when you first open the game: it will then allow you to play any level shown as unlocked, but the GameActivity fails to save your result if you complete the level, and the same wrong levels are displayed when you come back to LevelChooser; alternatively, if you back out of LevelChooser and re-choose the same user name, it goes back to behaving as expected. I have also managed to reproduce the error by repeatedly starting levels and backing out of them -- if you try enough times it eventually goes wrong. For my own user name (and, I think, for all users) the wrong information is always the same, i.e. the problem is intermittent but, when it occurs, not random.

I have tried debugging but, for some reason, (a) the problem only happens on my phone, not on the emulator, and (b) when debugging (as opposed to running) on my phone it either works correctly or else, if it goes wrong, simply terminates (AFAIR without even the "X has stopped" dialog) instead of displaying the wrong level screen. The only thing I have managed to see in debug is that the onCreate of the LevelChooser activity is sometimes executed more than once.

Because the problem is intermittent and not straightforwardly reproducible, I am wondering if I've unwittingly assumed that some asynchronous process has/will have completed in a timely, linear fashion, and that it usually (but not always) obliges; or otherwise, I'm thinking that I've failed to understand something relevant and significant about the Activity lifecycle. Otherwise, I'm stumped and guessing.

main Activity:

public class MainActivity extends AppCompatActivity {

public ImageView splash;
private int GET_USER_NAME_CODE = 0;
private int GET_LEVEL_CODE = 1;
private int PLAY_GAME_CODE = 2;
private String user;
private int level;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // remove title
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.activity_splash_screen);

    splash = (ImageView) findViewById(R.id.splashView);

    splash.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Intent sign_intent = new Intent(MainActivity.this, SignInActivity.class);
            startActivityForResult(sign_intent, GET_USER_NAME_CODE);
        }
    });

}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
    super.onActivityResult(requestCode, resultCode, data);
    // check if the request code is same as what is passed
    if(requestCode==GET_USER_NAME_CODE)
    {
        user=data.getStringExtra("USER");
        Intent intent = new Intent(MainActivity.this, LevelChooser.class);
                intent.putExtra("user", user);
                startActivityForResult(intent, GET_LEVEL_CODE);
    }
    else {
        if(requestCode==GET_LEVEL_CODE) {
            level=data.getIntExtra("LEVEL", 0);
            if(level==-1) {
                Intent sign_intent = new Intent(MainActivity.this, SignInActivity.class);
                startActivityForResult(sign_intent, GET_USER_NAME_CODE);
            }
            else {
                Intent intent = new Intent(MainActivity.this, GameActivity.class);
                intent.putExtra("user", user);
                intent.putExtra("level", level);
                startActivityForResult(intent, PLAY_GAME_CODE);
            }
        }
        else {
            if(requestCode==PLAY_GAME_CODE) {
                user=data.getStringExtra("user");
               Intent intent = new Intent(MainActivity.this, LevelChooser.class);
                intent.putExtra("user", user);
                startActivityForResult(intent, GET_LEVEL_CODE);
            }
        }
    }
}

LevelChooser:

public class LevelChooser extends AppCompatActivity {

private String user;
private ImageView[] level;
private Boolean[] locked;
private int[] stars;
private SharedPreferences userprefs;
private SharedPreferences.Editor prefseditor;
private Boolean createNewPrefsFile = false;
private int tempResIdVisible;
private int tempResIdInvisible;
private ImageView tempView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.levelchooser);
}

@Override
protected void onStart() {
    super.onStart();


        level = new ImageView[21];
        locked = new Boolean[21];
        stars = new int[21];

        user = getIntent().getStringExtra("user");
        userprefs = getSharedPreferences(user, MODE_PRIVATE);
        prefseditor = userprefs.edit();

        //level numbers for views etc start from 1 to match images etc
        level[0] = null;
        locked[0] = null;
        stars[0] = 0;

        locked[1] = false;
        level[1] = (ImageView) findViewById(R.id.level1);
        level[1].setOnClickListener(new LevelClickListener(level[1], 1));
        if (!userprefs.contains("stars1")) {
            createNewPrefsFile = true;
        }
        stars[1] = userprefs.getInt("stars1", 0);
        if (stars[1] != 0) {
            for (int j = 1; j < 4; j++) {
                tempResIdInvisible = getResources().getIdentifier("stars" + j + "_1", "id", getPackageName());
                tempView = (ImageView) findViewById(tempResIdInvisible);
                tempView.setVisibility(View.INVISIBLE);
            }
            tempResIdVisible = getResources().getIdentifier("stars" + stars[1] + "_1", "id", getPackageName());
            tempView = (ImageView) findViewById(tempResIdVisible);
            tempView.setVisibility(View.VISIBLE);
        }

        for (int i = 2; i < 21; i++) {
            locked[i] = userprefs.getBoolean("locked" + i, true);
            if (locked[i]) {
                tempResIdVisible = getResources().getIdentifier("padlock" + i, "id", getPackageName());
                tempResIdInvisible = getResources().getIdentifier("level" + i, "id", getPackageName());
            } else {
                tempResIdVisible = getResources().getIdentifier("level" + i, "id", getPackageName());
                tempResIdInvisible = getResources().getIdentifier("padlock" + i, "id", getPackageName());
                level[i] = (ImageView) findViewById(tempResIdVisible);
                level[i].setOnClickListener(new LevelClickListener(level[i], i));
            }
            tempView = (ImageView) findViewById(tempResIdVisible);
            tempView.setVisibility(View.VISIBLE);
            tempView = (ImageView) findViewById(tempResIdInvisible);
            tempView.setVisibility(View.INVISIBLE);

            stars[i] = userprefs.getInt("stars" + i, 0);
            if (stars[i] != 0) {
                for (int j = 1; j < 4; j++) {
                    tempResIdInvisible = getResources().getIdentifier("stars" + j + "_" + i, "id", getPackageName());
                    tempView = (ImageView) findViewById(tempResIdInvisible);
                    tempView.setVisibility(View.INVISIBLE);
                }
                tempResIdVisible = getResources().getIdentifier("stars" + stars[i] + "_" + i, "id", getPackageName());
                tempView = (ImageView) findViewById(tempResIdVisible);
                tempView.setVisibility(View.VISIBLE);
            }
        }

        if (createNewPrefsFile) {
            for (int i = 1; i < 21; i++) {
                prefseditor.putBoolean("locked" + i, locked[i]);
                prefseditor.putInt("stars" + i, stars[i]);
                prefseditor.commit();
            }
        }
}

GameActivity:

public class GameActivity extends AppCompatActivity implements TextToSpeech.OnInitListener {

//TTS Object
private TextToSpeech myTTS;
//TTS status check code
private int MY_DATA_CHECK_CODE = 0;
private int level;
private String user;
private PhonemeGroup levelGroup;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    level = getIntent().getIntExtra("level", 0);
    user = getIntent().getStringExtra("user");
    levelGroup = initializeLevels(level);

    Intent checkTTSIntent = new Intent();
    checkTTSIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
    startActivityForResult(checkTTSIntent, MY_DATA_CHECK_CODE);
}

@Override
public void finish() {
    Intent intent = new Intent();
    intent.putExtra("user", user);
    setResult(2, intent);

    super.finish();
}

@Override
public void onStop() {
    if (myTTS != null) {
        myTTS.stop();
    }
    super.onStop();
}

@Override
public void onDestroy() {
    if (myTTS != null) {
        myTTS.shutdown();
    }
    Button ok_button = (Button) findViewById(R.id.button);
    ok_button.setOnClickListener(null);
    ImageView tickImageView = (ImageView) findViewById(R.id.tickImageView);
    tickImageView.setOnClickListener(null);
    ImageView starsView = (ImageView) findViewById(R.id.starsImageView);
    starsView.setOnClickListener(null);

    super.onDestroy();

    unbindDrawables(findViewById(R.id.GameParentView));
    System.gc();
}

Screenshot of correct display:A screenshot of LevelChooser with no errors

Screenshot after choosing Level 1 above and then backing out:Screenshot of LevelChooser with errors


Solution

  • It turns out that the problem is not with getSharedPreferences() as such, but rather that the variable user returned by getIntent().getStringExtra() was sometimes -- under circumstances I have found hard to pin down or reproduce reliably -- null, as in this question. That string was then used as a parameter for getSharedPreferences(), resulting in a set of saved results for the user null. These results (shown in the second image in the question) were then displayed every time the problem recurred.

    The solution -- although it still doesn't explain why getStringExtra() should return null -- was to null-guard the extras by putting the following code into the onCreate of both the activities that required a valid user:

    if(getIntent().getExtras()==null || getIntent().getStringExtra("user")==null) {
        Intent intent = new Intent();
        setResult(0, intent);
        finish();
        }
    

    If this is executed before the layout for the new activity is loaded it returns to the Main Activity and tries again (so to speak) without the user seeing any problem.