Thursday, September 24, 2015

One way to solve the Screen Rotation problem while dealing with Android Asynctask

In many cases of Android applications employing Asynctask, the Activity that creates the Asynctask, may finish before the background job is actually done. In fact while the Asynctask is doing its job in the background, if we rotate the screen of the device, the previous Activity which created this Asynctask will be destroyed and a new instance of the same activity will be created. Hence even if the background task continues, it won't be able to update the UI, because it has already lost the connection with the previous Activity which created it. So if we were showing a ProgressDialog in the main Activity screen, it will throw runtime exception, because the Activity that created it has been already destroyed and the background task is still trying to update the previous ProgressDialog.

See the following Activity which creates an Asynctask to download an Image from a server.

package com.somitsolutions.training.android.asynctaskdownloadimage;

public class MainActivity extends Activity implements OnClickListener{
 
 Button mStartDownloadButton, mDisplayImageButton;
 EditText mURL;
 ImageView mImageView;
 private CallBack c;
 ProgressDialog mProgressDialog;
 DownloadImageAsyncTask mTask;
 Context context;
 Bitmap mBitmap;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  mStartDownloadButton = (Button)findViewById(R.id.buttonDownloadImage);
  mDisplayImageButton = (Button)findViewById(R.id.buttonDisplayImage);
  mURL = (EditText)findViewById(R.id.editTextURL);
  mImageView = (ImageView)findViewById(R.id.imageView1);
  mImageView.setVisibility(View.INVISIBLE);
  mStartDownloadButton.setOnClickListener(this);
  mDisplayImageButton.setOnClickListener(this);
  
  context = this;
  
  mProgressDialog = new ProgressDialog(this);
  mProgressDialog.setMessage("On Progress...");
  mProgressDialog.setCancelable(true);
  
   c = new CallBack() {
          
    public void onProgress(){
     if(mProgressDialog == null){
      mProgressDialog = new ProgressDialog(MainActivity.this);
     mProgressDialog.setMessage("On Progress...");
     mProgressDialog.setCancelable(true);
     }
     mProgressDialog.show();  
          }
          
          public void onResult(Bitmap result){
            mBitmap = result;
            mProgressDialog.dismiss();
            Toast.makeText(context, "Done...", Toast.LENGTH_LONG).show();
            mStartDownloadButton.setEnabled(true);
            mProgressDialog = null;
           
          }
          
          public void onCancel(){
           mProgressDialog.dismiss();
           Toast toast = Toast.makeText(getApplicationContext(), "Cancelled", Toast.LENGTH_LONG);
              toast.show();
              mProgressDialog = null;
          }
         };
 }
 @Override
 public void onClick(View v) {
  // TODO Auto-generated method stub
  if (v.equals(mStartDownloadButton)){
   String URL = mURL.getText().toString();
   URL = URL.replace(" ", "");
   
   if(URL != null && !URL.isEmpty()){
    DownloadImageAsyncTask asyncTask = new DownloadImageAsyncTask(context, c);
    asyncTask.execute(URL);
    mStartDownloadButton.setEnabled(false);
   }
   
  }
  
  if(v.equals(mDisplayImageButton)){
   mImageView.setImageBitmap(mBitmap);
   mImageView.setVisibility(View.VISIBLE);
  }
 }
}

For the above Activity, if we rotate the screen when the background task is going on, the following LogCat messages will be shown:

...................
09-24 14:23:26.431: E/WindowManager(8814): android.view.WindowLeaked: Activity com.somitsolutions.training.android.asynctaskdownloadimage.MainActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView{3eded954 V.E..... R....... 0,0-639,128} that was originally added here
09-24 14:23:26.431: E/WindowManager(8814): at android.view.ViewRootImpl.(ViewRootImpl.java:363)
09-24 14:23:26.431: E/WindowManager(8814): at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:271)
09-24 14:23:26.431: E/WindowManager(8814): at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
09-24 14:23:26.431: E/WindowManager(8814): at android.app.Dialog.show(Dialog.java:298)
09-24 14:23:26.431: E/WindowManager(8814): at com.somitsolutions.training.android.asynctaskdownloadimage.MainActivity$1.onProgress(MainActivity.java:52)
09-24 14:23:26.431: E/WindowManager(8814): at com.somitsolutions.training.android.asynctaskdownloadimage.DownloadImageAsyncTask.onProgressUpdate(DownloadImageAsyncTask.java:37)
09-24 14:23:26.431: E/WindowManager(8814): at com.somitsolutions.training.android.asynctaskdownloadimage.DownloadImageAsyncTask.onProgressUpdate(DownloadImageAsyncTask.java:1)
09-24 14:23:26.431: E/WindowManager(8814): at android.os.AsyncTask$InternalHandler.handleMessage(AsyncTask.java:656)
09-24 14:23:26.431: E/WindowManager(8814): at android.os.Handler.dispatchMessage(Handler.java:102)
09-24 14:23:26.431: E/WindowManager(8814): at android.os.Looper.loop(Looper.java:135)
09-24 14:23:26.431: E/WindowManager(8814): at android.app.ActivityThread.main(ActivityThread.java:5254)
09-24 14:23:26.431: E/WindowManager(8814): at java.lang.reflect.Method.invoke(Native Method)
09-24 14:23:26.431: E/WindowManager(8814): at java.lang.reflect.Method.invoke(Method.java:372)
09-24 14:23:26.431: E/WindowManager(8814): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
09-24 14:23:26.431: E/WindowManager(8814): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
09-24 14:23:27.764: D/AndroidRuntime(8814): Shutting down VM
09-24 14:23:27.765: E/AndroidRuntime(8814): FATAL EXCEPTION: main
09-24 14:23:27.765: E/AndroidRuntime(8814): Process: com.somitsolutions.training.android.asynctaskdownloadimage, PID: 8814
09-24 14:23:27.765: E/AndroidRuntime(8814): java.lang.IllegalArgumentException: View=com.android.internal.policy.impl.PhoneWindow$DecorView{3eded954 V.E..... R......D 0,0-639,128} not attached to window manager
09-24 14:23:27.765: E/AndroidRuntime(8814): at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:396)
09-24 14:23:27.765: E/AndroidRuntime(8814): at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:322)
09-24 14:23:27.765: E/AndroidRuntime(8814): at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:116)
09-24 14:23:27.765: E/AndroidRuntime(8814): at android.app.Dialog.dismissDialog(Dialog.java:341)
09-24 14:23:27.765: E/AndroidRuntime(8814): at android.app.Dialog.dismiss(Dialog.java:324)
09-24 14:23:27.765: E/AndroidRuntime(8814): at com.somitsolutions.training.android.asynctaskdownloadimage.MainActivity$1.onResult(MainActivity.java:57)
09-24 14:23:27.765: E/AndroidRuntime(8814): at com.somitsolutions.training.android.asynctaskdownloadimage.DownloadImageAsyncTask.onPostExecute(DownloadImageAsyncTask.java:42)
09-24 14:23:27.765: E/AndroidRuntime(8814): at com.somitsolutions.training.android.asynctaskdownloadimage.DownloadImageAsyncTask.onPostExecute(DownloadImageAsyncTask.java:1)
09-24 14:23:27.765: E/AndroidRuntime(8814): at android.os.AsyncTask.finish(AsyncTask.java:636)
09-24 14:23:27.765: E/AndroidRuntime(8814): at android.os.AsyncTask.access$500(AsyncTask.java:177)
09-24 14:23:27.765: E/AndroidRuntime(8814): at android.os.AsyncTask$InternalHandler.handleMessage(AsyncTask.java:653)
09-24 14:23:27.765: E/AndroidRuntime(8814): at android.os.Handler.dispatchMessage(Handler.java:102)
09-24 14:23:27.765: E/AndroidRuntime(8814): at android.os.Looper.loop(Looper.java:135)
09-24 14:23:27.765: E/AndroidRuntime(8814): at android.app.ActivityThread.main(ActivityThread.java:5254)
09-24 14:23:27.765: E/AndroidRuntime(8814): at java.lang.reflect.Method.invoke(Native Method)
09-24 14:23:27.765: E/AndroidRuntime(8814): at java.lang.reflect.Method.invoke(Method.java:372)
09-24 14:23:27.765: E/AndroidRuntime(8814): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)

09-24 14:23:27.765: E/AndroidRuntime(8814): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)

...................

One way to solve this problem is to override the onPause lifecycle method of the Activity, and inside that cancel the Background task and dismiss the ProgressDialog...

But the best way to handle this situation is to create a Fragment instead of the Activity, and inside that Fragment create the Asynctask.

Here is the code of such a fragment:

package com.somitsolutions.training.android.asynctaskdownloadimage;

import android.app.Fragment;
import android.app.ProgressDialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;

public class MainActivityFragment extends Fragment implements OnClickListener{
 
 Button mStartDownloadButton, mDisplayImageButton;
 EditText mURL;
 ImageView mImageView; 
 private CallBack c;
 ProgressDialog mProgressDialog;
 DownloadImageAsyncTask mTask;
 Context context;
 Bitmap mBitmap;
 private static MainActivityFragment app;
 Boolean isScreenRotated = false;
 
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setRetainInstance(true);

   c = new CallBack() {
          
    public void onProgress(){
     if(c != null && !isScreenRotated){
      if(mProgressDialog == null) {
       mProgressDialog = new ProgressDialog(getActivity());
      }
      mProgressDialog.setMessage("On Progress...");
      mProgressDialog.setCancelable(true);

      mProgressDialog.show();
      }

          }
          
          public void onResult(Bitmap result){

     if (c != null && !isScreenRotated){
      mBitmap = result;

      Toast.makeText(getActivity().getApplicationContext(), "Done...", Toast.LENGTH_LONG).show();
      mStartDownloadButton.setEnabled(true);

      if(mProgressDialog != null){
       mProgressDialog.dismiss();
       mProgressDialog = null;
      }
     }
     if (isScreenRotated){
      Toast.makeText(getActivity().getApplicationContext(),"Download not completed. Please retry...", Toast.LENGTH_LONG).show();
      isScreenRotated = false;
     }
           
          }
          
          public void onCancel(){

           Toast toast = Toast.makeText(getActivity().getApplicationContext(), "Cancelled", Toast.LENGTH_LONG);
              toast.show();
     if(mProgressDialog != null){
      mProgressDialog.dismiss();
      mProgressDialog = null;
     }

          }
         };
 }

 @Override
 public void onActivityCreated(Bundle savedInstanceState){
  super.onActivityCreated(savedInstanceState);
  //isScreenRotated = false;
 }


 @Override
 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

  View view = inflater.inflate(R.layout.activity_main,container,false);

  mStartDownloadButton = (Button)view.findViewById(R.id.buttonDownloadImage);
  mDisplayImageButton = (Button) view.findViewById(R.id.buttonDisplayImage);
  mURL = (EditText) view.findViewById(R.id.editTextURL);
  mImageView = (ImageView) view.findViewById(R.id.imageView1);
  mImageView.setVisibility(View.INVISIBLE);
  mStartDownloadButton.setOnClickListener(this);
  mDisplayImageButton.setOnClickListener(this);

  return view;
 }


 @Override
 public void onClick(View v) {
  // TODO Auto-generated method stub
  if (v.equals(mStartDownloadButton)){
   String URL = mURL.getText().toString();
   URL = URL.replace(" ", "");

   if(URL != null && !URL.isEmpty()){
    DownloadImageAsyncTask asyncTask = new DownloadImageAsyncTask(c);
    asyncTask.execute(URL);
    mStartDownloadButton.setEnabled(false);
   }
   
  }
  
  if(v.equals(mDisplayImageButton)){
   mImageView.setImageBitmap(mBitmap);
   mImageView.setVisibility(View.VISIBLE);
  }
 }

 @Override
 public void onDetach() {
  // All dialogs should be closed before leaving the activity in order to avoid
  // the: Activity has leaked window com.android.internal.policy... exception
  if (mProgressDialog != null && mProgressDialog.isShowing()) {
   mProgressDialog.dismiss();
   mProgressDialog = null;
  }
  if(mBitmap != null){
   mBitmap = null;

  }
  isScreenRotated = true;
  super.onDetach();
 }

 /*@Override
 public void onAttach(Activity activity){
  super.onAttach(activity);
  isScreenRotated = false;

 }*/
}


Look at the above code and see how we handle the rotation  in the Detach function (like dismissing the ProgressDialog) of the Fragment. Also important is calling setRetainInstance(true) in the onCreate method of the Fragment. Since we are calling setRetainInstance(true), the fragment won't be newly created when the screen rotates, however the Detach method will be called as usual.

And now the Activity will look like the following:

package com.somitsolutions.training.android.asynctaskdownloadimage;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends Activity {

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

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}

You can clone the source code of the complete App from https://github.com/sommukhopadhyay/AsynctaskDownloadImage 


No comments: