Android Handler changing WeakReference - android

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

Related

Method of DialogFragment instantiated in mainActivity.runOnUiThread method returns null but dialog is shown

Inside my SSHsocket class (not extending or implementing anything) I instantiate HandlerThread:
socketHandlerThread = new HandlerThread(sessionTag);
socketHandlerThread.start();
Then I call connect() method:
socketHandler = new Handler(socketHandlerThread.getLooper()) {
public void handleMessage(Message msg) {
switch (msg.what) {
case TerminalService.SERVICE_TO_SOCKET_DO_CONNECT:
try {
connect();
} catch (IOException e) {
Message statusMsg = Message.obtain(null,SOCKET_TO_SERVICE_STATUS_DEAD, sessionDetailData.getUuid());
serviceHandler.sendMessage(statusMsg);
Log.e("SSH Socket id:" + sessionDetailData.getUuid() + " fails. ", e.toString());
}
break;
Inside the connect() method I need to open a yes/no dialog:
final String titleMessage = "Do you want to accept the hostkey (type " + algo + ") from " + host + " ?\n";
mainActivity.runOnUiThread(new Runnable() {
public void run() {
FragmentTransaction fragmentTransaction=mainActivity.getFragmentManager().beginTransaction();
AcceptKeyDialog acceptKeyDialog = new AcceptKeyDialog();
acceptKeyDialog.show(fragmentTransaction, "KEY_ACCEPT_DIALOG");
acceptKeyDialog.getTitleView().setText(titleMessage);
}
});
What happens is that dialog is populated as expected even with buttons. But when debugging it then breakpoints which are (anywhere) inside of runOnUiThread() show attributes of acceptKeyDialog fragment instance being null (inflated views, listener.. I call it controller etc.). So obviously calling the getTitleView() method of AcceptKeyDialog also returns null.
public class AcceptKeyDialog extends DialogFragment {
private View keyDialogView;
//inner listener class for buttons
private AceeptKeyDialogFragmentController controller;
private TextView title;
private Button yesButton;
private Button noButton;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Window window = getDialog().getWindow();
window.setBackgroundDrawable(new ColorDrawable(Color.BLACK));
//DialogFragment.STYLE_NO_TITLE is not working as it should
window.requestFeature(Window.FEATURE_NO_TITLE);
controller = new AceeptKeyDialogFragmentController();
keyDialogView = inflater.inflate(R.layout.accept_key_dialog, container, false);
title = (TextView) keyDialogView.findViewById(R.id.accept_key_title);
yesButton = (Button) keyDialogView.findViewById(R.id.accept_key_yes_button);
noButton = (Button) keyDialogView.findViewById(R.id.accept_key_no_button);
title.setTextColor(Color.GREEN);
yesButton.setOnClickListener(controller);
noButton.setOnClickListener(controller);
return keyDialogView;
}
public TextView getTitleView(){
return title;
}
private class AceeptKeyDialogFragmentController implements View.OnClickListener {
#Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.accept_key_yes_button:
break;
case R.id.accept_key_no_button:
break;
}
}
}
I thought this might be better than using handler messages(or handler.post.. or by passing runnable in the message) but obviously I missed something fundamental in the HandlerThread concept. I thought also that it might be something related to passed reference of mainActivity which is done by mainActivity=(MainActivity)msg.obj
But I don't see activity status being changed (monitoring MainActivity onStop() method)
#Override
protected void onStop(){
Log.e("MainActivity is in onStop state","");
super.onStop();
}
The final goal is to pass user decision back to worker thread and it continues based on response. Can you advice please?
I've earned Tumbleweed badge on this question so it deserves an answer :)
However the answer might be sort of disappointing as I don't have full understanding of it. What I've described seems to be general problem related to Main Thread object backward visibility inside another thread where the mainActivity.runOnUiThread() method runs. As you can I see below I don't instantiate a dialog object. Instead I run in Main Thread method activity.fireInteractiveDialog(args) which does this for me.
So I had to stop the thread in order to get user input and then wait for an condition(as well as the input itself) making the thread running again. It was all done by "guarded lock" programming construct and of course instance of HandlerThread() was the thread delivering the condition state and notified stopped thread to run again.
Here is the code:
public synchronized void getUserInput() {
activity.runOnUiThread(new Runnable() {
#Override
public void run() {
Bundle args = new Bundle();
args.putString("somethingDialogTag", tag);
args.putString("somethingDialogTitle", title);
args.putStringArray("somethingDialogContent", content);
args.putBoolean("somethingDialogPassword", isPassword);
if (isPassword) {
args.putString("somethingDialogExistingPassword", sessionDetailData.getPassword());
}
//lets open the dialog
activity.fireInteractiveDialog(args);
}
});
while (!isInputAvailable) {
try {
Log.d("Thread " + String.valueOf(Thread.currentThread().getId()), " going to wait.");
wait();
Log.d("Thread " + String.valueOf(Thread.currentThread().getId()), " has woke up.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
And inside the same class the HandlerThread changed (as carried by a Message object) variable isInputAvailable as well as provided user input once it was ready.
socketHandlerThread = new HandlerThread(tag);
socketHandlerThread.start();
socketHandler = new Handler(socketHandlerThread.getLooper()) {
public void handleMessage(Message msg) {
switch (msg.what) {
case Bus.SERVICE_TO_SOCKET_STATUS_KEY_ACCEPTANCE:
isKeyAccepted = ((MessageHolder) msg.obj).getLogic();
synchronized (advancedVerifier) {
advancedVerifier.notifyAll();
}
Log.e(tag + "User decided to accept key", String.valueOf(((MessageHolder) msg.obj).getLogic()));
break;
I hope it helps somebody someday. It would be great if somebody understanding the object visibility across the threads(Android main thread and workers) responds.

How to manage custom adapters onPause , onResume of Activity in Android

I have an adapter used to display messages on the list view alike messages in chat application . I am able to display the content flawlessly once the activity is created , but when I go back and create activity again , adapter don't work as usual .
What I found in debugging is follows:
function receives() is called when message is received and update the
register , as I mentioned above there is no problem to display the
data in list view once the activity is created , but once I go back
and relauch the activity I am not able to display received messages .
Is there something I am missing in onResume() onPause or onStart() method with respect to custom adapter such as registering or decalring the custom adapter again? Thanks for help.
Following is the code of my activity class which uses custom adapter to display sent and received messages:
public class hotListener extends ListActivity {
private XMPPConnection connection;
private IBinder binder;
private Handler mHandler = new Handler();
private ArrayList<String> messages = new ArrayList<String>();
ArrayList<ChatMessage> messagex= new ArrayList<ChatMessage>();;
ChattingAdapter adaptex;
Intent mIntent ;
private ListView listview;
EditText sender_message ;
String msg;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.listener);
//messagex.add(new ChatMessage("Hello", false));
adaptex = new ChattingAdapter(getApplicationContext(),messagex);
setListAdapter(adaptex);
Button send_button = (Button) findViewById(R.id.chat_send_message);
sender_message = (EditText) findViewById(R.id.chat_input);
send_button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
msg = sender_message.getText().toString();
sender_message.setText("");
if(!(msg.length()==0)){
messagex.add(new ChatMessage(msg, true));
//addNewMessage(new ChatMessage(msg, true));
adaptex.notifyDataSetChanged();
getListView().setSelection(messagex.size()-1);
}
}
});
if(!isMyServiceRunning()){
System.out.println("seems like service not running");
startService(new Intent(this,xService.class));
System.out.print(" now started ");
}
}
#Override
protected void onStart(){
super.onStart();
Boolean kuch = bindService(new Intent(this,xService.class), mConnection,Context.BIND_AUTO_CREATE);
//System.out.println(kuch);
System.out.println("bind done");
}
private void receives(XMPPConnection connection2) {
//ChatManager chatmanager = connection.getChatManager();
connection2.getChatManager().addChatListener(new ChatManagerListener() {
#Override
public void chatCreated(Chat arg0, boolean arg1) {
arg0.addMessageListener(new MessageListener() {
#Override
public void processMessage(Chat chat, Message message) {
final String from = message.getFrom();
final String body = message.getBody();
mHandler.post(new Runnable() {
ChatMessage kudi = new ChatMessage(body, false);
#Override
public void run() {
messagex.add(kudi);
adaptex.notifyDataSetChanged();
getListView().setSelection(messagex.size()-1);
Toast.makeText(hotListener.this,body,Toast.LENGTH_SHORT).show(); }
});
}
});
}
});
}
private boolean isMyServiceRunning() {
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
for(RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)){
if(xService.class.getName().equals(service.service.getClassName())){
return true;
}
}
//System.out.print("false");
return false;
}
#Override
protected void onResume() {
bindService(new Intent(this, xService.class), mConnection, Context.BIND_AUTO_CREATE);
super.onResume();
}
#Override
protected void onPause() {
unbindService(mConnection);
super.onPause();
}
private ServiceConnection mConnection = new ServiceConnection() {
#Override
public void onServiceDisconnected(ComponentName name) {
connection = null;
service = null;
}
#Override
public void onServiceConnected(ComponentName name, IBinder binder) {
//System.out.println("binding in hot listener");
service = ((xService.MyBinder)binder).getService();
connection = service.getConnection();
receives(connection);
Log.wtf("Service","connected");
}
};
void addNewMessage(ChatMessage m)
{
System.out.println("1");
messagex.add(m);
System.out.println("2");
adaptex.notifyDataSetChanged();
System.out.println("3");
getListView().setSelection(messagex.size()-1);
}
}
Here is my custom adapter (there is no problem in custom adapter but adding to make things clear) :
public class ChattingAdapter extends BaseAdapter{
private Context mContext;
private ArrayList<ChatMessage> mMessages;
public ChattingAdapter(Context context, ArrayList<ChatMessage> messages) {
super();
this.mContext = context;
this.mMessages = messages;
}
#Override
public int getCount() {
return mMessages.size();
}
#Override
public Object getItem(int position) {
return mMessages.get(position);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ChatMessage message = (ChatMessage) this.getItem(position);
ViewHolder holder;
if(convertView == null)
{
holder = new ViewHolder();
convertView = LayoutInflater.from(mContext).inflate(R.layout.listitem, parent, false);
holder.message = (TextView) convertView.findViewById(R.id.text1);
convertView.setTag(holder);
}
else
holder = (ViewHolder) convertView.getTag();
holder.message.setText(message.getMessage());
LayoutParams lp = (LayoutParams) holder.message.getLayoutParams();
//Check whether message is mine to show green background and align to right
if(message.isMine())
{ holder.message.setBackgroundResource(R.drawable.msgbox_new_selected_go_up);
lp.gravity = Gravity.RIGHT;
}
//If not mine then it is from sender to show orange background and align to left
else
{
holder.message.setBackgroundResource(R.drawable.msgbox_other_go_up);
lp.gravity = Gravity.LEFT;
}
holder.message.setLayoutParams(lp);
//holder.message.setTextColor(R.color.textColor);
return convertView;
}
private static class ViewHolder
{
TextView message;
}
#Override
public long getItemId(int position) {
//Unimplemented, because we aren't using Sqlite.
return position;
}
}
p.s: I am not storing any messages in sqlite as I dont want to restore messages for now, but I want new messages to be displayed at least onresume of activty. I can display sent messages after pressing send button but no received messages which works fine for the first time activity is created.
EDIT: I did more debugging , it turns out problem is not in resume activity , if I dont use receives() function for first time , and resume activity after going back , then receives() will work , that means , function inside receives() : getListView().setSelection(messagex.size()-1); works only once .
Either first time on receiving message or next time if and only if its not called first time on activity .
I think problem lies when you try to resume activity , you are still running the previous mHandler running and thus your instance of message is not destroyed and when you resume your activity it creates a problem . Make sure your mhandler destroys all instance of objects when unstop is called.
There's no place in your code where you save your messagex ArrayList. When you quit your activity by hitting back, your array get's distroyed (Garbage Collection takes care of it).
When you relaunch your activity your messagex ArrayList is created again, it's a brand new variable.
In fact, you're not relaunching your activity, you're creating a new instance.
EDIT:
I've never worked with the XMPPConnection objects before, but something else worth trying is the following:
When binding to the service, you're calling connection2.getChatManager().addChatListener and also arg0.addMessageListener but when unbinding you're not calling any removeXXX methods. I could be that since you're not removing your listeners, the whole XMPPConnection object still have references to the listeners that live in a dead Activity, and they are not being garbage collected.

DialogFragment: always a Null Pointer Exception using a progress bar. How to solve it?

Consider this DialogFragment:
public class RollTriggerDialog extends DialogFragment{
private ProgressDialog _dialog;
int _progress;
public Handler _progressHandler;
public RollTriggerDialog() {
// empty
}
#Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
_dialog = new ProgressDialog(getActivity());
this.setStyle(STYLE_NO_TITLE, getTheme());
_dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
_dialog.setProgress(0);
_progressHandler = new Handler() {
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (_progress >= 100) {
_dialog.dismiss();
} else {
_progress++;
_dialog.incrementProgressBy(1);
_progressHandler.sendEmptyMessageDelayed(0,100);
}
}
};
//_progressHandler.sendEmptyMessage(0); <- This uncommented would start the progress
return _dialog;
}
}
It is just a horizontal progressbar with a handler, once the handler receives one message the progressbar goes from 0 to 100.
I am always getting a Null Pointer Exception if I want to trigger that sendEmptyMessage by myself from an activity:
public class MainActivity extends FragmentActivity {
private RollTriggerDialog mRollTriggerDialog;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentManager fm = getSupportFragmentManager();
mRollTriggerDialog = new RollTriggerDialog();
mRollTriggerDialog.show(fm, "addDiceDialog_tag");
((RollTriggerDialog)fm.findFragmentByTag("addDiceDialog_tag"))._progressHandler.sendEmptyMessage(0); // <--- NPE HERE!
}
}
If the line of sendEmptyMessage is uncommented in the dialogFragment and the line with NPE in the main activity is commented; the app runs. What is wrong with that invocation?
Note that this is the whole code, excepting manifest and layout files.
The NullPointerException appears because the findFragmentByTag returns null. The solution is to call fm.executePendingTransactions() before you use the findFragmentByTag method to execute that fragment transaction right away(see this question for more details).
Also, the Handler reference will be null at that moment so you'll want to initialize it in one of the fragment's lifecycle methods, for example, onCreate:
public static Handler _progressHandler; // should be made static
//...
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_progressHandler = new Handler() {
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (_progress >= 100) {
_dialog.dismiss();
} else {
_progress++;
_dialog.incrementProgressBy(1);
_progressHandler.sendEmptyMessageDelayed(0, 100);
}
}
};
}

How to handle Handler messages when activity/fragment is paused

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);
}
}

Android emulator crashes while using concurrency

This question is about using the Google Android SDK, in the Java programming language.
My question could be boiled down to: Why is this code causing the android emulator to crash?
I've been wrestling for a few days with concurrency related to setting up different threads for a game app.
I have made many variations, but they have all failed. At this point, I just want to get a basic concurrent setup going. The worst part is that it is the emulator that crashes, so DDMS reports nothing; therefore I'm pretty clueless as to where the issue is.
The following code shows an activity (class Main), that calls class SceneManager, which creates a thread to be used for game logic stuff. A 3rd class, StatusChannel, is (will be) used to communicate status information between the different threads (Eventually, there will also be a OpenGL rendering thread).
The emulator crashes at different times. It may run for 20 seconds or for 5 minutes.
The setContentView(R.layout.main) in the Activity class just the set basic layout that Eclipse creates.
I've commented out the usage of Node (Created in the Activity class and accessed in SceneManager)
I have installed sdk versions 1.5 through 2.3 -- The current app is targeted at 2.1
The issue has something to do with the SceneManager class. I'm specially suspicious of the run() method.
Here are the 3 classes.
Sorry for the code length.
public class Main extends Activity {
private SceneManager mSceneManager;
private volatile Node mSceneGraph = new Node();
private volatile Status mStatusChannel = new Status();
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
Log.d("-- Main", "onCreate()");
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Holds the scene assets, such as the stage,
// the agents, camera, etc.
mSceneManager = new SceneManager(mSceneGraph, mStatusChannel);
mSceneManager.onCreate();
}
#Override
protected void onResume() {
Log.d("-- Main", "onResume()");
super.onResume();
mSceneManager.onResume();
}
#Override
protected void onPause() {
Log.d("-- Main", "onPause()");
super.onPause();
mSceneManager.onPause();
}
#Override
protected void onDestroy() {
Log.d("-- Main", "onDestroy()");
super.onDestroy();
mSceneManager.onDestroy();
}
}
public class SceneManager implements Runnable{
private Thread mThread;
private volatile Status mStatusChannel;
private volatile Node mSceneGraph;
private volatile long mMillis = 0;
private volatile PrepareVisitor mPrepareVisitor;
private volatile int mStatus = Status.UNKNOWN_STATUS;
SceneManager(Node sceneGraph, Status statusChannel) {
mPrepareVisitor = new PrepareVisitor();
mStatusChannel = statusChannel;
mSceneGraph = sceneGraph;
mMillis = SystemClock.uptimeMillis();
mThread = new Thread(this);
mThread.setName("LogicThread");
mStatusChannel.setSceneManagerStatus(Status.READY_STATUS);
}
public void onCreate() {
Log.d("-- SceneManager", "onCreate()...");
// This will start the thread in a paused state.
mThread.start();
}
public void onResume() {
Log.d("-- SceneManager", "onResume()...");
// Unpause the status manager, if it is currently paused.
if (mStatusChannel.getSceneManagerStatus() == Status.PAUSED_STATUS) {
mStatusChannel.setSceneManagerStatus(Status.READY_STATUS);
}
}
public void onPause() {
Log.d("-- SceneManager", "onPause()...");
if (mStatusChannel.getSceneManagerStatus() != Status.UNKNOWN_STATUS) {
mStatusChannel.setSceneManagerStatus(Status.PAUSED_STATUS);
}
}
public void onDestroy() {
mStatusChannel.setSceneManagerStatus(Status.QUIT_STATUS);
try {
mThread.join();
}
catch (InterruptedException e) {
Log.d("-- SceneManager", "InterruptedException");
}
}
/**
* This method should not be called by clients of this class.
*/
#Override
public void run() {
Log.d("-- SceneManager", "Called...");
// Main logic loop.
outer: while (true) {
// How much time has elapsed since last call.
long timeDelta = SystemClock.uptimeMillis() - mMillis;
switch (mStatus) {
case Status.READY_STATUS:
//mPrepareVisitor.go(mSceneGraph, timeDelta);
break;
case Status.PAUSED_STATUS:
break;
case Status.QUIT_STATUS:
break outer;
case Status.UNKNOWN_STATUS:
int renderStatus = mStatusChannel.getRendererStatus();
if (renderStatus == Status.READY_STATUS) {
mStatusChannel.setSceneManagerStatus(Status.READY_STATUS);
}
break;
}
mStatus = mStatusChannel.getSceneManagerStatus();
// Update the time.
mMillis = SystemClock.uptimeMillis();
}
}
}
public class Status {
/* Generic Statuses */
public final static int UNKNOWN_STATUS = 0;
public final static int READY_STATUS = 1;
public final static int PAUSED_STATUS = 2;
public final static int QUIT_STATUS = 3;
/* Current statuses values */
private int mSceneManagerStatus = UNKNOWN_STATUS ;
private int mRendererStatus = UNKNOWN_STATUS ;
public synchronized int getSceneManagerStatus() {
return mSceneManagerStatus;
}
public synchronized int getRendererStatus() {
return mRendererStatus;
}
public synchronized void setSceneManagerStatus(int status) {
mSceneManagerStatus = status;
}
public synchronized void setRendererStatus(int status) {
mRendererStatus = status;
}
}
-- EDIT --
This issue happens even with something as simple as this:
public class ThreadActivity extends Activity {
private Booboo mBooboo;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mBooboo = new Booboo();
mBooboo.onCreate();
}
}
public class Booboo implements Runnable {
private Thread mThread;
Booboo() {
mThread = new Thread(this, "SceneManagerThread");
}
public void onCreate() {
Log.d("Booboo", "Thread started");
mThread.start();
}
#Override
public void run() {
while (true) {}
}
}
I know the first reaction is to say that it's the while(true){}. Just remember that this is a contrived example to show the issue. In my own code, I do the lifecycle activity as described in the docs. The issue is that the emulator crashes after some time in an infinite loop like that, whether you have break conditions or not.
You probably want to look into AsyncTask. There is great article here : http://android-developers.blogspot.com/2009/05/painless-threading.html

Categories

Resources