package com.netfluke.sergey.catpicker; import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; import android.graphics.drawable.GradientDrawable; import android.media.AudioManager; import android.media.SoundPool; import android.os.Build; import android.os.PersistableBundle; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import java.util.List; /* NOTE: I started out using tags in FragmentTransaction replace() when adding fragments. I thought to use these tags to retrieve these fragments when I needed them, to update their contents or to remove them. I believed that findFragmentByTag(..) would give me the active visible fragment with that tag. That way I would not need to keep extra global references to fragments. Unfortunately, this failed. I would occasionally get fragments that have already been removed and were waiting to be destroyed, rather than the added visible fragment added under the same tag. I confirmed it by running "adb shell dumpsys activity com.netfluke.sergey.catpicker"; the output showed fragments with mRemoving=true mAdded=false alongside the visible fragments. Apparently, findFragmentByTag(..) returned these. I tried forcefully committing pending transactions with getFragmentManager().executePendingTransactions(); but it did not eliminate the removed fragments from findFragmentByTag's output. See also the code snippet marked with "debug", which shows this is the case. Hence I opted to track active fragments explicitly in private class members; I left tags and code to show occasional defunct fragments in findFragmentByTag(..) results. */ public class MainActivity extends AppCompatActivity { private int catId; // Always start with the cat chosen at the previous run, even // after the app has been quit. private SharedPreferences prefs; // keep the fragments explicitly for onChangeConfiguration private ImageFragment im; // full screen or null in Portrait, always present in Landscape private PickerFragment pf; // there can be only one, but can go on backstack in Portrait mode @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d("MAIN", "onCreate( " + ((savedInstanceState == null) ? "null" : savedInstanceState.toString() + " )")); prefs = getPreferences(Context.MODE_PRIVATE); catId = prefs.getInt("CAT_ID", R.drawable.pusheen1); attachFragments(); } private boolean isPortrait() { Configuration o = getResources().getConfiguration(); return o.orientation == Configuration.ORIENTATION_PORTRAIT; } private void attachFragments() { FragmentManager fmgr = getFragmentManager(); if( isPortrait() ) { setContentView(R.layout.activity_portrait); // create and add a Picker (vertical buttons) pf = new PickerFragment(); FragmentTransaction t = fmgr.beginTransaction(); t.replace(R.id.content_frame, pf, "picker"); t.commit(); } else { // landscape setContentView(R.layout.activity_landscape); // add both new picker and image FragmentTransaction t = fmgr.beginTransaction(); pf = new PickerFragment(); im = new ImageFragment(); im.catId = catId; t.replace(R.id.content_frameA, pf, "picker"); t.replace(R.id.content_frameB, im, "image"); t.commit(); // this sometimes runs *before* onCreateView of the fragment, // so looking up image by id in loadPicture(..) fails to find it: // // im.loadPicture(catId); // Calling // fmgr.executePendingTransactions(); // right after commit does not actually help: although the // ImageFragment gets attached right away, views would still take // some time to load and may still be absent when looked for. // // By contrast, setting the property instead and having the fragment // load the picture when its views are ready in onResume() worked. } } @Override protected void onPause() { super.onPause(); // save the cat's state, to start with the same cat when app is next launched in landscape SharedPreferences.Editor ed = prefs.edit(); ed.putInt("CAT_ID", catId); ed.commit(); } /* We need to suppress calling super's methods to avoid automatic restoration of views and * thus duplication of fragments. Compare the contents of the savedInstanceState bundle * on entry to onCreate(..) when these functions are and are not called. */ @Override public void onSaveInstanceState(Bundle outState) { //super.onSaveInstanceState(outState); Log.d("STATE", "onSaveInstanceState"); outState.putInt("CAT_ID", catId); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { //super.onRestoreInstanceState(savedInstanceState); Log.d("STATE", "onRestoreInstanceState" + savedInstanceState.toString()); catId = savedInstanceState.getInt("CAT_ID"); } /* This function is called in response to all three button clicks in Picker. * It finds out which button was clicked by checking the ID of the View * passed in, i.e., the View that accepted the click. */ public void click(View v) { // which button? switch (v.getId()) { case R.id.button1: catId = R.drawable.manul1; break; case R.id.button2: catId = R.drawable.manul2; break; case R.id.button3: catId = R.drawable.manul3; break; default: Log.d("MAIN", "no such cat"); catId = R.drawable.pusheen1; } // debug: Finding fragment by tag will occasionally return a fragement // already removed, and waiting to be destroyed. Thus finding fragments // by tag is not always useful: it may return a "dead" fragment detached // from an activity; a live fragment with the same tag could be hidden. // Hence this activity keeps explicit references to it fragments. Fragment imgByTag = getFragmentManager().findFragmentByTag("image"); if (imgByTag != null && imgByTag.isRemoving() ) { Log.d("FRAG", "ImageFragment retrieved by tag is in removing stage"); } // end debug // We want to do different things in Portrait and Landscape: // portrait: push PickerFragment on backstack, replace content frame with new ImageFragment // landscape: set image in existing ImageFragment if (isPortrait()) { // there should be no active ImageFragment // im will retain a pointer to a previous one called from Picker, though. im = new ImageFragment(); im.catId = catId; FragmentTransaction t = getFragmentManager().beginTransaction(); t.replace(R.id.content_frame, im, "image"); // but keep the obscured PickerFragment on the backstack, so that // the "Back" button brings us back to it (so we can select more cats!) t.addToBackStack(null); t.commit(); } else { // landscape . Image fragment should exist and be visible if (im == null) Log.d("MAIN", "no image fragment in Landscape!"); else if (im.isVisible()) im.loadPicture(catId); else { Log.d("MAIN", "image fragment is not visible. This should not happen."); } } } int soundId; // For this function to get called, the Manifest must specify // android:configChanges="orientation|screenSize" on the activity element. // When this is the case, the activity does _not_ get destroyed on flip, // but rather this function is called. @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // exception is thrown if this isn't called! Log.d("MAIN", "onConfigurationChange"); // play a meow SoundPool soundPool; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { soundPool = new SoundPool.Builder().build(); } else { soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0); } // Observe the usual pattern for longer actions (reading, parsing, // and loading a sound file can be long, compared to the time // frame in which we expect UI responses). So we kick off the // loading, and set a listener to run when it completes; we cannot // wait on the UI thread until the loading is complete. Instead, // load(..) queues the task and gets an ID. // // Note that the callback we create is closed over soundId (the // SoundPool's internal id that a decoded sound gets in the pool). // So the ID is passed to play(..) via this closure. soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() { @Override public void onLoadComplete(SoundPool soundPool, int i, int i1) { soundPool.play(soundId, 1f, 1f, 1, 0, 1f); } }); soundId = soundPool.load(this, R.raw.meow, 1); // since the activity will not get destroyed, we must manually remove active // fragments removeActiveFragments(); attachFragments(); } private void removeActiveFragments(){ // print the list of existing fragments // This works only in API 26 (e.g., on the emulator) if (Build.VERSION.SDK_INT >= 26 ) { List frags = getFragmentManager().getFragments(); for (Fragment f : frags) { Log.d("FRAG", f.toString() + " (removing: " + f.isRemoving() + ")"); } } // remove active fragments FragmentTransaction t = getFragmentManager().beginTransaction(); if( im != null ) t.remove(im); // im can be null, e.g., when in portrait no button has been pressed if( pf != null ) t.remove(pf); else Log.d("FRAG", "null Picker fragment. This should never happen"); t.commit(); im = null; pf = null; // to make sure this transition goes through before we start adding fragments: // getFragmentManager().executePendingTransactions(); } }