We have a Webservice class that takes a WebserviceListener as the callback for that specific webservice call. My problem is that Android can recreate my Activity (orientation change, etc...), and then I have the old references in that callback. When I try to set the visibility of a view, or set my hidden flag to vai saveInstanceState method, they are all recreated. How should I address that problem?
The code I have:
public class UploadActivity extends Activity implements {
private Button mButton;
private volatile boolean mHidden;
private class UploadWSListener extends WebServiceAdapter {
#Override
public void onComplete(Bundle bundle) {
mSuggestion = (Suggestion) bundle.getSerializable(WebService.BUNDLE_DATA);
if (mSuggestion.isAutomatic()) {
myHandler.sendEmptyMessage(1);
}
else {
myHandler.sendEmptyMessage(0);
}
}
#Override
public void onServerError(ErrorResult error, Bundle bundle) {
onError(error.getDebugInfo());
}
#Override
public void onError(WebServiceException wse, Bundle bundle) {
onError(wse.getMessage());
}
private void onError(String message) {
Log.w(TAG, "Error downloading suggestions. Error: " + message);
myHandler.sendEmptyMessage(-1);
}
}
private class PhotoUploadHandler extends Handler {
#Override
public void handleMessage(Message msg) {
Log.d(TAG, "Handler is ivoked. What: " + msg.what);
switch (msg.what) {
case 0:
mButton.setText("Upload");
break;
case 1:
mButton.setVisibility(View.GONE);
mHidden = true;
break;
case -1:
Toast.makeText(PhotoUploadActivity.this, "Network error", Toast.LENGTH_LONG).show();
finish();
break;
}
Log.i(TAG, "Handler with " + msg.what + " run.");
}
}
#Override
protected void onSaveInstanceState(Bundle outState) {
Log.i(TAG, "onSaveInstanceState hidden:" + mHidden);
super.onSaveInstanceState(outState);
// outState.putBoolean("hide", mButton.getVisibility() == View.GONE);
outState.putBoolean("hide", mHidden);
}
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
if (savedInstanceState != null && savedInstanceState.getBoolean("hide")) {
mButton.setVisibility(View.GONE);
}
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myHandler = new UploadHandler();
Model model = Model.instance(getApplicationContext());
WebService service = model.getWebService();
if (Model.u) {
Model.u = false;
service.getSuggestions(new UploadWSListener(), Prefs.getUserToken(getApplicationContext()), getLatitude(),getLongitude(), getAltitude());
}
}
}
My main questions:
How can I use this callback design with an Activity? (I'm pretty much stuck with the design)
Is it even smart to hold Views as instance variables in my Activity, if they are recreated unpredictably, or just get them via findViewByID?
I think you need a way to register you activity to and from the listener doing this you can react on orientation changes by unregistering the activity and registering it again when the new activity is created. As far as I know between the destruction and recreation all calls to the "old" activity will be retained till the new activity was created.
Holding views in your activity is ok as they will be released/destructed when your activity gets destroyed. If you have to access them multiple times it's ok to hold them as a class member as getting them via findViewByID() can be expensive when you layout is very deep structured.
Related
I discovered a strange behaviour today.
I have my activity which connects to the GoogleApiClient in onStart() and disconnects in the onStop()
The activity uses a GridViewPager to show my fragments. To send messages through the Data Layer i use a callback interface between activity and fragment.
If i call sendMessage() from a button within the Activity layout it works fine. If sendMessage() is executed by the fragment using the callback interface sendMessage() shows the "not connected" Toast.
In both ways the same method in the Activity is called so how is it possible that it behaves different ?
I should mention that the problem only occours after the application is restarted for the first time.
Activity
public class WearPlex extends WearableActivity implements
NavigationRemoteCallbacks,
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
private List<Node> nodeList = new ArrayList<Node>();
private List<Fragment> fragmentList = new ArrayList<Fragment>();
private GoogleApiClient googleApiClient;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_wear_plex);
setAmbientEnabled();
fragmentList.add(NavigationRemoteFragment.getInstance(this));
GridViewPager mGridPager = (GridViewPager)findViewById(R.id.gridViewPager);
mGridPager.setAdapter(new MainGridPageAdapter(getFragmentManager(), fragmentList));
googleApiClient = new GoogleApiClient.Builder(this)
.addApi(Wearable.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
#Override
protected void onStart() {
super.onStart();
googleApiClient.connect();
}
#Override
protected void onStop() {
googleApiClient.disconnect();
super.onStop();
}
#Override
public void onConnected(Bundle bundle) {
Toast.makeText(this, "Connected", Toast.LENGTH_SHORT).show();
nodeList.clear();
Wearable.NodeApi.getConnectedNodes(googleApiClient).setResultCallback(new ResultCallback<NodeApi.GetConnectedNodesResult>() {
#Override
public void onResult(NodeApi.GetConnectedNodesResult nodes) {
for (Node node : nodes.getNodes()) nodeList.add(node);
}
});
}
#Override
public void navigationRemoteSendCommand(String commandPath) {
sendMessage(commandPath, null);
}
public void debugOnClick(View view) {
sendMessage("/debug", null);
}
public void sendMessage(String path, byte[] data) {
if (googleApiClient.isConnected()) {
for (int i = 0; i < nodeList.size(); i++) {
if (nodeList.get(i).isNearby()) {
Toast.makeText(this, "Send message", Toast.LENGTH_SHORT).show();
Wearable.MessageApi.sendMessage(googleApiClient, nodeList.get(i).getId(), path, data);
}
}
} else {
Toast.makeText(this, "Not connected", Toast.LENGTH_SHORT).show();
}
}
#Override
public void onConnectionFailed(ConnectionResult connectionResult) {
Toast.makeText(this, "Connection failed", Toast.LENGTH_SHORT).show();
}
Fragment
public class NavigationRemoteFragment extends Fragment {
private static NavigationRemoteFragment navigationRemoteFragment = null;
private NavigationRemoteCallbacks callbackHandler = null;
private ImageButton navBtnCenter;
public static NavigationRemoteFragment getInstance(NavigationRemoteCallbacks handler) {
if (navigationRemoteFragment == null) {
navigationRemoteFragment = new NavigationRemoteFragment();
navigationRemoteFragment.callbackHandler = handler;
}
return navigationRemoteFragment;
}
public NavigationRemoteFragment() {
// Required empty public constructor
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Inflate the layout for this fragment
View v = inflater.inflate(R.layout.fragment_navigation_remote, container, false);
navBtnCenter = (ImageButton)v.findViewById(R.id.navBtnCenter);
navBtnCenter.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
callbackHandler.navigationRemoteSendCommand("/debug");
}
});
return v;
}
}
Callback interface
public interface NavigationRemoteCallbacks {
public void navigationRemoteSendCommand(String commandPath);
}
EDIT 1 code for MainGridPageAdapter
public class MainGridPageAdapter extends FragmentGridPagerAdapter {
private List<Fragment> fragmentList = null;
public MainGridPageAdapter(FragmentManager fm, List<Fragment> fragmentList) {
super(fm);
this.fragmentList = fragmentList;
}
#Override
public Fragment getFragment(int i, int i1) {
if (i1 < fragmentList.size()) return fragmentList.get(i1);
return null;
}
#Override
public int getRowCount() {
return 1;
}
#Override
public int getColumnCount(int i) {
return fragmentList.size();
}
You don't show the code for MainGridPageAdapter so I don't know how it is managing fragments. You mention that the problem occurs after a restart. Looking at the code in WearPlex.onCreate(), I suspect that the problem is caused fragments that are holding a reference to an old, destroyed instance of the activity.
A poorly documented behavior of FragmentManager is that it saves its state across restarts. This is often overlooked, resulting in duplicate fragment instances after a restart. The correct pattern for managing fragment creation in the onCreate() method of the host activity is:
if (savedInstanceState == null) {
// Not a restart
// Create a new instance of the fragment
// Add it to the fragment manager
} else {
// Restart
// The fragment manager has saved and restored the fragment instances
// Use findFragmentById() to get the fragment if you need it
}
You are not using savedInstanceState in onCreate() to test for restart. Are you seeing more fragments than you expect after restart? If so, the original fragments are holding a reference to the old activity, which was stopped, and has a disconnected GoogleApiClient. If the NavBtn of one of those fragments is clicked, you will see the "not connected" toast.
Update
The problem is caused by the way you are creating new instances of NavigationRemoteFragment, specifically the use of static member navigationRemoteFragment. After a restart, when the activity is recreated, the code calls NavigationRemoteFragment.getInstance(this). getInstance() finds navigationRemoteFragment not null because it is static, and does not create a new fragment. The fragment returned is the old one, which holds a reference to the old activity, which has been stopped and has a disconnected GoogleApiClient.
This could be confirmed by using the isDestroyed method and adding a some debug logging:
#Override
public void navigationRemoteSendCommand(String commandPath) {
if (isDestroyed()) {
Log.w("TEST", "This is an old instance of the activity");
}
sendMessage(commandPath, null);
}
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.
Any idea why the list might be empty?
The code is below.
public class PickFBFriendsActivity extends FragmentActivity {
FriendPickerFragment friendPickerFragment;
// A helper to simplify life for callers who want to populate a Bundle with the necessary
// parameters. A more sophisticated Activity might define its own set of parameters; our needs
// are simple, so we just populate what we want to pass to the FriendPickerFragment.
public static void populateParameters(Intent intent, String userId, boolean multiSelect, boolean showTitleBar) {
intent.putExtra(FriendPickerFragment.USER_ID_BUNDLE_KEY, userId);
intent.putExtra(FriendPickerFragment.MULTI_SELECT_BUNDLE_KEY, multiSelect);
intent.putExtra(FriendPickerFragment.SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar);
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.pick_friends_activity);
FragmentManager fm = getSupportFragmentManager();
if (savedInstanceState == null) {
// First time through, we create our fragment programmatically.
final Bundle args = getIntent().getExtras();
friendPickerFragment = new FriendPickerFragment(args);
friendPickerFragment.setUserId(null);
fm.beginTransaction()
.add(R.id.friend_picker_fragment, friendPickerFragment)
.commit();
} else {
// Subsequent times, our fragment is recreated by the framework and already has saved and
// restored its state, so we don't need to specify args again. (In fact, this might be
// incorrect if the fragment was modified programmatically since it was created.)
friendPickerFragment = (FriendPickerFragment) fm.findFragmentById(R.id.friend_picker_fragment);
}
friendPickerFragment.setOnErrorListener(new PickerFragment.OnErrorListener() {
#Override
public void onError(PickerFragment<?> fragment, FacebookException error) {
PickFBFriendsActivity.this.onError(error);
}
});
friendPickerFragment.setOnDoneButtonClickedListener(new PickerFragment.OnDoneButtonClickedListener() {
#Override
public void onDoneButtonClicked(PickerFragment<?> fragment) {
setResult(RESULT_OK, null);
finish();
}
});
}
private void onError(Exception error) {
String text = getString(R.string.exception, error.getMessage());
Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
toast.show();
}
#Override
protected void onStart() {
super.onStart();
}
}
Note that it's pretty much the same as the sample one.
Figured it out: my onStart() method was incomplete, missing the following line:
friendPickerFragment.loadData(false);
Must have deleted it accidently.
I'm trying to use an AsyncTaskLoader to load data in the background to populate a detail view in response to a list item being chosen. I've gotten it mostly working but I'm still having one issue. If I choose a second item in the list and then rotate the device before the load for the first selected item has completed, then the onLoadFinished() call is reporting to the activity being stopped rather than the new activity. This works fine when choosing just a single item and then rotating.
Here is the code I'm using. Activity:
public final class DemoActivity extends Activity
implements NumberListFragment.RowTappedListener,
LoaderManager.LoaderCallbacks<String> {
private static final AtomicInteger activityCounter = new AtomicInteger(0);
private int myActivityId;
private ResultFragment resultFragment;
private Integer selectedNumber;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myActivityId = activityCounter.incrementAndGet();
Log.d("DemoActivity", "onCreate for " + myActivityId);
setContentView(R.layout.demo);
resultFragment = (ResultFragment) getFragmentManager().findFragmentById(R.id.result_fragment);
getLoaderManager().initLoader(0, null, this);
}
#Override
protected void onDestroy() {
super.onDestroy();
Log.d("DemoActivity", "onDestroy for " + myActivityId);
}
#Override
public void onRowTapped(Integer number) {
selectedNumber = number;
resultFragment.setResultText("Fetching details for item " + number + "...");
getLoaderManager().restartLoader(0, null, this);
}
#Override
public Loader<String> onCreateLoader(int id, Bundle args) {
return new ResultLoader(this, selectedNumber);
}
#Override
public void onLoadFinished(Loader<String> loader, String data) {
Log.d("DemoActivity", "onLoadFinished reporting to activity " + myActivityId);
resultFragment.setResultText(data);
}
#Override
public void onLoaderReset(Loader<String> loader) {
}
static final class ResultLoader extends AsyncTaskLoader<String> {
private static final Random random = new Random();
private final Integer number;
private String result;
ResultLoader(Context context, Integer number) {
super(context);
this.number = number;
}
#Override
public String loadInBackground() {
// Simulate expensive Web call
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Item " + number + " - Price: $" + random.nextInt(500) + ".00, Number in stock: " + random.nextInt(10000);
}
#Override
public void deliverResult(String data) {
if (isReset()) {
// An async query came in while the loader is stopped
return;
}
result = data;
if (isStarted()) {
super.deliverResult(data);
}
}
#Override
protected void onStartLoading() {
if (result != null) {
deliverResult(result);
}
// Only do a load if we have a source to load from
if (number != null) {
forceLoad();
}
}
#Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
#Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
result = null;
}
}
}
List fragment:
public final class NumberListFragment extends ListFragment {
interface RowTappedListener {
void onRowTapped(Integer number);
}
private RowTappedListener rowTappedListener;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
rowTappedListener = (RowTappedListener) activity;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(getActivity(),
R.layout.simple_list_item_1,
Arrays.asList(1, 2, 3, 4, 5, 6));
setListAdapter(adapter);
}
#Override
public void onListItemClick(ListView l, View v, int position, long id) {
ArrayAdapter<Integer> adapter = (ArrayAdapter<Integer>) getListAdapter();
rowTappedListener.onRowTapped(adapter.getItem(position));
}
}
Result fragment:
public final class ResultFragment extends Fragment {
private TextView resultLabel;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.result_fragment, container, false);
resultLabel = (TextView) root.findViewById(R.id.result_label);
if (savedInstanceState != null) {
resultLabel.setText(savedInstanceState.getString("labelText", ""));
}
return root;
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("labelText", resultLabel.getText().toString());
}
void setResultText(String resultText) {
resultLabel.setText(resultText);
}
}
I've been able to get this working using plain AsyncTasks but I'm trying to learn more about Loaders since they handle the configuration changes automatically.
EDIT: I think I may have tracked down the issue by looking at the source for LoaderManager. When initLoader is called after the configuration change, the LoaderInfo object has its mCallbacks field updated with the new activity as the implementation of LoaderCallbacks, as I would expect.
public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
LoaderInfo info = mLoaders.get(id);
if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);
if (info == null) {
// Loader doesn't already exist; create.
info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
if (DEBUG) Log.v(TAG, " Created new loader " + info);
} else {
if (DEBUG) Log.v(TAG, " Re-using existing loader " + info);
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
}
if (info.mHaveData && mStarted) {
// If the loader has already generated its data, report it now.
info.callOnLoadFinished(info.mLoader, info.mData);
}
return (Loader<D>)info.mLoader;
}
However, when there is a pending loader, the main LoaderInfo object also has an mPendingLoader field with a reference to a LoaderCallbacks as well, and this object is never updated with the new activity in the mCallbacks field. I would expect to see the code look like this instead:
// This line was already there
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
// This line is not currently there
info.mPendingLoader.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
It appears to be because of this that the pending loader calls onLoadFinished on the old activity instance. If I breakpoint in this method and make the call that I feel is missing using the debugger, everything works as I expect.
The new question is: Have I found a bug, or is this the expected behavior?
In most cases you should just ignore such reports if Activity is already destroyed.
public void onLoadFinished(Loader<String> loader, String data) {
Log.d("DemoActivity", "onLoadFinished reporting to activity " + myActivityId);
if (isDestroyed()) {
Log.i("DemoActivity", "Activity already destroyed, report ignored: " + data);
return;
}
resultFragment.setResultText(data);
}
Also you should insert checking isDestroyed() in any inner classes. Runnable - is the most used case.
For example:
// UI thread
final Handler handler = new Handler();
Executor someExecutorService = ... ;
someExecutorService.execute(new Runnable() {
public void run() {
// some heavy operations
...
// notification to UI thread
handler.post(new Runnable() {
// this runnable can link to 'dead' activity or any outer instance
if (isDestroyed()) {
return;
}
// we are alive
onSomeHeavyOperationFinished();
});
}
});
But in such cases the best way is to avoid passing strong reference on Activity to another thread (AsynkTask, Loader, Executor, etc).
The most reliable solution is here:
// BackgroundExecutor.java
public class BackgroundExecutor {
private static final Executor instance = Executors.newSingleThreadExecutor();
public static void execute(Runnable command) {
instance.execute(command);
}
}
// MyActivity.java
public class MyActivity extends Activity {
// Some callback method from any button you want
public void onSomeButtonClicked() {
// Show toast or progress bar if needed
// Start your heavy operation
BackgroundExecutor.execute(new SomeHeavyOperation(this));
}
public void onSomeHeavyOperationFinished() {
if (isDestroyed()) {
return;
}
// Hide progress bar, update UI
}
}
// SomeHeavyOperation.java
public class SomeHeavyOperation implements Runnable {
private final WeakReference<MyActivity> ref;
public SomeHeavyOperation(MyActivity owner) {
// Unlike inner class we do not store strong reference to Activity here
this.ref = new WeakReference<MyActivity>(owner);
}
public void run() {
// Perform your heavy operation
// ...
// Done!
// It's time to notify Activity
final MyActivity owner = ref.get();
// Already died reference
if (owner == null) return;
// Perform notification in UI thread
owner.runOnUiThread(new Runnable() {
public void run() {
owner.onSomeHeavyOperationFinished();
}
});
}
}
Maybe not best solution but ...
This code restart loader every time, which is bad but only work around that works - if you want to used loader.
Loader l = getLoaderManager().getLoader(MY_LOADER);
if (l != null) {
getLoaderManager().restartLoader(MY_LOADER, null, this);
} else {
getLoaderManager().initLoader(MY_LOADER, null, this);
}
BTW. I am using Cursorloader ...
A possible solution is to start the AsyncTask in a custom singleton object and access the onFinished() result from the singleton within your Activity. Every time you rotate your screen, go onPause() or onResume(), the latest result will be used/accessed. If you still don't have a result in your singleton object, you know it is still busy or that you can relaunch the task.
Another approach is to work with a service bus like Otto, or to work with a Service.
Ok I'm trying to understand this excuse me if I misunderstood anything, but you are losing references to something when the device rotates.
Taking a stab...
would adding
android:configChanges="orientation|keyboardHidden|screenSize"
in your manifest for that activity fix your error? or prevent onLoadFinished() from saying the activity stopped?
I got a VERY STRANGE situation...(to me)
For example, 2 objects,
1 is an activity member boolean called isInPage,
2 is a static bitmap object called bmpPhoto.
When I get into my own activity called FacebookShareActivity
isInPage will be true until I quit this activity,
bmpPhoto will be given a picture.
After onCreare() and onResume(), there is no any code running, until user click some GUI.
What I did is close screen by press hardware power button, and maybe wait 5 or 10 minutes.
OK, now I press porwe again to wake phone up, unlock screen,
and my FacebookShareActivity goes back to front.
And I click my GUI button to check variable value via Logcat, it says:
isInPage=false;
And I forget bmpPhoto's value, but on my GUI, the photo just gone,
not displayed anymore...
How is this happen ?
And it just not happen every time after I do that......
What if I override onSaveInstanceState(Bundle savedInstanceState) and
onRestoreInstanceState(Bundle savedInstanceState) ?
Will it help ?
And what about the bitmap object ?
Still don't know how is that happen...
Did I miss something ?
I really need your help, please everyone~
Following is part of my code, quite long...
The "isPageRunning" and "bmp" changed sometime when back from desktop, but not everytime.
public class FacebookShareActivity extends Activity
{
private Bundle b=null;
private Bitmap bmp=null;
private boolean isFacebookWorking=false;
private boolean isPageRunning=true; //This value sometime changed when back from desktop, but not every time
protected void onCreate(Bundle savedInstanceState)
{
Log.i(Constants.TAG, "ON Facebook Share create......");
super.onCreate(savedInstanceState);
setContentView(R.layout.facebook_share);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
private void initUI()
{
btnBack=(Button)findViewById(R.id.btnBack);
btnBack.setOnClickListener(new ButtonClickHandler());
formImage=(RelativeLayout)findViewById(R.id.form_image);
formImage.setDrawingCacheEnabled(true);
btnShare=(Button)findViewById(R.id.btnShare);
btnShare.setOnClickListener(new ButtonClickHandler());
txtIntroText=(TextView)findViewById(R.id.txtIntroText);
txtIntroText.setOnClickListener(new ButtonClickHandler());
txtIntroText.setText(getUploadInImageText());
photo=(ImageView)findViewById(R.id.photo);
bmp=Constants.PROFILE.getName().getPhoto();
if(bmp!=null)
{photo.setImageBitmap(bmp);} //bmp wouldn't be null, it filled by some other activity before
}
#Override
protected void onResume()
{
super.onResume();
Log.i(Constants.TAG, "Trying to set UI on resume...");
b=getIntent().getExtras();
// ...
// ... Get some String value passed from prev activity
facebook=new Facebook("123456789012345"); //Test
asyncFacebook=new AsyncFacebookRunner(facebook);
initUI();
System.gc();
}
#Override
public void onBackPressed()
{
Log.d(Constants.TAG, "Activity receive back key...");
lockButtons(false);
return;
}
private void lockButtons(boolean b)
{
if(isPageRunning)
{
btnBack.setClickable(!b);
btnShare.setClickable(!b);
}
}
private class DelayReleaseKey implements Runnable
{
public void run()
{
try{Thread.sleep(10000);}
catch(InterruptedException ie){}
handler.sendEmptyMessage(0);
}
}
private class ButtonClickHandler implements OnClickListener
{
public void onClick(View v)
{
if(v==btnBack)
{
if(isFacebookWorking)
{ShowAlertDialog(Constants.MESSAGE_FACEBOOK_WORK);}
else
{
lockButtons(true);
formImage=null;
photo=null;
b=null;
facebook=null;
isPageRunning=false;
Intent intent=new Intent(FacebookShareActivity.this, PracticeListActivity.class);
startActivity(intent);
FacebookShareActivity.this.finish();
overridePendingTransition(android.R.anim.slide_in_left,android.R.anim.slide_out_right);
}
}
if(v==btnShare)
{
lockButtons(true);
facebookLogin();
}
}
}
}
Now I know i must override onSaveInstanceState, onRestoreInstanceState.
They can help me to save variable like String, int, boolean...
What about Bitmap ?
And what if my variable is static ?
Now try again.
public class FacebookShareActivity extends Activity
{
private Bundle b=null;
private static Bitmap bmp=null;
private static boolean isFacebookWorking=false;
private static boolean isPageRunning=true; //This value sometime changed when back from desktop, but not every time
protected void onCreate(Bundle savedInstanceState)
{
Log.i(Constants.TAG, "ON Facebook Share create......");
super.onCreate(savedInstanceState);
setContentView(R.layout.facebook_share);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
private void initUI()
{
btnBack=(Button)findViewById(R.id.btnBack);
btnBack.setOnClickListener(new ButtonClickHandler());
formImage=(RelativeLayout)findViewById(R.id.form_image);
formImage.setDrawingCacheEnabled(true);
btnShare=(Button)findViewById(R.id.btnShare);
btnShare.setOnClickListener(new ButtonClickHandler());
txtIntroText=(TextView)findViewById(R.id.txtIntroText);
txtIntroText.setOnClickListener(new ButtonClickHandler());
txtIntroText.setText(getUploadInImageText());
photo=(ImageView)findViewById(R.id.photo);
bmp=Constants.PROFILE.getName().getPhoto();
if(bmp!=null)
{photo.setImageBitmap(bmp);} //bmp wouldn't be null, it filled by some other activity before
}
#Override
protected void onResume()
{
super.onResume();
isPageRunning = true;
Log.i(Constants.TAG, "Trying to set UI on resume...");
b=getIntent().getExtras();
// ...
// ... Get some String value passed from prev activity
facebook=new Facebook("123456789012345"); //Test
asyncFacebook=new AsyncFacebookRunner(facebook);
initUI();
System.gc();
}
#Override
protected void onPause()
{
isPageRunning = false;
}
#Override
public void onBackPressed()
{
Log.d(Constants.TAG, "Activity receive back key...");
lockButtons(false);
return;
}
private void lockButtons(boolean b)
{
if(isPageRunning)
{
btnBack.setClickable(!b);
btnShare.setClickable(!b);
}
}
private class DelayReleaseKey implements Runnable
{
public void run()
{
try{Thread.sleep(10000);}
catch(InterruptedException ie){}
handler.sendEmptyMessage(0);
}
}
private class ButtonClickHandler implements OnClickListener
{
public void onClick(View v)
{
if(v==btnBack)
{
if(isFacebookWorking)
{ShowAlertDialog(Constants.MESSAGE_FACEBOOK_WORK);}
else
{
lockButtons(true);
formImage=null;
photo=null;
b=null;
facebook=null;
isPageRunning=false;
Intent intent=new Intent(FacebookShareActivity.this, PracticeListActivity.class);
startActivity(intent);
FacebookShareActivity.this.finish();
overridePendingTransition(android.R.anim.slide_in_left,android.R.anim.slide_out_right);
}
}
if(v==btnShare)
{
lockButtons(true);
facebookLogin();
}
}
}
}
For primitive values, you should use onSaveInstanceState. For restoring you can use onRestoreInstanceState or you can some code in onCreate like this:
if(savedInstanceState != null) {
// restore old state
} else {
// a fresh start
}
Now for restoring objects like Bitmap if they are not expensive to create and doesn't make your UI sluggish, create them again on restore. If you do not want to that then use onRetainNonConfigurationInstance and code will look like this:
#Override
public Object onRetainNonConfigurationInstance () {
return bmp;
}
#Override
public void onCreate() {
if
bmp = (Bitmap)getLastNonConfigurationInstance();
}
WARNING: This api is deprecate, you might use it on old platforms. I put it here for illustration purpose. The new way to do this is more involving.
Here is detailed ref:
getLastNonConfigurationInstance
onRetainNonConfigurationInstance