How to handle Handler messages when activity/fragment is paused - android

Slight variation on my other posting
Basically I have a message Handler in my Fragment which receives a bunch of messages that can result in dialogs being dismissed or shown.
When the app is put into the background I get an onPause but then still get my messages coming through as one would expect. However, because I'm using fragments I can't just dismiss and show dialogs as that will result in an IllegalStateException.
I can't just dismiss or cancel allowing state loss.
Given that I have a Handler I'm wondering whether there is a recommended approach as to
how I should handle messages while in a paused state.
One possible solution I'm considering is to record the messages coming through while paused and play them back on an onResume. This is somewhat unsatisfactory and I'm thinking that there must be something in the framework to handle this more elegantly.

Although the Android operating system does not appear to have a mechanism that sufficiently addresses your problem I believe this pattern does provide a relatively simple to implement workaround.
The following class is a wrapper around android.os.Handler that buffers up messages when an activity is paused and plays them back on resume.
Ensure any code that you have which asynchronously changes a fragment state (e.g. commit, dismiss) is only called from a message in the handler.
Derive your handler from the PauseHandler class.
Whenever your activity receives an onPause() call PauseHandler.pause() and for onResume() call PauseHandler.resume().
Replace your implementation of the Handler handleMessage() with processMessage().
Provide a simple implementation of storeMessage() which always returns true.
/**
* Message Handler class that supports buffering up of messages when the
* activity is paused i.e. in the background.
*/
public abstract class PauseHandler extends Handler {
/**
* Message Queue Buffer
*/
final Vector<Message> messageQueueBuffer = new Vector<Message>();
/**
* Flag indicating the pause state
*/
private boolean paused;
/**
* Resume the handler
*/
final public void resume() {
paused = false;
while (messageQueueBuffer.size() > 0) {
final Message msg = messageQueueBuffer.elementAt(0);
messageQueueBuffer.removeElementAt(0);
sendMessage(msg);
}
}
/**
* Pause the handler
*/
final public void pause() {
paused = true;
}
/**
* Notification that the message is about to be stored as the activity is
* paused. If not handled the message will be saved and replayed when the
* activity resumes.
*
* #param message
* the message which optional can be handled
* #return true if the message is to be stored
*/
protected abstract boolean storeMessage(Message message);
/**
* Notification message to be processed. This will either be directly from
* handleMessage or played back from a saved message when the activity was
* paused.
*
* #param message
* the message to be handled
*/
protected abstract void processMessage(Message message);
/** {#inheritDoc} */
#Override
final public void handleMessage(Message msg) {
if (paused) {
if (storeMessage(msg)) {
Message msgCopy = new Message();
msgCopy.copyFrom(msg);
messageQueueBuffer.add(msgCopy);
}
} else {
processMessage(msg);
}
}
}
Below is a simple example of how the PausedHandler class can be used.
On the click of a button a delayed message is sent to the handler.
When the handler receives the message (on the UI thread) it displays a DialogFragment.
If the PausedHandler class was not being used an IllegalStateException would be shown if the home button was pressed after pressing the test button to launch the dialog.
public class FragmentTestActivity extends Activity {
/**
* Used for "what" parameter to handler messages
*/
final static int MSG_WHAT = ('F' << 16) + ('T' << 8) + 'A';
final static int MSG_SHOW_DIALOG = 1;
int value = 1;
final static class State extends Fragment {
static final String TAG = "State";
/**
* Handler for this activity
*/
public ConcreteTestHandler handler = new ConcreteTestHandler();
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
#Override
public void onResume() {
super.onResume();
handler.setActivity(getActivity());
handler.resume();
}
#Override
public void onPause() {
super.onPause();
handler.pause();
}
public void onDestroy() {
super.onDestroy();
handler.setActivity(null);
}
}
/**
* 2 second delay
*/
final static int DELAY = 2000;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
if (savedInstanceState == null) {
final Fragment state = new State();
final FragmentManager fm = getFragmentManager();
final FragmentTransaction ft = fm.beginTransaction();
ft.add(state, State.TAG);
ft.commit();
}
final Button button = (Button) findViewById(R.id.popup);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
final FragmentManager fm = getFragmentManager();
State fragment = (State) fm.findFragmentByTag(State.TAG);
if (fragment != null) {
// Send a message with a delay onto the message looper
fragment.handler.sendMessageDelayed(
fragment.handler.obtainMessage(MSG_WHAT, MSG_SHOW_DIALOG, value++),
DELAY);
}
}
});
}
public void onSaveInstanceState(Bundle bundle) {
super.onSaveInstanceState(bundle);
}
/**
* Simple test dialog fragment
*/
public static class TestDialog extends DialogFragment {
int value;
/**
* Fragment Tag
*/
final static String TAG = "TestDialog";
public TestDialog() {
}
public TestDialog(int value) {
this.value = value;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View inflatedView = inflater.inflate(R.layout.dialog, container, false);
TextView text = (TextView) inflatedView.findViewById(R.id.count);
text.setText(getString(R.string.count, value));
return inflatedView;
}
}
/**
* Message Handler class that supports buffering up of messages when the
* activity is paused i.e. in the background.
*/
static class ConcreteTestHandler extends PauseHandler {
/**
* Activity instance
*/
protected Activity activity;
/**
* Set the activity associated with the handler
*
* #param activity
* the activity to set
*/
final void setActivity(Activity activity) {
this.activity = activity;
}
#Override
final protected boolean storeMessage(Message message) {
// All messages are stored by default
return true;
};
#Override
final protected void processMessage(Message msg) {
final Activity activity = this.activity;
if (activity != null) {
switch (msg.what) {
case MSG_WHAT:
switch (msg.arg1) {
case MSG_SHOW_DIALOG:
final FragmentManager fm = activity.getFragmentManager();
final TestDialog dialog = new TestDialog(msg.arg2);
// We are on the UI thread so display the dialog
// fragment
dialog.show(fm, TestDialog.TAG);
break;
}
break;
}
}
}
}
}
I've added a storeMessage() method to the PausedHandler class in case any messages should be processed immediately even when the activity is paused. If a message is handled then false should be returned and the message will be discarded.

A slightly simpler version of quickdraw's excellent PauseHandler is
/**
* Message Handler class that supports buffering up of messages when the activity is paused i.e. in the background.
*/
public abstract class PauseHandler extends Handler {
/**
* Message Queue Buffer
*/
private final List<Message> messageQueueBuffer = Collections.synchronizedList(new ArrayList<Message>());
/**
* Flag indicating the pause state
*/
private Activity activity;
/**
* Resume the handler.
*/
public final synchronized void resume(Activity activity) {
this.activity = activity;
while (messageQueueBuffer.size() > 0) {
final Message msg = messageQueueBuffer.get(0);
messageQueueBuffer.remove(0);
sendMessage(msg);
}
}
/**
* Pause the handler.
*/
public final synchronized void pause() {
activity = null;
}
/**
* Store the message if we have been paused, otherwise handle it now.
*
* #param msg Message to handle.
*/
#Override
public final synchronized void handleMessage(Message msg) {
if (activity == null) {
final Message msgCopy = new Message();
msgCopy.copyFrom(msg);
messageQueueBuffer.add(msgCopy);
} else {
processMessage(activity, msg);
}
}
/**
* Notification message to be processed. This will either be directly from
* handleMessage or played back from a saved message when the activity was
* paused.
*
* #param activity Activity owning this Handler that isn't currently paused.
* #param message Message to be handled
*/
protected abstract void processMessage(Activity activity, Message message);
}
It does assume that you always want to store offline messages for replay. And provides the Activity as input to #processMessages so you don't need to manage it in the sub class.

Here is a slightly different way to approach the problem of doing Fragment commits in a callback function and avoiding the IllegalStateException issue.
First create a custom runnable interface.
public interface MyRunnable {
void run(AppCompatActivity context);
}
Next, create a fragment for processing the MyRunnable objects. If the MyRunnable object was created after the Activity was paused, for e.g. if the screen is rotated, or the user presses the home button, it is put in a queue for later processing with a new context. The queue survives any configuration changes because setRetain instance is set to true. The method runProtected runs on UI thread to avoid a race condition with the isPaused flag.
public class PauseHandlerFragment extends Fragment {
private AppCompatActivity context;
private boolean isPaused = true;
private Vector<MyRunnable> buffer = new Vector<>();
#Override
public void onAttach(Context context) {
super.onAttach(context);
this.context = (AppCompatActivity)context;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
#Override
public void onPause() {
isPaused = true;
super.onPause();
}
#Override
public void onResume() {
isPaused = false;
playback();
super.onResume();
}
private void playback() {
while (buffer.size() > 0) {
final MyRunnable runnable = buffer.elementAt(0);
buffer.removeElementAt(0);
new Handler(Looper.getMainLooper()).post(new Runnable() {
#Override
public void run() {
//execute run block, providing new context, incase
//Android re-creates the parent activity
runnable.run(context);
}
});
}
}
public final void runProtected(final MyRunnable runnable) {
context.runOnUiThread(new Runnable() {
#Override
public void run() {
if(isPaused) {
buffer.add(runnable);
} else {
runnable.run(context);
}
}
});
}
}
Finally, the fragment may be used in a main application as follows:
public class SomeActivity extends AppCompatActivity implements SomeListener {
PauseHandlerFragment mPauseHandlerFragment;
static class Storyboard {
public static String PAUSE_HANDLER_FRAGMENT_TAG = "phft";
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
//register pause handler
FragmentManager fm = getSupportFragmentManager();
mPauseHandlerFragment = (PauseHandlerFragment) fm.
findFragmentByTag(Storyboard.PAUSE_HANDLER_FRAGMENT_TAG);
if(mPauseHandlerFragment == null) {
mPauseHandlerFragment = new PauseHandlerFragment();
fm.beginTransaction()
.add(mPauseHandlerFragment, Storyboard.PAUSE_HANDLER_FRAGMENT_TAG)
.commit();
}
}
// part of SomeListener interface
public void OnCallback(final String data) {
mPauseHandlerFragment.runProtected(new MyRunnable() {
#Override
public void run(AppCompatActivity context) {
//this block of code should be protected from IllegalStateException
FragmentManager fm = context.getSupportFragmentManager();
...
}
});
}
}

In my projects I use the observer design pattern to solve this. In Android, broadcast receivers and intents are an implemenation of this pattern.
What I do is create a BroadcastReceiver which I register in fragment's/activity's onResume and unregister in fragment's/activity's onPause.
In BroadcastReceiver's method onReceive I put all code that needs to run as result of - the BroadcastReceiver - receiving an Intent(message) that was sent to your app in general. To increase selectivity on what type of intents your fragment can receive you can use an intent filter as in the example below.
An advantage of this approach is that the Intent(message) can be sent from everywhere whithin your app(a dialog that opened on top of your fragment, an async task, another fragment etc.). Parameters can even passed as intent extras.
Another advantage is that this approach is compatible with any Android API version, since BroadcastReceivers and Intents have been introduced on API level 1.
Your are not required to setup any special permissions on your app's manifest file except if you plan to use sendStickyBroadcast(where you need to add BROADCAST_STICKY).
public class MyFragment extends Fragment {
public static final String INTENT_FILTER = "gr.tasos.myfragment.refresh";
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
// this always runs in UI Thread
#Override
public void onReceive(Context context, Intent intent) {
// your UI related code here
// you can receiver data login with the intent as below
boolean parameter = intent.getExtras().getBoolean("parameter");
}
};
public void onResume() {
super.onResume();
getActivity().registerReceiver(mReceiver, new IntentFilter(INTENT_FILTER));
};
#Override
public void onPause() {
getActivity().unregisterReceiver(mReceiver);
super.onPause();
}
// send a broadcast that will be "caught" once the receiver is up
protected void notifyFragment() {
Intent intent = new Intent(SelectCategoryFragment.INTENT_FILTER);
// you can send data to receiver as intent extras
intent.putExtra("parameter", true);
getActivity().sendBroadcast(intent);
}
}

Related

ProgressDialog not executing the first time

I've got an Activity that implements a retained fragment to perform a time consuming task. This allows me to rotate the screen without loosing a reference to my async task and without stopping it from being executed. I have a button on the activity to launch the Async task. Everytime the activity is created, for example due to a rotated screen, the activity checks if its retained fragment has an async task running. If so, it shows the progressDialog to let the user know there are still some tasks running on the background. I attach the code that does what I mentioned. Actually it works. However, what is strange is that it works everytime but the first! When I push the button to launch the Async process for the first time, the progressDialog is not shown, eventhough "onPreExecute()" is called and the line "progressDialog.show()" executed. If I rotate the screen after I push the button the progressDialog shows and when the process ends, If I push the button again then it works just fine. As I said, it is working all the time except the first one. Any idea why?
Thanks!
Activity
public class Activity extends AppCompatActivity implements ActivityTaskFragment.TaskCallbacks{
private ProgressDialog progressDialog;
private static final String TAG_TASK_FRAGMENT = "task_fragment";
private ActivityTaskFragment activityTaskFragment;
//Some additional code here
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crear_turno);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
//------------------------------------------------------
setListeners();
setFragment();
createProgressDialog();
showProgressDialog();
}
private void showProgressDialog()
{
if(activityTaskFragment!=null)
{
if(activityTaskFragment.isRunning())
{
progressDialog.show();
}
}
}
private void setFragment()
{
FragmentManager fm = getFragmentManager();
activityTaskFragment = (ActivityTaskFragment) fm.findFragmentByTag(TAG_TASK_FRAGMENT);
// If the Fragment is non-null, then it is currently being
// retained across a configuration change.
if (activityTaskFragment == null) {
activityTaskFragment = new ActivityTaskFragment();
fm.beginTransaction().add(activityTaskFragment, TAG_TASK_FRAGMENT).commit();
}
}
private void setListeners() {
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
activityTaskFragment.execute();
}
});
}
//fragment interface implementation
#Override
public void onPreExecute()
{
if(progressDialog!=null)
progressDialog.show();
}
#Override
public void onCancelled() {
if(progressDialog!=null)
{
if(progressDialog.isShowing())
{
progressDialog.dismiss();
}
}
}
#Override
public void onPostExecute()
{
if(progressDialog!=null)
{
if(progressDialog.isShowing())
{
progressDialog.dismiss();
}
}
}
private void createProgressDialog()
{
if(progressDialog == null)
{
progressDialog = new ProgressDialog(Activity.this);
progressDialog.setTitle("Executing job");
progressDialog.setMessage("please wait...");
}
}
}
TaskFragment
/**
* This Fragment manages a single background task and retains
* itself across configuration changes.
*/
public class ActivityTaskFragment extends Fragment {
/**
* Callback interface through which the fragment will report the
* task's progress and results back to the Activity.
*/
interface TaskCallbacks {
void onPreExecute();
void onCancelled();
void onPostExecute();
}
private TaskCallbacks mCallbacks;
private CheckTask mTask;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mCallbacks = (TaskCallbacks) activity;
}
/**
* Hold a reference to the parent Activity so we can report the
* task's current progress and results. The Android framework
* will pass us a reference to the newly created Activity after
* each configuration change.
*/
public void execute()
{
mTask.cancel(true);
mTask = new CheckTask();
mTask.execute();
}
public void cancel()
{
if(mTask != null)
{
mTask.cancel(true);
}
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
if(context instanceof Activity) {
Activity activity = (Activity) context;
mCallbacks = (TaskCallbacks) activity;
}
}
/**
* This method will only be called once when the retained
* Fragment is first created.
*/
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Retain this fragment across configuration changes.
setRetainInstance(true);
mTask = new CheckTask();
}
/**
* Set the callback to null so we don't accidentally leak the
* Activity instance.
*/
#Override
public void onDetach() {
super.onDetach();
mCallbacks = null;
}
/**
* A dummy task that performs some (dumb) background work and
* proxies progress updates and results back to the Activity.
*
* Note that we need to check if the callbacks are null in each
* method in case they are invoked after the Activity's and
* Fragment's onDestroy() method have been called.
*/
private class CheckTask extends AsyncTask<Void, Void, Void> {
#Override
protected void onPreExecute()
{
if (mCallbacks != null) {
mCallbacks.onPreExecute();
}
}
/**
* Note that we do NOT call the callback object's methods
* directly from the background thread, as this could result
* in a race condition.
*/
#Override
protected Void doInBackground(Void... ignore) {
/*
LONG TIME CONSUMING TASK
*/
return null;
}
#Override
protected void onCancelled() {
running = false;
if (mCallbacks != null) {
mCallbacks.onCancelled();
}
}
#Override
protected void onPostExecute(Void ignore)
{
running = false;
if (mCallbacks != null) {
mCallbacks.onPostExecute();
}
}
}
public boolean isRunning()
{
if(mTask!=null)
{
if(mTask.getStatus() == AsyncTask.Status.RUNNING)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
}

Using progress Dialog instead of progress bar in asyncTask

I am trying to implement this tutorial, for handling Configuration changes while running background tasks. Everything works fine, and the app does not crash after a configuration change. In the tutorial, a progress bar is used to display progress. But in my own implementation i want to use a Progress Dialog.
I have used progress Dialog's lots of times, so calling it and getting to appear is not the problem. My problem is that unlike the progress Bar, the progress dialog gets dismissed on configuration change. Just like that.
Here is my code:
My MainActivity:
private TaskFragment mTaskFragment;
private ProgressDialog mProgressDialog;
private TextView mPercent;
private Button mButton;
#Override
protected void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate(Bundle)");
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Initialize views
mButton = (Button) findViewById(R.id.task_button);
mButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
if (mTaskFragment.isRunning()) {
mTaskFragment.cancel();
} else {
mTaskFragment.start();
}
}
});
mProgressBar = new ProgressDialog(this);
FragmentManager fm = getSupportFragmentManager();
mTaskFragment = (TaskFragment) fm.findFragmentByTag("task");
// If the Fragment is non-null, then it is currently being
// retained across a configuration change.
if (mTaskFragment == null) {
mTaskFragment = new TaskFragment();
fm.beginTransaction().add(mTaskFragment, "task").commit();
}
if (mTaskFragment.isRunning()) {
mButton.setText(getString(R.string.cancel));
} else {
mButton.setText(getString(R.string.start));
}
}
/****************************/
/***** CALLBACK METHODS *****/
/****************************/
#Override
public void onPreExecute() {
Log.i(TAG, "onPreExecute()");
mProgressBar.setTitle("Wacky");
mProgressBar.setMessage("wack");
mProgressBar.setIndeterminate(true);
mProgressBar.show();
mButton.setText(getString(R.string.cancel));
mButton.setText(getString(R.string.cancel));
Toast.makeText(this, R.string.task_started_msg, Toast.LENGTH_SHORT).show();
}
#Override
public void onProgressUpdate(int percent) {
//Log.i(TAG, "onProgressUpdate(" + percent + "%)");
}
#Override
public void onCancelled() {
Log.i(TAG, "onCancelled()");
mButton.setText(getString(R.string.start));
Toast.makeText(this, R.string.task_cancelled_msg, Toast.LENGTH_SHORT).show();
}
#Override
public void onPostExecute() {
Log.i(TAG, "onPostExecute()");
mButton.setText(getString(R.string.start));
Toast.makeText(this, R.string.task_complete_msg, Toast.LENGTH_SHORT).show();
}
My headless Fragment that holds my asyncTask
/**
* This Fragment manages a single background task and retains itself across
* configuration changes.
*/
public class TaskFragment extends Fragment {
public static final String TAG = TaskFragment.class.getSimpleName();
/**
* Callback interface through which the fragment can report the task's
* progress and results back to the Activity.
*/
public static interface TaskCallbacks {
public void onPreExecute();
public void onProgressUpdate(int percent);
public void onCancelled();
public void onPostExecute();
}
public TaskCallbacks mCallbacks;
public DummyTask mTask;
public boolean mRunning;
/**
* Android passes us a reference to the newly created Activity by calling this
* method after each configuration change.
*/
#Override
public void onAttach(Activity activity) {
Log.i(TAG, "onAttach(Activity)");
super.onAttach(activity);
if (!(activity instanceof TaskCallbacks)) {
throw new IllegalStateException("Activity must implement the TaskCallbacks interface.");
}
// Hold a reference to the parent Activity so we can report back the task's
// current progress and results.
mCallbacks = (TaskCallbacks) activity;
}
/**
* This method is called only once when the Fragment is first created.
*/
#Override
public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate(Bundle)");
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
/**
* This method is <em>not</em> called when the Fragment is being retained
* across Activity instances.
*/
#Override
public void onDestroy() {
Log.i(TAG, "onDestroy()");
super.onDestroy();
cancel();
}
/*****************************/
/***** TASK FRAGMENT API *****/
/*****************************/
/**
* Start the background task.
*/
public void start() {
if (!mRunning) {
mTask = new DummyTask(this, mCallbacks);
mTask.execute();
mRunning = true;
}
}
/**
* Cancel the background task.
*/
public void cancel() {
if (mRunning) {
mTask.cancel(false);
mTask = null;
mRunning = false;
}
}
/**
* Returns the current state of the background task.
*/
public boolean isRunning() {
return mRunning;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
Log.i(TAG, "onActivityCreated(Bundle)");
super.onActivityCreated(savedInstanceState);
}
}
My Background Task (in a seperate outer class)
/**
* A dummy task that performs some (dumb) background work and proxies progress
* updates and results back to the Activity.
*/
public class DummyTask extends AsyncTask<Void, Integer, Void> {
private TaskFragment fragment;
private TaskCallbacks callbacks;
private ProgressDialog mProgressBar;
MainActivity activity;
public DummyTask(TaskFragment taskFragment, TaskCallbacks mCallbacks) {
// TODO Auto-generated constructor stub
this.fragment = taskFragment;
this.callbacks = mCallbacks;
}
#Override
protected void onPreExecute() {
// Proxy the call to the Activity
fragment.mCallbacks.onPreExecute();
fragment.mRunning = true;
}
#Override
protected Void doInBackground(Void... ignore) {
for (int i = 0; !isCancelled() && i < 100; i++) {
//Log.i(TAG, "publishProgress(" + i + "%)");
SystemClock.sleep(100);
publishProgress(i);
}
return null;
}
#Override
protected void onProgressUpdate(Integer... percent) {
// Proxy the call to the Activity
fragment.mCallbacks.onProgressUpdate(percent[0]);
}
#Override
protected void onCancelled() {
// Proxy the call to the Activity
fragment.mCallbacks.onCancelled();
fragment.mRunning = false;
}
#Override
protected void onPostExecute(Void ignore) {
// Proxy the call to the Activity
fragment.mCallbacks.onPostExecute();
fragment.mRunning = false;
}
}
I am thinking it is the context which i am passing the progress dialog in the onCreate method of my Main Activity. Thanks for your help.
First, Activity is subclass of Context, you should know this already. Second, if Activity is destroyed, it is don't have a window anymore. Third, Dialog uses Context (read Activity) not because it wants so, but because it uses window associated with Activity to display itself.
It should be perfectly understandable, why after destroying activity during configuration change, Dialog no longer visible to you.
Method of preserving objects you using is good, but it can't preserve anything that would be destroyed during configuration change, such as any object that related to non-application Context, all this objects you need create manually every time Context changes.
You should use onSaveInstanceState(Bundle) to store state of ProgressDialog (shown or not) and show it again in your onCreate(Bunde) using value stored in Bundle.
Just an example
#Override
protected void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putBoolean("SHOW_DIALOG", mProgressBar.isShowing());
}
//...
#Override
protected void onCreate(Bundle savedInstanceState) {
//...
if (savedInstanceState != null){
if (savedInstanceState.getBoolean("SHOW_DIALOG") && mTaskFragment.isRunning()){
mProgressBar.setTitle("Wacky");
mProgressBar.setMessage("wack");
mProgressBar.setIndeterminate(true);
mProgressBar.show();
}
}
//...

Android Handler changing WeakReference

My static handler has a WeakReference to my Activity (this is to prevent the well documented memory leak issue).
I post a long delayed message and I want this message delivered to my activity (which should be in the foreground).
My concern is that on orientation change, my activity is destroyed and the handler has a reference to the old activity which should have been destroyed.
In order to get around this in my onCreate for the activity I do this.
if(mHandler == null)
mHandler = new LoginHandler(this);
else {
mHandler.setTarget(this);
}
And my handler is declared as a static global variable:
private static LoginHandler mHandler = null;
and the implementing class is also static as below:
private static class LoginHandler extends Handler {
private WeakReference<LoginActivity> mTarget;
LoginHandler(LoginActivity target) {
mTarget = new WeakReference<LoginActivity>(target);
}
public void setTarget(LoginActivity target) {
mTarget = new WeakReference<LoginActivity>(target);
}
#Override
public void handleMessage(Message msg) {
// process incoming messages here
LoginActivity activity = mTarget.get();
switch (msg.what) {
case Constants.SUCCESS:
activity.doSomething();
break;
default:
activity.setStatusMessage("failed " + msg.obj, STATUS_TYPE_DONE);
}
}
}
What I want to know is if there is something wrong with changing the WeakReference on onCreate or is there anything else wrong with this approach?
Thanks,
So I wrote the following test to figure out whether I had the right idea or not and it seems that m approach is correct. In onCreate we change the WeakReference and the posted message will always get delivered to the activity that is in the foreground. If you change this code to always create a new Handler in onCreate you'll notice the update messages do not get delivered.
public class MainActivity extends Activity {
private static int COUNT = 0;
static LoginHandler mHandler;
private static class LoginHandler extends Handler {
private WeakReference<MainActivity> mTarget;
LoginHandler(MainActivity target) {
mTarget = new WeakReference<MainActivity>(target);
}
public void setTarget(MainActivity target) {
mTarget.clear();
mTarget = new WeakReference<MainActivity>(target);
}
#Override
public void handleMessage(Message msg) {
// int duration = Toast.LENGTH_LONG;
// process incoming messages here
MainActivity activity = mTarget.get();
activity.update(msg.arg1);
}
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mHandler == null)
mHandler = new LoginHandler(this);
else
mHandler.setTarget(this);
((Button)findViewById(R.id.button)).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Message msg = new Message();
msg.arg1 = COUNT++;
mHandler.sendMessageDelayed(msg, 3000);
}
});
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
private void update(int count) {
((TextView) findViewById(R.id.hello_world)).setText("Hello World # "+ count);
}
}
A solution in getting away with activity's destroy-and-create life cycle, if you want to retain the active objects is to make use of the "Retent Fragments".
The idea is simple, you are telling the Android system to " retain" your fragment, when it's associated activity is being destroyed and re created. And make sure you grab the current activity's context in the fragment's onAttach() callable, so you are always updating the correct activity.
Below link has more details:
http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html

How to communicate with activity after long task because activity was destroyed

I have created simple application with login feature. I have created separate task for do the login into server called LoginTask and a listener class called LoginListener.
public interface LoginListener {
public void onLoginComplete();
public void onLoginFailure(String msg);
}
public class LoginTask extends AsyncTask<String, Void, Boolean>{
private final LoginListener listener;
private final Context c;
private String msg;
public LoginTask(final Context c, final LoginListener listener) {
this.c = c;
this.listener = listener;
}
#Override
protected Boolean doInBackground(String... args) {
// loging in to server
//return true if success
}
#Override
protected void onPostExecute(Boolean status) {
if(!status){
if(listener != null) listener.onLoginFailure(msg);
return;
}
// the problem is here, listener is null, because activity/fragment destroyed
if(listener != null) listener.onLoginComplete();
}
}
I executed LoginTask from LoginFragment. The LoginFragment implements LoginListener.
public class LoginFragment extends Fragment implements LoginListener{
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.frg_login, container, false);
}
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
doInitView();
};
private void doInitView(){
Button loginButton = (Button) getActivity().findViewById(R.id.login_btn);
Button regButton = (Button) getActivity().findViewById(R.id.toreg_btn);
ButtonListener listener = new ButtonListener();
loginButton.setOnClickListener(listener);
regButton.setOnClickListener(listener);
}
private void doLogin(){
Activity activity = getActivity();
EditText emailText = (EditText)activity.findViewById(R.id.login_email);
EditText pwdText = (EditText)activity.findViewById(R.id.login_pwd);
String email = emailText.getText().toString().trim();
String pwd = pwdText.getText().toString().trim();
if(StringUtil.isAnyNull(email, pwd)){
Popup.showMsg(getActivity(), "Silahkan lengkapi data", Popup.SHORT);
return;
}
savedEmail = email;
savedPwd = pwd;
String url = getActivity().getResources().getString(R.string.url_login);
Popup.showLoading(getActivity(), "Login", "Please wait...");
LoginTask task = new LoginTask(getActivity(), this);
task.execute(url, email, pwd);
}
private final class ButtonListener implements OnClickListener{
#Override
public void onClick(View v) {
switch(v.getId()){
case R.id.login_btn:
doLogin();
break;
case R.id.toreg_btn:
doToRegister();
break;
case R.id.demo_btn:
doDemo();
break;
}
}
}
#Override
public void onLoginComplete() {
// getActivity() is null
((MainActivity)getActivity()).gotoMain();
}
#Override
public void onLoginFailure(String msg) {
}
}
Because of the login task takes time, sometime the device light turn off before the task was finished so activity was destroyed. This caused the task failed to call the listener(fragment). How to solve this problem?
Thanks
AsyncTask should be used for tasks that take a bit longer and return a result into the current activity. However it's not intended for really long running tasks or for those cases where you want to evaluate its results even if the activity has been destroyed. You might consider using a Service here. In any case you shouldn't do updates in onPostExecute() anymore cause the activity context might be gone (see Doctoror Drive's post). Having that service in place, you can either send an Intent or a Broadcast event to the system. Then do the further processing in that intent activity / broadcast receiver.
You can cancel the asynctask in onDestroy() of your LoginActivity.
Override onCancelled() of the asynctask. When the activity is destroyed, a call to onCancelled() will be made instead of onPostExecute()
Here you can avoid a call back to the LoginActivity.
You should use Service or IntentService. because AsyncTask does not record any variables or context of Activity. When you finish login task launch PendingIntent or startActivity(intent). This can be best practice of Android. This way you never get exception.
In onLoginComplete and onLoginFailure check if the fragment is still attached to the activity. If not, do nothing.
#Override
public void onLoginComplete() {
if (isAdded() && !isRemoving() && !isDetached()) {
((MainActivity)getActivity()).gotoMain();
}
}

Android. Fragment getActivity() sometimes returns null

In developer console error reports sometimes I see reports with NPE issue. I do not understand what is wrong with my code. On emulator and my device application works good without forcecloses, however some users get NullPointerException in fragment class when the getActivity() method is called.
Activity
pulic class MyActivity extends FragmentActivity{
private ViewPager pager;
private TitlePageIndicator indicator;
private TabsAdapter adapter;
#Override
public void onCreate(Bundle savedInstanceState) {
pager = (ViewPager) findViewById(R.id.pager);
indicator = (TitlePageIndicator) findViewById(R.id.indicator);
adapter = new TabsAdapter(getSupportFragmentManager(), false);
adapter.addFragment(new FirstFragment());
adapter.addFragment(new SecondFragment());
indicator.notifyDataSetChanged();
adapter.notifyDataSetChanged();
// push first task
FirstTask firstTask = new FirstTask(MyActivity.this);
// set first fragment as listener
firstTask.setTaskListener((TaskListener) adapter.getItem(0));
firstTask.execute();
}
indicator.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageSelected(int position) {
Fragment currentFragment = adapter.getItem(position);
((Taskable) currentFragment).executeTask();
}
#Override
public void onPageScrolled(int i, float v, int i1) {}
#Override
public void onPageScrollStateChanged(int i) {}
});
}
AsyncTask class
public class FirstTask extends AsyncTask{
private TaskListener taskListener;
...
#Override
protected void onPostExecute(T result) {
...
taskListener.onTaskComplete(result);
}
}
Fragment class
public class FirstFragment extends Fragment immplements Taskable, TaskListener{
public FirstFragment() {
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.first_view, container, false);
}
#Override
public void executeTask() {
FirstTask firstTask = new FirstTask(MyActivity.this);
firstTask.setTaskListener(this);
firstTask.execute();
}
#Override
public void onTaskComplete(T result) {
// NPE is here
Resources res = getActivity().getResources();
...
}
}
Maybe this error happens when applications resumed from background. In this case how I should handle this situation properly?
It seems that I found a solution to my problem.
Very good explanations are given here and here.
Here is my example:
pulic class MyActivity extends FragmentActivity{
private ViewPager pager;
private TitlePageIndicator indicator;
private TabsAdapter adapter;
private Bundle savedInstanceState;
#Override
public void onCreate(Bundle savedInstanceState) {
....
this.savedInstanceState = savedInstanceState;
pager = (ViewPager) findViewById(R.id.pager);;
indicator = (TitlePageIndicator) findViewById(R.id.indicator);
adapter = new TabsAdapter(getSupportFragmentManager(), false);
if (savedInstanceState == null){
adapter.addFragment(new FirstFragment());
adapter.addFragment(new SecondFragment());
}else{
Integer count = savedInstanceState.getInt("tabsCount");
String[] titles = savedInstanceState.getStringArray("titles");
for (int i = 0; i < count; i++){
adapter.addFragment(getFragment(i), titles[i]);
}
}
indicator.notifyDataSetChanged();
adapter.notifyDataSetChanged();
// push first task
FirstTask firstTask = new FirstTask(MyActivity.this);
// set first fragment as listener
firstTask.setTaskListener((TaskListener) getFragment(0));
firstTask.execute();
}
private Fragment getFragment(int position){
return savedInstanceState == null ? adapter.getItem(position) : getSupportFragmentManager().findFragmentByTag(getFragmentTag(position));
}
private String getFragmentTag(int position) {
return "android:switcher:" + R.id.pager + ":" + position;
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("tabsCount", adapter.getCount());
outState.putStringArray("titles", adapter.getTitles().toArray(new String[0]));
}
indicator.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageSelected(int position) {
Fragment currentFragment = adapter.getItem(position);
((Taskable) currentFragment).executeTask();
}
#Override
public void onPageScrolled(int i, float v, int i1) {}
#Override
public void onPageScrollStateChanged(int i) {}
});
The main idea in this code is that, while running your application normally, you create new fragments and pass them to the adapter. When you are resuming your application fragment manager already has this fragment's instance and you need to get it from fragment manager and pass it to the adapter.
UPDATE
Also, it is a good practice when using fragments to check isAdded before getActivity() is called. This helps avoid a null pointer exception when the fragment is detached from the activity. For example, an activity could contain a fragment that pushes an async task. When the task is finished, the onTaskComplete listener is called.
#Override
public void onTaskComplete(List<Feed> result) {
progress.setVisibility(View.GONE);
progress.setIndeterminate(false);
list.setVisibility(View.VISIBLE);
if (isAdded()) {
adapter = new FeedAdapter(getActivity(), R.layout.feed_item, result);
list.setAdapter(adapter);
adapter.notifyDataSetChanged();
}
}
If we open the fragment, push a task, and then quickly press back to return to a previous activity, when the task is finished, it will try to access the activity in onPostExecute() by calling the getActivity() method. If the activity is already detached and this check is not there:
if (isAdded())
then the application crashes.
Ok, I know that this question is actually solved but I decided to share my solution for this. I've created abstract parent class for my Fragment:
public abstract class ABaseFragment extends Fragment{
protected IActivityEnabledListener aeListener;
protected interface IActivityEnabledListener{
void onActivityEnabled(FragmentActivity activity);
}
protected void getAvailableActivity(IActivityEnabledListener listener){
if (getActivity() == null){
aeListener = listener;
} else {
listener.onActivityEnabled(getActivity());
}
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (aeListener != null){
aeListener.onActivityEnabled((FragmentActivity) activity);
aeListener = null;
}
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
if (aeListener != null){
aeListener.onActivityEnabled((FragmentActivity) context);
aeListener = null;
}
}
}
As you can see, I've added a listener so, whenever I'll need to get Fragments Activity instead of standard getActivity(), I'll need to call
getAvailableActivity(new IActivityEnabledListener() {
#Override
public void onActivityEnabled(FragmentActivity activity) {
// Do manipulations with your activity
}
});
The best to get rid of this is to keep activity reference when onAttach is called and use the activity reference wherever needed, for e.g.
#Override
public void onAttach(Context context) {
super.onAttach(context);
mContext = context;
}
#Override
public void onDetach() {
super.onDetach();
mContext = null;
}
Edited, since onAttach(Activity) is depreciated & now onAttach(Context) is being used
Don't call methods within the Fragment that require getActivity() until onStart in the parent Activity.
private MyFragment myFragment;
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
myFragment = new MyFragment();
ft.add(android.R.id.content, youtubeListFragment).commit();
//Other init calls
//...
}
#Override
public void onStart()
{
super.onStart();
//Call your Fragment functions that uses getActivity()
myFragment.onPageSelected();
}
I've been battling this kind of problem for a while, and I think I've come up with a reliable solution.
It's pretty difficult to know for sure that this.getActivity() isn't going to return null for a Fragment, especially if you're dealing with any kind of network behaviour which gives your code ample time to withdraw Activity references.
In the solution below, I declare a small management class called the ActivityBuffer. Essentially, this class deals with maintaining a reliable reference to an owning Activity, and promising to execute Runnables within a valid Activity context whenever there's a valid reference available. The Runnables are scheduled for execution on the UI Thread immediately if the Context is available, otherwise execution is deferred until that Context is ready.
/** A class which maintains a list of transactions to occur when Context becomes available. */
public final class ActivityBuffer {
/** A class which defines operations to execute once there's an available Context. */
public interface IRunnable {
/** Executes when there's an available Context. Ideally, will it operate immediately. */
void run(final Activity pActivity);
}
/* Member Variables. */
private Activity mActivity;
private final List<IRunnable> mRunnables;
/** Constructor. */
public ActivityBuffer() {
// Initialize Member Variables.
this.mActivity = null;
this.mRunnables = new ArrayList<IRunnable>();
}
/** Executes the Runnable if there's an available Context. Otherwise, defers execution until it becomes available. */
public final void safely(final IRunnable pRunnable) {
// Synchronize along the current instance.
synchronized(this) {
// Do we have a context available?
if(this.isContextAvailable()) {
// Fetch the Activity.
final Activity lActivity = this.getActivity();
// Execute the Runnable along the Activity.
lActivity.runOnUiThread(new Runnable() { #Override public final void run() { pRunnable.run(lActivity); } });
}
else {
// Buffer the Runnable so that it's ready to receive a valid reference.
this.getRunnables().add(pRunnable);
}
}
}
/** Called to inform the ActivityBuffer that there's an available Activity reference. */
public final void onContextGained(final Activity pActivity) {
// Synchronize along ourself.
synchronized(this) {
// Update the Activity reference.
this.setActivity(pActivity);
// Are there any Runnables awaiting execution?
if(!this.getRunnables().isEmpty()) {
// Iterate the Runnables.
for(final IRunnable lRunnable : this.getRunnables()) {
// Execute the Runnable on the UI Thread.
pActivity.runOnUiThread(new Runnable() { #Override public final void run() {
// Execute the Runnable.
lRunnable.run(pActivity);
} });
}
// Empty the Runnables.
this.getRunnables().clear();
}
}
}
/** Called to inform the ActivityBuffer that the Context has been lost. */
public final void onContextLost() {
// Synchronize along ourself.
synchronized(this) {
// Remove the Context reference.
this.setActivity(null);
}
}
/** Defines whether there's a safe Context available for the ActivityBuffer. */
public final boolean isContextAvailable() {
// Synchronize upon ourself.
synchronized(this) {
// Return the state of the Activity reference.
return (this.getActivity() != null);
}
}
/* Getters and Setters. */
private final void setActivity(final Activity pActivity) {
this.mActivity = pActivity;
}
private final Activity getActivity() {
return this.mActivity;
}
private final List<IRunnable> getRunnables() {
return this.mRunnables;
}
}
In terms of its implementation, we must take care to apply the life cycle methods to coincide with the behaviour described above by Pawan M:
public class BaseFragment extends Fragment {
/* Member Variables. */
private ActivityBuffer mActivityBuffer;
public BaseFragment() {
// Implement the Parent.
super();
// Allocate the ActivityBuffer.
this.mActivityBuffer = new ActivityBuffer();
}
#Override
public final void onAttach(final Context pContext) {
// Handle as usual.
super.onAttach(pContext);
// Is the Context an Activity?
if(pContext instanceof Activity) {
// Cast Accordingly.
final Activity lActivity = (Activity)pContext;
// Inform the ActivityBuffer.
this.getActivityBuffer().onContextGained(lActivity);
}
}
#Deprecated #Override
public final void onAttach(final Activity pActivity) {
// Handle as usual.
super.onAttach(pActivity);
// Inform the ActivityBuffer.
this.getActivityBuffer().onContextGained(pActivity);
}
#Override
public final void onDetach() {
// Handle as usual.
super.onDetach();
// Inform the ActivityBuffer.
this.getActivityBuffer().onContextLost();
}
/* Getters. */
public final ActivityBuffer getActivityBuffer() {
return this.mActivityBuffer;
}
}
Finally, in any areas within your Fragment that extends BaseFragment that you're untrustworthy about a call to getActivity(), simply make a call to this.getActivityBuffer().safely(...) and declare an ActivityBuffer.IRunnable for the task!
The contents of your void run(final Activity pActivity) are then guaranteed to execute along the UI Thread.
The ActivityBuffer can then be used as follows:
this.getActivityBuffer().safely(
new ActivityBuffer.IRunnable() {
#Override public final void run(final Activity pActivity) {
// Do something with guaranteed Context.
}
}
);
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// run the code making use of getActivity() from here
}
I know this is a old question but i think i must provide my answer to it because my problem was not solved by others.
first of all : i was dynamically adding fragments using fragmentTransactions.
Second: my fragments were modified using AsyncTasks (DB queries on a server).
Third: my fragment was not instantiated at activity start
Fourth: i used a custom fragment instantiation "create or load it" in order to get the fragment variable.
Fourth: activity was recreated because of orientation change
The problem was that i wanted to "remove" the fragment because of the query answer, but the fragment was incorrectly created just before. I don't know why, probably because of the "commit" be done later, the fragment was not added yet when it was time to remove it. Therefore getActivity() was returning null.
Solution :
1)I had to check that i was correctly trying to find the first instance of the fragment before creating a new one
2)I had to put serRetainInstance(true) on that fragment in order to keep it through orientation change (no backstack needed therefore no problem)
3)Instead of "recreating or getting old fragment" just before "remove it", I directly put the fragment at activity start.
Instantiating it at activity start instead of "loading" (or instantiating) the fragment variable before removing it prevented getActivity problems.
In Kotlin you can try this way to handle getActivity() null condition.
activity?.let { // activity == getActivity() in java
//your code here
}
It will check activity is null or not and if not null then execute inner code.

Categories

Resources