In my application, after enough clicking around, I get this error:
06-08 19:47:59.967: ERROR/AndroidRuntime(2429): java.lang.RuntimeException: Unable to pause activity {com.MYAPP.app/com.MYAPP.app.MainActivity}: android.database.StaleDataException: Access closed cursor
What I have is a Tab Activity (my MainActivity), which has a ListActivity as the contents for each tab. Inside the onCreate for each ListActivity I get a cursor that represents the data to be displayed in that list.
The onListItemClick for each list also creates another activity, so clicking on an item in the list will show more information about that item in a new screen. It's inconsistent, but after enough clicking into these new activities, or going back to the ListView from a new activity, the program crashes.
In searching around for a solution to my problem, I did stumble upon registerDataSetObserver, but it doesn't seem to be the whole answer. I am also having trouble finding documentation on it, so I'm not sure I fully understand it. I have a custom ListAdapter that both my ListViews use and have called registerDataSetObservers on the cursors there.
I have attached the relevant code from one of my ListActivities and from my custom ListAdapter class.
The ListActivity. I have two of these, almost identical, except they both have different cursors created from different database queries:
import com.MYAPP.app.listmanager.DeviceListAdapter;
public class AllSensorsActivity extends ListActivity{
private DeviceListAdapter AllList;
private DbManager db;
protected Cursor AllCur;
protected Cursor AllSensors;
private static final String TAG = "AllSensorsActivity";
#Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Log.e(TAG, "Calling All onCreate");
db = new DbManager(this);
db.open();
AllCur = db.fetchAllDevices();
startManagingCursor(AllCur);
AllSensors = db.fetchAllSensors();
startManagingCursor(AllSensors);
AllList = new DeviceListAdapter(this, AllCur, AllSensors);
setListAdapter(AllList);
}
#Override
protected void onListItemClick(ListView l, View v, int position, long id){
String device_name = (String) ((DeviceListAdapter)getListAdapter()).getItem(position);
String sensor_string = ((DeviceListAdapter)getListAdapter()).getSensors(id);
Intent i = new Intent(this, SensorActivity.class);
Bundle bundle = new Bundle();
bundle.putString("NAME", device_name);
i.putExtras(bundle);
bundle.putString("SENSORS", sensor_string);
i.putExtras(bundle);
this.startActivity(i);
}
The custom ListAdapter:
public class DeviceListAdapter extends BaseAdapter {
private static final String TAG = "DeviceListAdapter";
private Context mContext;
private Cursor mSensors;
private Cursor mDevices;
protected MyDataSetObserver sensors_observer;
protected MyDataSetObserver devices_observer;
public DeviceListAdapter(Context context, Cursor devices, Cursor sensors){
mContext = context;
mDevices = devices;
mSensors = sensors;
sensors_observer = new MyDataSetObserver();
mSensors.registerDataSetObserver(sensors_observer);
devices_observer = new MyDataSetObserver();
mDevices.registerDataSetObserver(devices_observer);
}
// ... more functions and stuff that are not relevant go down here...
}
private class MyDataSetObserver extends DataSetObserver {
public void onChanged(){
Log.e(TAG, "CHANGED CURSOR!");
}
public void onInvalidated(){
Log.e(TAG, "INVALIDATED CURSOR!");
}
}
Should I just have MyDataSetObserver catch the exception and move on? I'd like a more robust solution than that if possible. Or is there some other way I could rearrange my program so that the staleDataException doesn't occur (as often)? I believe that it is happening because I am launching the new activity in my onListItemClick.
I believe when it's invalidated you want to call requery() on the cursor. You could possibly do that in onInvalidated().
Related
I have a listview of items in my ShoppingListActivity.
Items are added from another activity thought an intent. I want to make sure that all items are kept in the list when going between both activities; however, right now my list only has the last item added from the previous activity.
My ShoppingListActivity.class
public class ShoppingListActivity extends Activity {
private ListView mainListView ;
private ArrayAdapter<String> listAdapter ;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_shopping_list);
// Find the ListView resource.
mainListView = (ListView) findViewById( R.id.mainListView );
ArrayList<String> shoppingList = new ArrayList<String>();
shoppingList.add(itemLookup());
// Create ArrayAdapter using the shopping list.
listAdapter = new ArrayAdapter<String>(this, R.layout.simplerow, shoppingList);
// Set the ArrayAdapter as the ListView's adapter.
mainListView.setAdapter( listAdapter );
}
//Lookup item by ID
public String itemLookup() {
String itemName = "";
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (intent != null) {
String itemId = extras.getString("BARCODE_ID");
try {
itemName = ItemLookup.lookupById(itemId);
} catch (IOException e) {
e.printStackTrace();
}
}
return itemName;
}
#Override
public void onBackPressed() {
startActivity(new Intent(ShoppingListActivity.this, MainActivity.class));
}
}
I have a feeling I should be putting my add somewhere else. I'm pretty sure I should be passing the list back and forth in a putExtra, but if that's how I have to do it, it's fine.
How can I make sure that the list is maintained between activities?
One way around your problem is Singleton Pattern.
In your case you can implement something like this:
public class ShoppingListManager {
private static ShoppingListManager instance = new ShoppingListManager();
private List<String> shoppingList;
public static ShoppingListManager getInstance() {
return instance;
}
public List<String> getShoppingList() {
return shoppingList;
}
// Make the constructor private so that this class cannot be instantiated
private ShoppingListManager(){
shoppingList = new ArrayList<String>();
}
}
Then access it anywhere in your code.
ShoppingListManager.getInstance().getShoppingList();
One point to remember never store context in singleton classes as it will lead to memory leaks.
Keeping your data structures in an Activity makes your app prone to data loss because Activities can be destroyed at various times and for a variety of reasons, including rotating the device between portrait and landscape.
You should use a separate class to store and track which items are in the shopping list. The Activity with the ListView should only get the list of items stored and display them. Anything that causes an item to be added should simply trigger a reload of the list (if the Activity is running in the foreground), otherwise the Activity should see that new item anyway the next time it starts.
If you also need your data to be persistent after your process is terminated, you should look into the possible data storage options available.
I have an activity that grabs data via WebService, from there it creates elements to display the data. Some data is grouped so my solution was to display the grouped data in their own fragments below the main layout, allowing the user to swipe across the groups, probably with a tabs at the top to show the group name.
The problem I came across was that the fragments in the activity are created before that web call takes place, making them empty or using old data. I then created a sharedpreferences listener and placed the fragments layout creation method within it. The main method grabs the data, writes to sharedpreferences the fragment detects the change and creates it's layout, Or so I thought.
Some groups are the same between items, so moving from one to the other won't trigger that onchange event thus not triggering the layout creation method. I then decided to do the following to always trigger the onchange event after the sharedpreferences are written
final Boolean updated = settings.getBoolean("UPDATED_1", false);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("UPDATED_" + pageNum, !updated);
I just don't think that's the best solution, it also has it's problems and isn't triggering every time (Which I have yet to troubleshoot)
What's a better solution for all this? I also have a memory leak I haven't diagnosed yet to make things even more of a headache.
I've just thought of moving my data grabbing method to before the ViewPager initialization but I'm not yet sure if this will solve my problem.
I would not recommend waiting until you get the data to show the view as it will affect the User Experience and look sluggish.
Instead, you could implement an AsyncTaskLoader in your fragment so you can inform the Fragment's View with a BroadcastReceiver once you get the data from your server. In the meantime, just show a spinner until the data are retrieved, then you hide it and update your list with a adapter.notifyDataSetChanged();.
Here is an example of a AsyncTaskLoader (In my case it's a database query instead of a server call like you):
public class GenericLoader<T extends Comparable<T>> extends AsyncTaskLoader<ArrayList<T>> {
private Class clazz;
public GenericLoader(Context context, Class<T> clazz) {
super(context);
this.clazz = clazz;
}
#Override
public ArrayList<T> loadInBackground() {
ArrayList<T> data = new ArrayList<>();
data.addAll(GenericDAO.getInstance(clazz).queryForAll());
Collections.sort(data);
return data;
}
}
Then in your Fragment:
public class FragmentMobileData extends Fragment implements ListAdapter.OnItemClickListener, LoaderManager.LoaderCallbacks<ArrayList<EntityCategories.EntityCategory>> {
public static String TAG = "FragmentMobileData";
private ImageListAdapter adapter;
private ArrayList<EntityList> mCategories = new ArrayList<>();
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
Bundle bundle = intent.getExtras();
String result = bundle.getString(DatabaseService.RESULT);
if (DatabaseService.NO_CONNECTION.equals(result)) {
Utils.showToastMessage(getActivity(), "No internet connexion", true);
} else if (DatabaseService.RESULT_TIMEOUT.equals(result)) {
Utils.showToastMessage(getActivity(), "Bad connection. Retry", true);
}
getActivity().getSupportLoaderManager().initLoader(1, null, FragmentMobileData.this).forceLoad();
}
};
#Bind(R.id.progressBarEcard)
ProgressBar spinner;
#Bind(R.id.list)
RecyclerView list;
public FragmentMobileData() {
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_mobile_plan, container, false);
ButterKnife.bind(this, view);
((AppCompatActivity) getActivity()).getSupportActionBar().setTitle("Mobile");
list.setLayoutManager(new LinearLayoutManager(context));
list.addItemDecoration(new DividerItemDecoration(context, R.drawable.divider));
adapter = new ImageListAdapter(mCategories, this);
list.setAdapter(adapter);
Intent intent = new Intent(context, DatabaseService.class);
intent.setAction(DatabaseService.UPDATE_DATA);
getActivity().startService(intent);
return view;
}
#Override
public void onPause() {
super.onPause();
getActivity().unregisterReceiver(mReceiver);
}
#Override
public void onResume() {
super.onResume();
getActivity().registerReceiver(mReceiver, new IntentFilter(DatabaseService.UPDATE_DATA));
}
#Override
public Loader<ArrayList<EntityCategories.EntityCategory>> onCreateLoader(int id, Bundle args) {
return new GenericLoader(context, EntityCategories.EntityCategory.class);
}
#Override
public void onLoadFinished(Loader<ArrayList<EntityCategories.EntityCategory>> loader, ArrayList<EntityCategories.EntityCategory> data) {
if (mCategories.size() != data.size()) {
mCategories.clear();
mCategories.addAll(data);
adapter.notifyDataSetChanged();
Intent intent = new Intent(context, DownloadFilesService.class);
context.startService(intent);
}
spinner.setVisibility(View.GONE);
}
#Override
public void onLoaderReset(Loader<ArrayList<EntityCategories.EntityCategory>> loader) {
mCategories.clear();
adapter.notifyDataSetChanged();
}
//...
}
Maybe I misunderstood something. But in your case I think there is pretty good alternative to create, for example, your fragment which will display some group of data, then in it's creation stage show progress bar in ui, and meantime do request to the data in background. Then handle result data and show it, and hide progress bar.
This can be achieved with implementing MVP pattern to provide flexibility of code and easy testing. Also you can use rxJava and Retrofit to handle requests in a convenient way. More information about MVP and samples you can find here.
If you don't want to provide this way for some reason. For example, you have undetermined number of groups, which you will receive in future somehow and you want to dynamically build your fragments base on data which you receive, then I suggest you can organize presentation layer in your activity. In this layer your will receive data then pass it to special handler, which will divide it to groups and base on them will ask activity to create fragment. In constructor you will send already received data (so it is need to implement Parcelable interface).
I am developing a Quizz App in which an activity shows question and options from SQLite and on selecting option, another activity is showing result for 2000 ms(it has a timer)and then it calls First Activity via an Intent.
So, Most of the interaction is between 2 activities. But each time my MainActivity is called, it re-initializes all the variables again and again.
I am opening my database connection in onCreate() and also keeping a counter (that can count how many questions have been asked yet) whose value is not retained after the intent from Second Activity. I am worried on how to solve this.
I am a bit confused about the life cycle that is followed. Whether the call to First Activity from Second one starting with onCreate() or it's also initializing the instance variables again.
This is onCreate() method I wrote:
public class MainActivity extends Activity {
protected static final int SCORE_INCREMENT = 5;
TextView question;
Button score, opt1, opt2, opt3;
MyDatabaseManager dbManager;
QuizManager quizManager;
private int quiz_counter =1;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dbManager = new MyDatabaseManager(getApplicationContext());
dbManager.open();
quizManager = new QuizManager(MainActivity.this, dbManager);
Toast.makeText(MainActivity.this, "Asking The First Question", 0).show();
askQuestion();
}
}
Is there any difference between the above written code and the one I am writing now... if the activity is called again via an Intent
public class MainActivity extends Activity {
protected static final int SCORE_INCREMENT = 5;
TextView question;
Button score, opt1, opt2, opt3;
MyDatabaseManager dbManager = new MyDatabaseManager(getApplicationContext());
QuizManager quizManager = new QuizManager(this, dbManager);
private int quiz_counter =1;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(MainActivity.this, "Asking The First Question", 0).show();
askQuestion();
}
}
This might be a silly question. But it's a bit confusing for me. Suggestions are welcome.
If you have variables that you want to maintain between changing activities, then you should either
Store them in SharedPreferences
or
Pass them between the Activites in the Intents (see Starting another activity)
My app consists of a number of activities, upto now I have had no problems with accessing the database when moving between the activities. However on the last listActivity (LocationActivity), I have an embedded button on each listView item.
When one of these buttons are clicked, it sends you to SpecificationEdit.java where the user inputs the specfication into some EditText fields for that listView item (a damaged component), but when you click Save it crashes with the following error message (note the data is saved to database ok):
java.lang.RuntimeException: Unable to resume activity blah blah
Exception: trying to requery an already closed cursor blah blah
Here is the listActivity class:
public class LocationActivity extends ListActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_location);
setLongClick();
rmDbHelper = new RMDbAdapter(this);
rmDbHelper.open();
getIntents();
setUpViews();
setAdapter();
setTextChangedListeners();
}
protected void onResume(){
super.onResume();
final Cursor locationCursor = (Cursor) rmDbHelper.fetchLocationsForRun(runId);
startManagingCursor(locationCursor);
locationCursorSize = locationCursor.getCount();
setAdapter();
setTextChangedListeners();
}
And here is the bit in this activity where is sends you to SpecificationEdit.java
private void startComponentEdit() {
Intent i = new Intent(LocationActivity.this, SpecificationEdit.class);
i.putExtra("Intent_InspectionID", inspectionId);
i.putExtra("Intent_AreaID", areaId);
i.putExtra("Intent_RunID", runId);
i.putExtra("Intent_LocationID", locationId);
i.putExtra("Intent_Ref", locationRef);
i.putExtra("Intent_DamagedComponentID", damagedComponentId);
startActivityForResult(i, ACTIVITY_CREATE);
}
And here is the OnCreate in SpecificationEdit.java:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
rmDbHelper = new RMDbAdapter(this);
rmDbHelper.open();
Intent i = getIntent();
inspectionId = i.getLongExtra("Intent_InspectionID", -1);
areaId = i.getLongExtra("Intent_AreaID", -1);
runId = i.getLongExtra("Intent_RunID", -1);
locationId = i.getLongExtra("Intent_LocationID", -1);
damagedComponentId = i.getLongExtra("Intent_DamagedComponentID", -1);
setContentView(R.layout.edit_specification);
setUpViews();
populateFields();
fillSpinner();
setListeners();
}
With the bit of code which fires when you click the save button:
protected void saveDamagedComponentSpec() {
String manufacturer = ((Cursor)manufacturerSpinner.getSelectedItem()).getString(1).toString();
String text1 = specEditText1.getText().toString();
String text2 = specEditText2.getText().toString();
String text3 = specEditText3.getText().toString();
String text4 = specEditText4.getText().toString();
String notes_spec = specEditTextNotes.getText().toString();
rmDbHelper.saveDamagedComponentSpec(damagedComponentId, manufacturer, text1, text2, text3, text4, notes_spec);
if ("Yes".equals(specSaved)){
Toast.makeText(getApplicationContext(), "Component specification updated",
Toast.LENGTH_SHORT).show();
}
else {
Toast.makeText(getApplicationContext(), "Component specification added",
Toast.LENGTH_SHORT).show();
}
finish();
}
Finally, here is the code in my database helper class:
//Constructor - takes the context to allow the database to be opened/created
public RMDbAdapter(Context ctx) {
this.mCtx = ctx;
}
/**
* Open the rm database. If it cannot be opened, try to create a new
* instance of the database. If it cannot be created, throw an exception to
* signal the failure
*
* #return this (self reference, allowing this to be chained in an
* Initialisation call)
* #throws SQLException if the database could be neither opened or created
*/
public RMDbAdapter open() throws SQLException {
rmDbHelper = new DatabaseHelper(mCtx);
rmDb = rmDbHelper.getWritableDatabase();
return this;
}
public void close() {
rmDbHelper.close();
}
The weird thing is, you can click on one of the listView item (the actual item not the embedded item) or the button 'add new component' and this will send you to another activity ComponentEdit.java with very similar interface (to add a component to the list) as SpecificationEdit but which when it finishes doesn't crash the app.
I can't see any major difference between the two activities, yet one is crashing with this error when you return to LocationActivity and one is not.
I have just tried removing onResume and this made no difference.. Hit a brick wall with this and it's driving me nuts.
I should add that it is working ok on my emulator, but crashes when I test it on my phone (HTC One S). Very strange..
Don't forget to call rmDbHelper.close(); before start another activity
Right, found the issue (spot the obvious mistake):
Cursor componentsCursor = (Cursor) rmDbHelper.fetchDamagedComponentSpecForInspection(inspectionId, componentType);
startManagingCursor(componentsCursor);
Intent i = new Intent(this, SpecificationEdit.class);
i.putExtra("Intent_InspectionID", inspectionId);
i.putExtra("Intent_AreaID", areaId);
i.putExtra("Intent_RunID", runId);
i.putExtra("Intent_LocationID", locationId);
i.putExtra("Intent_Ref", locationRef);
i.putExtra("Intent_DamagedComponentID", damagedComponentId);
startActivityForResult(i, ACTIVITY_CREATE);
componentsCursor.close();
So it wasn't this obvious (I had some blocked out code left in before the componentsCursor.close()), but when I finished SpecifcationEdit.class, I guess it returns to this activity and tries to close the componentsCursor but obviously fails.
Stupid thing is, I hadn't actually got this cursor doing anything yet! Doh!
Just for some additional advice/whittering; my app is fundimentally different to the Google notePad example, as I don't actually use the startActivityForResult as they do (in fact now I understand it better I will replace these all with just startActivity) as I input the data into the database while still on the edit activities, there and then (rather than passing this information through an intent then addinig when you return to the previous activity.
I find this more logical in the realms of my code, but any feedback on this approach?
I have an activity group and it starts 2 activities. When the user presses a button on one of the activities, the activity group populates an ArrayList.
I am wondering if there is a way to allow both of my activities to access this ArrayList.
Here's what I have at the moment:
public class ExampleGroup extends ActivityGroup {
public static ExampleGroup group;
ArrayList<String> strs = new ArrayList<String>();
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
group = this;
View exampleView = getLocalActivityManager().startActivity(
"Example",
new Intent(this, Example.class).addFlags(
Intent.FLAG_ACTIVITY_CLEAR_TOP))
.getDecorView();
setContentView(exampleView);
}
public void populateArrayList(){
//code to do it
}
}
public class Example extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
ExampleGroup.group.populateArrayList();
ArrayList<String> strs2 = ExampleGroup.group.strs;
Log.i("ArrayList contents", strs2);
}
}
The arraylist returns null. Is there something I am missing, or is there a better way to do it?
Yes essentially you're wanting to share a model object between two activities, and this has much to do with the structure of your program. See this post for more details on how that can be done:
Where should I put global methods and variables in an Android app?