Written on
Fighting Orientation Changes in Android
One of the problems beginners often struggle with in Android is orientation changes. The problem with an orientation change is that it per se destroys the currently running activity, creates a new instance and starts that instance with the original Intent again. This behavior can be pretty disturbing in applications with multiple background threads/asynchronous tasks. This article shows how to handle background threads when the current activity instance is destroyed and created back and forth.Orientation Change Behavior
For the matter of purpose let us assume we have an Activity that shows a large Image using an ImageView loaded from a web server. As the image is very large we decided to show a progress dialog during loading the image.
public class RemoteImageViewActivity extends Activity {
private ImageView imageView;
private ProgressDialog dialog;
private class DownloadImageTask extends AsyncTask<URL, Integer, Bitmap> {
protected Bitmap doInBackground(URL... urls) {
return Downloader.downloadFile(urls[0]); // or for testing purpose just Thread.sleep(30000);
}
protected void onPostExecute(Bitmap bitmap) {
imageView.setImageBitmap(bitmap);
dialog.dismiss();
}
}
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
imageView = (ImageView) findViewById(R.id.imageView);
dialog = ProgressDialog.show(this, "", "Loading. Please wait...", true);
try {
new DownloadImageTask().execute(new URL("http://blog.andresteingress.com/"));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
}
ERROR/WindowManager(1336): Activity com.example.RemoteImageViewActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@4051c210 that was originally added here
android.view.WindowLeaked: Activity com.example.RemoteImageViewActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@4051c210 that was originally added here
at android.view.ViewRoot.<init>(ViewRoot.java:258)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:148)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:91)
at android.view.Window$LocalWindowManager.addView(Window.java:424)
at android.app.Dialog.show(Dialog.java:241)
at android.app.ProgressDialog.show(ProgressDialog.java:107)
at android.app.ProgressDialog.show(ProgressDialog.java:90)
at com.example.RemoteImageViewActivity.onCreate(RemoteImageViewActivity.java:44)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1611)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1663)
at android.app.ActivityThread.access$1500(ActivityThread.java:117)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:931)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:130)
at android.app.ActivityThread.main(ActivityThread.java:3683)
ERROR/AndroidRuntime(1336): FATAL EXCEPTION: main
java.lang.IllegalArgumentException: View not attached to window manager
at android.view.WindowManagerImpl.findViewLocked(WindowManagerImpl.java:355)
at android.view.WindowManagerImpl.removeView(WindowManagerImpl.java:200)
at android.view.Window$LocalWindowManager.removeView(Window.java:432)
at android.app.Dialog.dismissDialog(Dialog.java:278)
at android.app.Dialog.access$000(Dialog.java:71)
at android.app.Dialog$1.run(Dialog.java:111)
at android.app.Dialog.dismiss(Dialog.java:268)
Handling Orientation changes
There are several ways how to handle an orientation change without leaking windows or accessing "dead" views. In our example above, one way would be to leave the dialog management over to the current Activity. This is done by overriding onCreateDialog and using its companion methods showDialog and dismissDialog.
public class RemoteImageViewActivity extends Activity
{
protected static final int PROGRESS_BAR_DIALOG = 1;
private ImageView imageView;
private class DownloadImageTask extends AsyncTask<URL, Integer, Bitmap> {
protected Bitmap doInBackground(URL... urls) {
try {
Thread.sleep(30000);
return Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
} catch (InterruptedException e) {
return null;
}
}
protected void onPostExecute(Bitmap bitmap) {
imageView.setImageBitmap(bitmap);
dismissDialog(PROGRESS_BAR_DIALOG);
}
}
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
imageView = (ImageView) findViewById(R.id.imageView);
showDialog(PROGRESS_BAR_DIALOG);
try {
new DownloadImageTask().execute(new URL("http://blog.andresteingress.com/"));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
@Override protected Dialog onCreateDialog(int id) {
switch (id) {
case PROGRESS_BAR_DIALOG:
return new ProgressDialog.Builder(this).setTitle("").setMessage("Loading. Please wait...").setCancelable(false).create();
}
return null;
}
}
ERROR/WindowManager(1336): Activity com.example.RemoteImageViewActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@4051c210 that was originally added here
android.view.WindowLeaked: Activity com.example.RemoteImageViewActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@4051c210 that was originally added here
at android.view.ViewRoot.<init>(ViewRoot.java:258)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:148)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:91)
at android.view.Window$LocalWindowManager.addView(Window.java:424)
at android.app.Dialog.show(Dialog.java:241)
at android.app.ProgressDialog.show(ProgressDialog.java:107)
at android.app.ProgressDialog.show(ProgressDialog.java:90)
at com.example.RemoteImageViewActivity.onCreate(RemoteImageViewActivity.java:44)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1611)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1663)
at android.app.ActivityThread.access$1500(ActivityThread.java:117)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:931)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:130)
at android.app.ActivityThread.main(ActivityThread.java:3683)
Using a static Handler/Activity Combo
In order to handle the problem with references to the old activity inside inner class object instances, we could use static variables in combination with Android's Handler class.
public class RemoteImageViewActivity extends Activity
{
private static RemoteImageViewActivity ACTIVITY = null;
private static final int SHOW_PROGRESS_BAR_DIALOG = 1;
private static final int HIDE_PROGRESS_BAR_DIALOG = 2;
private static final int SHOW_IMAGE = 3;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (ACTIVITY == null) return;
switch (msg.what) {
case SHOW_PROGRESS_BAR_DIALOG:
ACTIVITY.showDialog(PROGRESS_BAR_DIALOG);
break;
case HIDE_PROGRESS_BAR_DIALOG:
ACTIVITY.dismissDialog(PROGRESS_BAR_DIALOG);
break;
case SHOW_IMAGE:
ImageView imageView = (ImageView) ACTIVITY.findViewById(R.id.imageView);
imageView.setImageBitmap((Bitmap) msg.obj);
}
}
};
protected static final int PROGRESS_BAR_DIALOG = 1;
private static class DownloadImageTask extends AsyncTask<URL, Integer, Bitmap> {
private Handler handler;
public DownloadImageTask(Handler handler) {
this.handler = handler;
}
protected Bitmap doInBackground(URL... urls) {
try {
Thread.sleep(30000);
return Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
} catch (InterruptedException e) {
return null;
}
}
protected void onPostExecute(Bitmap bitmap) {
handler.sendEmptyMessage(HIDE_PROGRESS_BAR_DIALOG);
handler.sendMessage(Message.obtain(handler, SHOW_IMAGE, bitmap));
}
}
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
handler.sendEmptyMessage(SHOW_PROGRESS_BAR_DIALOG);
try {
new DownloadImageTask(handler).execute(new URL("http://blog.andresteingress.com/"));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
@Override
protected void onStart() {
super.onStart();
ACTIVITY = this;
}
@Override
protected void onStop() {
ACTIVITY = null;
super.onStop();
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case PROGRESS_BAR_DIALOG:
return new ProgressDialog.Builder(this).setTitle("").setMessage("Loading. Please wait...").setCancelable(false).create();
}
return null;
}
}
Overriding android:configChanges
One way to solve the problem is to introduce another static variable indicating whether the download is currently running in the background or not. But there is another way: we can provide the android:configChanges="orientation" XML attribute in our AndroidManifest.xml. The configChanges attribute is documented [0] asLists configuration changes that the activity will handle itself. When a configuration change occurs at runtime, the activity is shut down and restarted by default, but declaring a configuration with this attribute will prevent the activity from being restarted. Instead, the activity remains running and its onConfigurationChanged() method is called.where as orientation is used to react on orientation changes. Let's add android:configChanges="orientation" to our activity XML declaration and modify the code to
public class RemoteImageViewActivity extends Activity
{
protected static final int PROGRESS_BAR_DIALOG = 1;
private ImageView imageView;
private class DownloadImageTask extends AsyncTask<URL, Integer, Bitmap> {
protected Bitmap doInBackground(URL... urls) {
try {
Thread.sleep(30000);
return Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
} catch (InterruptedException e) {
return null;
}
}
protected void onPostExecute(Bitmap bitmap) {
imageView.setImageBitmap(bitmap);
dismissDialog(PROGRESS_BAR_DIALOG);
}
}
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
imageView = (ImageView) findViewById(R.id.imageView);
showDialog(PROGRESS_BAR_DIALOG);
try {
new DownloadImageTask().execute(new URL("http://blog.andresteingress.com/"));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case PROGRESS_BAR_DIALOG:
return new ProgressDialog.Builder(this).setTitle("").setMessage("Loading. Please wait...").setCancelable(false).create();
}
return null;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// some work that needs to be done on orientation change
}
}