I have an acitivty which looks for user location (MyLocation class), then with the geopoint or without it it runs an AsyncTask to connect to server and get list of cities from my server. When the list is ready it saves them in ArrayList cities. Once the cities ArrayList is filled I would like it to be saved for good (proof to configuration changes). CityItem implements Parcelable. I save them in onSaveInstanceState and retrieve them onCreate.
Now, everything works fine if the task has completed and the cities list is filled. Then I rotate my device back and forth and Log.i("StartActivity", "Cities list downloaded:"+cities.toString()); gets called.
But if I rotate the device before the geopoint was found (or the task finished - hard to tell because it happens fast), then
public void gotCities(ArrayList<CityItem> _cities){
cities = _cities;
Log.i("StartActivity", "gotCities("+cities.size()+"): "+cities.toString());
}
gets called (and cities are perfectly fine in the log) but when I rotate it once more ArrayList cities appears to be null again.
It appears that if the configuration changed and the savedInstanceState.cities was null, the ArrayList cities is somehow created again and it's not the same ArrayList as the one in gotCities() function.
I'm pretty sure it's something easy but I've been searching for answer for hours and I simply can't do it.
Code of the Activity:
public class StartActivity extends Activity {
public static final String PREFS_NAME = "PrefsFile";
MyLocation myLocationObject = null;
LatLngPoint point = null;
ArrayList<CityItem> cities = null;
FindCityTask task = null;
Activity startActivity;
#Override
public void onCreate(Bundle savedInstanceState) {
if(savedInstanceState!=null) if(savedInstanceState.containsKey("cities")) cities = savedInstanceState.getParcelableArrayList("cities"); if(cities!=null) Log.i("Cities retrieved", cities.toString());
super.onCreate(savedInstanceState);
startActivity = this;
setContentView(R.layout.start);
//check if the configuration (orientation) has been changed
NonConfigurationObject nco = (NonConfigurationObject)getLastNonConfigurationInstance();
if(nco!=null) if(nco.myLocationObject!=null) myLocationObject = nco.myLocationObject;
if(nco!=null) if(nco.task!=null) task = nco.task;
if(cities==null){
Log.i("StartActivity", "Cities list is empty - retrieve them.");
if(myLocationObject==null){
getGeopoint();
}
} else {
Log.i("StartActivity", "Cities list downloaded:"+cities.toString());
}
}
private void getGeopoint(){
if(isOnline()){ //there is internet connection
if(myLocationObject==null){
myLocationObject = new MyLocation();
//calls function to check user location (returns false if no providers are enabled
if(!myLocationObject.getLocation(this, locationResult)){ /*TODO handle */Log.i("StartActivity", "Location providers disabled");}
}
} else { //not online - show msg
Log.i("StartActivity", "No internet connection");
}
}
//waits for user geopoint. then starts FindCityTask
LocationResult locationResult = new LocationResult(){
#Override
public void gotLocation(final Location location){
if(location!=null){
// location found
Log.i("StartActivity", "Received location: "+location.toString());
point = new LatLngPoint((float)location.getLatitude(), (float)location.getLongitude());
} else {
// location not found
Log.i("StartActivity", "No location received after 20 seconds");
point = null;
}
//RUN TASK to connect to server to get cities list (even if there's no geopoint)
task = new FindCityTask(startActivity);
task.execute(point);
}
};
public void gotCities(ArrayList<CityItem> _cities){
cities = _cities;
Log.i("StartActivity", "gotCities("+cities.size()+"): "+cities.toString());
}
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
Log.i("onSaveInstanceState", "onSaveInstanceState");
if(cities!=null) savedInstanceState.putParcelableArrayList("cities", cities);
}
#Override
public NonConfigurationObject onRetainNonConfigurationInstance() {
NonConfigurationObject nco = new NonConfigurationObject();
if(myLocationObject!=null){
nco.myLocationObject = myLocationObject;
}
if(task!=null){
nco.task = task;
}
return nco;
}
static class NonConfigurationObject{
MyLocation myLocationObject;
FindCityTask task;
}
gotCities() method is called from AsyncTask onPostExecute:
#Override
protected void onPostExecute(Void result) {
if(this.activity!=null){
((StartActivity) activity).gotCities(cities);
}
}
When orientation cnange hits, your activity is stopped, destroyed and created anew, and the only callback which is guaranted to be called is onPause(). The same happens when screen lock kick in - it forces your activity in portrait mode.
I recommend you to read about android activity lifecycle. Rule of thumb is:
onCreate() is for initalizing interface objects and services
onResume() is called when your activity comes on top and is about to be presented to user
onPause() when it loses focus and is not presented to use anymore.
Since obtaining location takes long time, it is better to move it away from activity to background service, ( start it in onCreate() if necessary ) and decouple its lifecycle from activity. Service can pass results to activity via broadcast messages, or java method calls
And look into your onSaveInstanceState() - first you are calling super.onSaveInstanceState(), and then you modify bundle to include your data. This way they are never saved.
I finally got it. It's the AsyncTask which pointed to wrong (the one before configuration change) activity. The trick is to attach to the task the reference to activity (and to do it in the right moment).
So the first thing to do is to put these functions in the AsyncTask:
void attach(Activity activity){
this.activity = activity;
}
void detach(){
this.activity = null;
}
When the task is first called we should attach the activity to it.
Then onRetainNonConfigurationInstance() detach it.
#Override
public NonConfigurationObject onRetainNonConfigurationInstance() {
//normally it would return only the task, but i have to return another object
//hence the NonConfigurationObject which holds reference to both the AsyncTask and MyLocation
//(as seen in the original question)
NonConfigurationObject nco = new NonConfigurationObject();
if(task!=null){
task.detach();
nco.task = task;
}
return nco;
}
And finally attach it again when we call getLastNonConfigurationInstance() in onCreate().
//check if the configuration (orientation) has been changed
NonConfigurationObject nco = (NonConfigurationObject)getLastNonConfigurationInstance();
if(nco!=null){ //not created for the first time
Log.i("StartActivity", "NCO: "+nco.toString());
task = nco.task;
if(task!=null){ //nco can be present but task still null
task.attach(this);
} else {
task = new FindCityTask(this);
}
} else {
Log.i("StartActivity", "NCO: null");
task = new FindCityTask(this);
}
Please share your thoughts on this solution if you have any. I'll modify the question so it better suits the problem.
Related
I want to establish a good understanding of the AsyncTaskLoader lifecycle. I checked several resources, everything is clear but the usage of deliverResult. According to this picture from the internet (available here):
onStartLoading will be called, then if there is data already loaded, deliverResult is called, then it deliver the result to onLoadFinished. However, if there is no data foceLoad will be called, then loadInBackground, then deliverResult, then onLoadFinished.
I did the same way croworc answer suggests here: What does AsyncTaskLoader.deliverResult() actually do?
This is the code:
public class WeatherLoader extends AsyncTaskLoader<List<Weather>> {
List <Weather> receivedData;
/** Tag for log messages */
private String mUrl;
public WeatherLoader(Context context, String url) {
super(context);
mUrl = url;
}
#Override
protected void onStartLoading() {
if (receivedData == null){
Log.i ("loader ", "No data yet");
forceLoad();
} else {
deliverResult(receivedData);
Log.i ("loader ", "data is available no reload");
}
}
#Override
public void deliverResult(List<Weather> data) {
receivedData = data;
super.deliverResult(data);
Log.i ("loader ", "deliver result");
}
#Override
public List<Weather> loadInBackground() {
Log.i ("loader ", "load in background");
if (mUrl == null) {
return null;
}
// Perform the network request, parse the response, and extract a list of earthquakes.
List<Weather> weather = getweatherData(mUrl);
return weather;
}
}
But this is the sequence of the callbacks I'm getting when I initialize the loader or restart it:
onCreatLoader
No data yet
load in background
onLoaderFinish
deliver result
What really confuses me is that deliverResult is called after onLoaderFinished which also I think contradicts with this page of this book:
available here
The check for the availability of the data used in onStartLoading which calls deliverResult only gets called when the activity is stopped and restarted, like if I navigate to another activity then get back to it. Here is what gets printed in the logcat in this case:
deliver result
data is available no reload
Even onLoadFinished doesn't get called in this case. However, if I do the same behavior of navigating to another activity and getting back to the first one with having deliverResult with its original behavior (where I only call the super version of it), onStart gets called, then loadInBackground, then onLoadFinished, then DeliverResult. So, a new load happens
Can anyone please clarify why this behavior of callbacks is taking place? Does this mean that the image that shows the lifecycle is inaccurate?
Thanks.
Put the log calls before calling super and check the flow sequence again.
onLoadFinished is called during the call to super.deliverResult.
#Override
public void deliverResult(List<Weather> data) {
Log.i ("loader ", "deliver result");
receivedData = data;
super.deliverResult(data);
}
Currently I am trying to collect location updates periodically, say for every 10 samples. I am using an arraylist to collect them and clear off the arraylist in the main UI thread once I passed them to the server using async task. In the async task I am loading the arraylist with the one from main UI.
The problem is, it is clearing the arraylist in the async task even it is in separate variable. How can I keep the activity in sync. Do I need to sleep the main activity till the async task finishes. I am not sure about the variable. Can someone explain how to do this?
MainMapActivity(X){
locationupdate for every 1 min{
arraylist a;//this collects all location updates 10 samples each time
call asynctask b;
clear a;
}
asynctask b{
arraylist c = getall from a;
db= insert(c);//save a into database;
}
Clearing a in main UI clears of variable c. How can I prevent that? The variable c should be only cleared after saving all the data in it.
If I'm getting what you are trying to say is,then yes, we have a way to fix your problem using handlers.
In your async task, do something like this -
private mLocations;
public MyTask(Handler mResponseHandler, List<Location> mLocations){
super();
this.mLocations = mLocations;
this.mResponseHandler = mResponseHandler;
}
in onPostExecute,
onPostExecute(List<Location>){
#Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
Log.i(TAG, "result = "+result);
if (mResponseHandler == null) return;
MyLocationData<Location> resultData = new MyLocationData<Location>();
if(result != null && result){
resultData.requestSuccessful = true;
resultData.responseErrorCode = 0;
}else{
resultData.requestSuccessful = false;
resultData.responseErrorCode = errorCode; //set this when result is null
}
android.os.Message m = android.os.Message.obtain();
m.obj = resultData;
mResponseHandler.sendMessage(m);
}
}
Where MyLocationData is a model class in which I'm saving all relevant data. It can be a class like this -
public class MyLocationData<Type> {
public Type response;
public int responseErrorCode;
public boolean requestSuccessful;
}
Now, in your activity, you can get this data like,
private Handler mExportHandler = new Handler(){
public void handleMessage(android.os.Message msg) {
MyLocationData<Location> responseData = (MyLocationData<Location>) msg.obj;
// your logic for fetching new locations from responseData and using them in your activity
};
};
I have an application that uses IntentService to run a background task where I pull data from a website, parse the data out, and create calendar events based on the results. Everything seems to be working create, except I'm running into an issue with rotation.
Using the code below, when I rotate the screen, the ProgressDialog box stays visible, but is never updated with new text when the process is updated, and never goes away once the call is completed. I'm using an IntentService instead of an ASyncTask because the user can also schedule the IntentService to run at other times without having to interface with the app. Any help is appreciated, thanks!
public void onCreate(Bundle savedInstanceState) {
Object retained = getLastNonConfigurationInstance();
if (retained instanceof CalendarHandler) {
// CH is a class level variable defined at the top which references my IntentService, aptly named CalendarHandler
ch = (CalendarHandler) retained;
ch.setActivity(this);
} else {
ch = null;
}
activity = this;
btnLogin.setOnClickListener(OnClickListener(View view) {
ch = new CalendarHandler();
ch.setActivity(MyTlc.this);
// Do other stuff, like run the intent service
}
}
public Handler handler = new Handler() {
public void handleMessage(Message message) {
// We read the information from the message and do something with it
// based on what the result code is
String result = message.getData().getString("status");
if (result.equals("ERROR")) {
activity.removeDialog(PROGRESS_DIALOG);
results.setText(message.getData().getString("error"));
} else if (result.equals("DONE")) {
activity.removeDialog(PROGRESS_DIALOG);
int count = message.getData().getInt("count", 0);
activity.results.setText("Added " + count + " shifts to the calendar");
} else {
activity.pDialog.setMessage(result);
}
super.handleMessage(message);
}
};
From what I understand, this should work, and like I said the ProgressDialog box does stay properly, I just can't seem to pass information to the dialog box after rotating.
Iv'e got an Android app that is using a list activity to display a list of items pulled from the internet. I First use an AsyncTask to load the list and that Async task finishes it calls a different async task to start loading the thumbnail pictures that go along with the list items. The problem I am having is that the user has access to a refresh button that they can press at any time and when it is pressed, the whole list of items is delete and the loading starts over. The Async task that loads the thumbnails could potentially still be running if this happens and may try to add a thumbnail to a non existing list item then. Iv'e tried synchronizing on the list, using a Boolean which after researching I realized would not work. I have also tried using a static atomic boolean to check if refresh has been hit to cancel the thumbnail loader. Any ideas?
public class LoadItems extends AsyncTask<Void, Void, Boolean> {
private Activity activity;
private static boolean loading = false;
public static final AtomicBoolean refreshing = new AtomicBoolean(false);
private static final String TAG = "LoadItems";
private int start;
private List<ListItem> items;
public LoadItems(Activity activity) {
this.activity = activity;
}
#Override
protected void onPreExecute() {
loading = true;
start = ItemViewer.itemList.size();
}
#Override
protected Boolean doInBackground(Void... arg0) {
items = WebFunctions.getMoreItems(activity);
return (items != null);
}
protected void onPostExecute(Boolean success) {
if (success) {
for (ListItem item: items) {
ItemViewer.itemList.add(item);
Log.d(TAG, "added item " + item.getTitle());
}
LoadThumbnails thumbnailLoader = new LoadThumbnails();
thumbnailLoader.execute(start, ItemViewer.itemList.size());
}
loading = false;
}
public void protectedExecute() {
if (!loading)
execute();
}
public void refresh() {
if (!refreshing.getAndSet(true)) {
WebFunctions.reset();
ItemViewer.itemList.removeAllItems();
execute();
}
}
}
public class LoadThumbnails extends AsyncTask<Integer, Void, Drawable> {
private int position;
private int end;
#Override
protected Drawable doInBackground(Integer... params) {
position = params[0];
end = params[1];
Drawable thumbnail = null;
synchronized(ItemViewer.itemList) {
if (LoadItems.refreshing.get())
cancel(true);
String url = ItemViewer.itemList.get(position).getThumbnailUrl();
if (!url.isEmpty())
thumbnail = WebFunctions.loadDrawableFromUrl(ItemViewer.activity, url);
}
return thumbnail;
}
protected void onPostExecute(Drawable d) {
synchronized (ItemViewer.itemList) {
if (LoadItems.refreshing.get())
cancel(true);
if (d != null)
ItemViewer.itemList.setThumbnail(position, d);
position++;
if (position < end) {
LoadThumbnails lt = new LoadThumbnails();
lt.execute(position, end);
}
}
}
}
This is pretty simple to solve. Whenever the user hits the refresh button, make sure you call cancel() on the last async tasks you have created before you create new tasks. For example,
private void onRefreshClick(View v) {
if(mLastLoadItemTask != null) mLastLoadItemTask.cancel(true);
if(mLastLoadThumbnailTask != null) mLastLoadThumbnailTask.cancel(true);
mLastLoadItemTask = new LoadItems(...);
mLastLoadItemTask.execute();
}
Then, in the onPostExecute of each of your async tasks, first check to see if they were cancelled by calling isCancelled(). If they were cancelled, make sure the onPostExecute method does no work by just returning. For example,
protected void onPostExecute(...) {
if(isCancelled()) return;
//Adding items to list
//Or start load thumbnail task
}
As you can see that should prevent any unintentional or stale updates because the onPostExecute methods and your cancel calls will all happen on the main therad. The last thing I would suggest is to alter your loadThumbs task to be able to stop doing work as soon as possibly by checking isCancelled() whenever it makes sense to do so.
The following steps might help:
cache the results, whatever you have previously pulled from the net should be saved and quickly restored back when your application is launched. this way you avoid long delays and empty screens on application startup, which, in turn, stops the user from pressing 'reload'
make a boolean variable reload_in_progress, set it to true when you start pulling data from the net, and set it to false when all thumbnails are ready. 'reload' click handler should ignore clicks when reload_in_progress is true.
show some king of progress bar to the user, so (s)he knows it's already reloading and does not push reload again.
almost forgot, never update data shown to the user "live", this leads to wonderful situations, when the user clicks on item while it's changing and doing something completely different from what (s)he expected. long updates should keep its data to themselves and quickly swap old data for the new one only when everything is ready.
I'm trying to make my app wait till the current location is found. I've tried few different ways using Threads and all have failed really. I was using wait() and notify() but application just hung and never found the current location.
I amen't using google map api as it is not part of the application. Does anyone have any ideas how to do this or examples.
EDIT : The Thread I was using did not start till the user pressed a button then within onLocationChanged other data is processed e.g. adding the new location to ArrayList, Calculate the distance between the current and last Location as well as the Time taken to get to the new location
You could try starting an AsyncTask in onCreateto get the location. Your default onCreate layout could be a "loading" page, then when your AsyncTask completes successfully with the location it draws your "real" UI.
So if I understand what you want to do correctly, then I would avoid making another thread in onClick(). Instead, onClick() should just request a location, display a progress dialog, and return. Since the work you want to do happens after you receive the new location, I would start an AsyncTask there. Then you finally remove the dialog box (removing it returns control to the user) when the AsyncTask finishes.
Code usually helps, so, I would put this in onCreate() or wherever:
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
listener.refresh();
}
});
And put this in your LocationListener:
public void refresh() {
myLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
myDialog = new ProgressDialog(myContext);
myDialog.setIndeterminate(true);
myDialog.show();
}
#Override
public void onLocationChanged(Location location) {
// now do work with your location,
// which your probably want to do in a different thread
new MyAsyncTask().execute(new Location[] { location });
}
And then you need an AsyncTask, which may look like this:
class MyAsyncTask extends AsyncTask<Location, Void, Void> {
#Override
protected Void doInBackground(Location... location) {
// start doing your distance/directions/etc work here
return null;
}
#Override
protected void onPostExecute(Void v) {
// this gets called automatically when you're done,
// so release the dialog box
myDialog.dismiss();
myDialog = null;
}
}