0

I have a problem with my activity. It work well for the first time i start but after that is crashed with this

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1842)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1860)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:650)
at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:609)

I try add this to my activity but it not working

@Override
protected void onSaveInstanceState(Bundle outState) {
    //No call for super(). Bug on API Level > 11.
}

When i use commitAllowingStateLoss() instead of commit() for changing Fragment the error now is Activity has been destroy. I have to close the app. I dont know why it run only in the first time of the activity is called.

Here is my function to change the fragment

private void showWaitingFragment() {
    FragmentManager fragmentManager = this.getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

    WaitingFragment fragment = new WaitingFragment();
    fragmentTransaction.replace(R.id.quiz_content_frame, fragment);

    fragmentTransaction.commit();
}

private void showAnswerFragment() {
    FragmentManager fragmentManager = this.getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

    AnswerFragment fragment = new AnswerFragment();
    fragmentTransaction.replace(R.id.quiz_content_frame, fragment);

    fragmentTransaction.commit();
}

First i call showWattingFragment after the server emit event on socket i call showAnswerFragment.

The code for socket

socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {

            @Override
            public void call(Object... args) {
                Log.d("socket_log", "connected");
            }

        }).on("quizQuestionReady", new Emitter.Listener() {

            @Override
            public void call(Object... args) {
                showWaitingFragment();
            }

        }).on("quizQuestionLoaded", new Emitter.Listener() {

            @Override
            public void call(Object... args) {
                showAnswerFragment();
            }

        }).on("quizEnded", new Emitter.Listener() {

            @Override
            public void call(Object... args) {
                showResultFragment();
            }

        }).on(Socket.EVENT_DISCONNECT, new Emitter.Listener() {

            @Override
            public void call(Object... args) {}

        });
        socket.connect();

As I said before this worked for the first time i start the activity, not working after i call onBackPress() or finish() then startActivity again
UPDATE my activity

public class StudentQuizActivity extends AppCompatActivity {

@BindView(R.id.quiz_content_frame)
FrameLayout _frame_content;

public Socket socket;
String quiz_code;
public SharedPreferences prefs;
ProgressDialog progressDialog;
int user_id;
String token;
public int currentQuestionIndex = -1;
public int currentQuestionIndexForShowing = 0;
public int countCorrect = 0;

public boolean no_answer = true;

public ArrayList<String> answers;
public ArrayList<String> correct_answers;
public ArrayList<Boolean> corrects;

public JSONObject quizConfig;
public JSONArray quizQuestions;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_student_quiz);

    prefs = new SecurePreferences(this);

    quiz_code = prefs.getString(AppVariable.QUIZ_CODE, null);
    user_id = prefs.getInt(AppVariable.USER_ID, 0);
    token = prefs.getString(AppVariable.USER_TOKEN, null);

    ButterKnife.bind(this);

    this.setTitle("QUIZ");

    // prepare spinner
    progressDialog = new ProgressDialog(this, R.style.AppTheme_Dark_Dialog);
    progressDialog.setIndeterminate(true);
    progressDialog.setCanceledOnTouchOutside(false);

    prefs.edit().putString(AppVariable.QUIZ_MESSAGE, "Waiting for the quiz to start").apply();

    answers = new ArrayList<>();
    correct_answers = new ArrayList<>();
    corrects = new ArrayList<>();

    setSocket();

    new GetQuizTask().execute();
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    //No call for super(). Bug on API Level > 11.
}

//UI

private void showWaitingFragment() {
    FragmentManager fragmentManager = this.getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

    WaitingFragment fragment = new WaitingFragment();
    fragmentTransaction.replace(R.id.quiz_content_frame, fragment);

    fragmentTransaction.commit();
}

private void showAnswerFragment() {
    FragmentManager fragmentManager = this.getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();


    AnswerFragment fragment = new AnswerFragment();
    fragmentTransaction.replace(R.id.quiz_content_frame, fragment);

    fragmentTransaction.commit();
}

private void showResultFragment() {
    FragmentManager fragmentManager = this.getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

    ResultFragment fragment = new ResultFragment();
    fragmentTransaction.replace(R.id.quiz_content_frame, fragment);

    fragmentTransaction.commit();
}

public void showDetailFragment() {
    FragmentManager fragmentManager = this.getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

    DetailFragment fragment = new DetailFragment();
    fragmentTransaction.replace(R.id.quiz_content_frame, fragment);

    fragmentTransaction.commit();
}

@Override
public void onBackPressed() {
    super.onBackPressed();
}

private class GetQuizTask extends AsyncTask<String, Void, Integer> {

    private Exception exception;
    private String strJsonResponse;

    @Override
    protected void onPreExecute() {
        progressDialog.setMessage("Loading...");
        progressDialog.show();
    }

    @Override
    protected Integer doInBackground(String... params) {
        int flag = 0;
        try {
            URL url = new URL(Network.API_GET_QUIZ);

            //prepare json data
            JSONObject jsonUserData = new JSONObject();
            jsonUserData.put("token", token);
            jsonUserData.put("quiz_code", quiz_code);

            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            try {

                connection.setReadTimeout(10000);
                connection.setConnectTimeout(15000);
                connection.setRequestMethod("POST");
                connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
                connection.setRequestProperty("Accept", "application/json");
                connection.setDoInput(true);
                connection.setDoOutput(true);

                //write
                OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
                writer.write(jsonUserData.toString());
                writer.flush();

                //check http response code
                int status = connection.getResponseCode();
                switch (status){
                    case HttpURLConnection.HTTP_OK:
                        //read response
                        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));

                        StringBuilder sb = new StringBuilder();
                        String line;

                        while ((line = bufferedReader.readLine()) != null) {
                            sb.append(line).append("\n");
                        }

                        bufferedReader.close();
                        strJsonResponse = sb.toString();

                        flag = HttpURLConnection.HTTP_OK;
                    default:
                        exception = new Exception(connection.getResponseMessage());
                }
            }
            finally{
                connection.disconnect();
            }
        }
        catch(Exception e) {
            exception = e;
        }
        return flag;
    }

    @Override
    protected void onPostExecute(Integer status) {
        if (status != HttpURLConnection.HTTP_OK){
            displayToast(exception.getMessage());
        }
        else {
            try{
                JSONObject jsonObject = new JSONObject(strJsonResponse);
                String result = jsonObject.getString("result");

                if (result.equals("failure")){
                    String message = jsonObject.getString("message");
                    progressDialog.dismiss();
                    displayToast(message);
                    return;
                }

                quizConfig = jsonObject.getJSONObject("quiz");
                quizQuestions = quizConfig.getJSONArray("questions");

                prefs.edit().putInt(AppVariable.QUIZ_TOTAL, quizQuestions.length())
                        .putString(AppVariable.QUIZ_TITLE, quizConfig.getString("title")).apply();

                progressDialog.dismiss();
                showWaitingFragment();
                return;

            } catch (JSONException e) {
                e.printStackTrace();
                displayToast(e.getMessage());
            }
        }
        progressDialog.dismiss();
    }
}

//Socket
private void setSocket(){
    if (Network.isOnline(this)){
        try {
            socket = IO.socket(Network.HOST);
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
        socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {

            @Override
            public void call(Object... args) {
                Log.d("socket_log", "connected");
            }

        }).on("quizQuestionReady", new Emitter.Listener() {

            @Override
            public void call(Object... args) {
                currentQuestionIndexForShowing += 1;
                prefs.edit().putString(AppVariable.QUIZ_MESSAGE, "Ready for the next question")
                        .putInt(AppVariable.QUIZ_INDEX, currentQuestionIndexForShowing).apply();
                showWaitingFragment();
            }

        }).on("quizQuestionLoaded", new Emitter.Listener() {

            @Override
            public void call(Object... args) {
                currentQuestionIndex += 1;
                showAnswerFragment();
            }

        }).on("quizQuestionEnded", new Emitter.Listener() {

            @Override
            public void call(Object... args) {
            }

        }).on("quizEnded", new Emitter.Listener() {

            @Override
            public void call(Object... args) {
                prefs.edit()
                        .putInt(AppVariable.QUIZ_INDEX, 0).apply();
                showResultFragment();
            }

        }).on(Socket.EVENT_DISCONNECT, new Emitter.Listener() {

            @Override
            public void call(Object... args) {}

        });
        socket.connect();
    }
}

public void emitAnswer(String option){
    JSONObject payload = new JSONObject();
    try {
        payload.put("quiz_code", quiz_code);
        payload.put("question_index", currentQuestionIndex);
        payload.put("option", option);
        payload.put("student_id", user_id);
    } catch (JSONException e) {
        e.printStackTrace();
    }

    no_answer = false;

    try {
        JSONObject quizDetail = quizQuestions.getJSONObject(currentQuestionIndex);
        String correct_option = quizDetail.getString("correct_option");

        ArrayList<String> options = new ArrayList<>();

        options.add(quizDetail.getString("option_a"));
        options.add(quizDetail.getString("option_b"));
        options.add(quizDetail.getString("option_c"));
        options.add(quizDetail.getString("option_d"));

        String[] keys = {"a", "b", "c", "d"};

        for (int i = 0; i < 4; i++){
            if (correct_option.equals(options.get(i))){
                correct_option = keys[i];
                break;
            }
        }

        if (option.equals(correct_option)){
            corrects.add(true);
            countCorrect += 1;
        }
        else {
            corrects.add(false);
        }

        correct_answers.add(correct_option);
    } catch (JSONException e) {
        e.printStackTrace();
    }

    answers.add(option);

    socket.emit("answeredQuiz", payload);
}
}
Felix
  • 571
  • 14
  • 34

2 Answers2

2

There are many things need to check inside your code. Let take a look one by one.

  1. Inner class GetQuizTask

This is an API request task which could take much time. By default, if everything ok, AsyncTask will help you handle it in background, then post the result in UI thread.

In your onPostExecute(Integer status), you implicit call showWaitingFragment() which is the same as this.showWaitingFragment().

That means you are holding a parent reference inside your inner class. This could lead you to memory leak. Example: If your request takes 10 seconds to finish, but before onPostExecute() called, what happens if your activity is destroyed or change its state before 10 seconds? (There are many things could make you activity change, like you call finish(), rotate the screen, System have to clean memory, User press Home button, etc...). So when these things happen, the activity (or this in this. showWaitingFragment()) can't be collected by garbage collection). That leads you to the memory leak.

One more thing. If your activity changed from foreground to background (user press Home button), the onPostExecute needs to perform showFragment in UI thread, which can't be done right now, what happens then?

Those errors will give you the IllegalStateException.

Solution: Using WeakPreference, something like this:

private class GetQuizTask extends AsyncTask<String, Void, Integer> {
    WeakReference<StudentQuizActivity> studentQuizActivity;

    @Override
    protected void onPreExecute() {
        // do your work before the task execute..
    }

    @Override
    protected Integer doInBackground(String... params) {
        // do your work in background..
    }

    @Override
    protected void onPostExecute(Integer status) {
        try{
            if (studentQuizActivity.get() != null) {
                studentQuizActivity.get().showInfo();
            }
        } catch (Exception e) {
        }
    }
}
  1. Avoid boilerplate codes

There are many functions to show fragment, which can simplify like this

private void showFragment(Fragment fragment) {
    FragmentManager fragmentManager = this.getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    fragmentTransaction.replace(R.id.quiz_content_frame, fragment);
    fragmentTransaction.commit();
}

Then

showFragment(yourResultFragment);
showFragment(yourTransactionFragment);
  1. Before using dismiss for ProgressDialog, check if (progressDialog.isShowing()). Because user can dismiss it before onPostExecute() performing.

  2. Please, decouple your code. Don't push everything inside your inner class. It will easily lead you to tons of errors. One more thing, if there is no special use-case, try to use the static inner class instead. Follow this answers.

I can't detect all your mistake because I don't get your situation, but I wish this helps! Rewrite your code, carefully, one by one.

phatnhse
  • 3,870
  • 2
  • 20
  • 29
0

This is very simple.

You should use commitAllowingStateLoss() instead of commit()

transaction.commitAllowingStateLoss();
redAllocator
  • 725
  • 6
  • 12
  • When i use commitAllowingStateLoss() instead of commit() for changing Fragment the error now is Activity has been destroy. I have to close the app. I dont know why it run only in the first time of the activity is called. – Felix Aug 05 '17 at 07:06