Refresh ListView (with BaseAdapter) after SQLiteDatabase data changed - android

Firstly, I want to apologize for asking this question, because I know that there are a lot similar ones here, but none of the answers solve my problem.
As the title of the question suggests I need to populate ListView with new items when data in SQLiteDatabase changes.
To be more specific...
I have an activity that shows contacts in a ListView. At the bottom of the screen I have a button that adds a contact (A Dialog pops up with fields for name, phone number etc...).
When an item in the list is clicked another Dialog is opened. In that dialog there are 3 buttons for SendSMS (to the selected contact), Edit and Delete contact.
When I fill in the form for adding new contact, or click the Delete button, I want the ListView to refresh.
It doesn't happen. In order to see the updated list I need to navigate back, and start the Contacts activity again.
Here is the code:
Activity:
public class ContactsActivity extends Activity {
private MyUtilities myUtilities;
private MyDatabaseHelper mdbh;
private AdapterContactListView contactsAdapter;
private ListView contactsListView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contacts);
mdbh = new MyDatabaseHelper(this);
myUtilities = new MyUtilities(this);
contactsAdapter = new AdapterContactListView(this,mdbh);
contactsListView = (ListView)findViewById(R.id.contactActivityLV);
contactsListView.setAdapter(contactsAdapter);
contactsAdapter.notifyDataSetChanged();
contactsListView.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// TODO Auto-generated method stub
TextView phoneNumberTV = (TextView)view.findViewById(R.id.contactElementNumberTV);
String phoneNumber = phoneNumberTV.getText().toString();
Contact contact = mdbh.getContactFromPhoneNumber(phoneNumber);
Dialog d = myUtilities.createSelectedContactOptionsDialog(contact);
d.show();
contactsAdapter.updateAdapter(mdbh.getAllContacts());
}
});
public void addContact(View view) {
Dialog d = myUtilities.createAddContactDialog();
d.show();
contactsAdapter.updateAdapter(mdbh.getAllContacts());
}
}
The Adater:
private List<Contact> allContacts;
private int numberOfContacts;
private MyDatabaseHelper mdbh;
private Context context;
public AdapterContactListView(Context c, MyDatabaseHelper m) {
super();
context = c;
mdbh = m;
allContacts = mdbh.getAllContacts();
numberOfContacts = allContacts.size();
}
public void updateAdapter(List<Contact> cs) {
allContacts = cs;
numberOfContacts = allContacts.size();
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
View view = convertView;
if (view == null) {
LayoutInflater li = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = li.inflate(R.layout.contact_element, null);
}
String fullName = allContacts.get(position).getFirstName();
fullName = fullName.concat(" ");
fullName = fullName.concat(allContacts.get(position).getLastName());
TextView contactName = (TextView)view.findViewById(R.id.contactElementNameTV);
TextView phoneNumber = (TextView)view.findViewById(R.id.contactElementNumberTV);
contactName.setText(fullName);
phoneNumber.setText(allContacts.get(position).getPhoneNumber());
return view;
}
The job of the dialogs is to do operations with the database (update, add, delete).
I tried placing the notifyDataSetChanged() right after updateAdapter() and in the updateAdapter() method itself, but none of that works.
One thing I think I might be missing:
The definition notifyDataSetChanged() says that it notifies attached observers. I have no attached observers, but in all the answers that I have read no one mentioned anything about attaching an observer. If you think this is the problem, please tell me how to do this.
Can someone, please, shed some light on this problem.
Thanks in advance.

To refresh your listview, either you can call a method from a list called notifydatasetchanged but that will only work when youll get the contacts in the contactsalllist. So what you need to do is that you should first get the new data in your arraylist and then call notifydataset changed on your listview.
Apart from that if you are using Contacts database I would recommend you using a simplecursoradapter instead of using the baseadapter but its completely upto you. Anyhow I have added the code for that as well. Let me know if it helps you.
/**
*
* #author Syed Ahmed Hussain
*/
public class TestListActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks<Cursor>, MultiChoiceModeListener {
NotificationsAdapter mNotificationAdapter;
ListView mNotificationsListView;
TextView mTxtNotificationsInfo;
Button mBtnCreateNotification;
public static final String TAG = "NotificationsList";
// ---------------------------------------------------------------------------
#Override
protected void onCreate(Bundle pSavedInstanceState) {
super.onCreate(pSavedInstanceState);
setContentView(R.layout.fragment_notifications);
initializeUIElements();
// registerForContextMenu(mNotificationsListView);
getSupportLoaderManager().initLoader(0, null, this);
mNotificationsListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
mNotificationsListView.setMultiChoiceModeListener(this);
}
// ---------------------------------------------------------------------------
/**
*
*/
private void initializeUIElements() {
mTxtNotificationsInfo = (TextView) findViewById(R.id.txtNotificationInfo);
mNotificationsListView = (ListView) findViewById(R.id.list_notifications);
mBtnCreateNotification = (Button) findViewById(R.id.btnAddNotification);
}
// ---------------------------------------------------------------------------
/**
*
*/
public void onCreateNewNotificationClick(View pV) {
Log.d(TAG, "onCreateNewNotificationClick");
Intent intent = new Intent(this, AddNewNotification.class);
startActivity(intent);
// setResult(0);
// finish();
}
// ---------------------------------------------------------------------------
#Override
public Loader<Cursor> onCreateLoader(int pId, Bundle pArgs) {
return new android.support.v4.content.CursorLoader(getApplicationContext(), NotificationsContentProvider.CONTENT_URI, null, null, null, null);
}
#Override
public void onLoadFinished(Loader<Cursor> pLoader, Cursor pData) {
if (pData == null || pData.getCount() == 0) {
Log.d("pData", "is null");
showTextView();
return;
}
mNotificationAdapter = new NotificationsAdapter(this, R.layout.item_notification, pData, new String[] { NotificationDatabaseHelper.COL_TITLE }, new int[] {R.id.txtNotificationTitle}, 0);
mNotificationsListView.setAdapter(mNotificationAdapter);
}
#Override
public void onLoaderReset(Loader<Cursor> pLoader) {
}
// ---------------------------------------------------------------------------
/**
* Hides the list view. shows the textview
*/
private void showTextView() {
mNotificationsListView.setVisibility(View.GONE);
mTxtNotificationsInfo.setVisibility(View.VISIBLE);
}
// ---------------------------------------------------------------------
// Multi-choice list item
#Override
public boolean onCreateActionMode(ActionMode pMode, Menu pMenu) {
// Inflate the menu for the CAB
MenuInflater inflater = pMode.getMenuInflater();
inflater.inflate(R.menu.menu_list_item, pMenu);
return true;
}
#Override
public boolean onPrepareActionMode(ActionMode pMode, Menu pMenu) {
return false;
}
#Override
public boolean onActionItemClicked(ActionMode pMode, MenuItem pItem) {
switch (pItem.getItemId()) {
case 1:
Toast.makeText(getApplicationContext(), pItem.getTitle(), Toast.LENGTH_SHORT).show();
pMode.finish();
break;
case 2:
Toast.makeText(getApplicationContext(), pItem.getTitle(), Toast.LENGTH_SHORT).show();
pMode.finish();
break;
default:
break;
}
return true;
}
#Override
public void onDestroyActionMode(ActionMode pMode) {
}
#Override
public void onItemCheckedStateChanged(ActionMode pMode, int pPosition, long pId, boolean pChecked) {
}
// --------------------------------------------------------------------
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu items for use in the action bar
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.actionbar_menu_items, menu);
return super.onCreateOptionsMenu(menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
return super.onOptionsItemSelected(item);
}
}
**
Notification Adapter is a Simple Curser Adapter:
**
/**
*
* #author Syed Ahmed Hussain
*/
public class NotificationsAdapter extends SimpleCursorAdapter {
private LayoutInflater mLayoutInflater;
private Context mContext;
private int mLayout;
public NotificationsAdapter(Context pContext, int pLayout, Cursor pC, String[] pFrom, int[] pTo, int pFlags) {
super(pContext, pLayout, pC, pFrom, pTo, pFlags);
mLayout = pLayout;
mContext = pContext;
mLayoutInflater = LayoutInflater.from(mContext);
}
#Override
public View newView(Context pContext, Cursor pCursor, ViewGroup pParent) {
return mLayoutInflater.inflate(mLayout, null);
}
}
To get it updated whenever you have a change in records do notifydatasetchange or take a refresh cursor to get all the values. Recall the method which returns you the dataset/cursor.

Related

Items clickable but invisible on screen: downloaded via AsyncTaskLoader

I'm storing a list of the most frequented transit lines in a Content Provider and a RecyclerView Adapter to be available in online and offline viewing. They are downloaded into an activity via AsyncTaskLoader. They are accessible from the menu option in the activity below. I debugged and it shows that items are being added to the Content Provider in another activity. Then, when I click on "most frequented" in the menu option, the screen is blank but the items are clickable(both on and offline). I've tried to debug but it's not showing any errors.
I found a similar thread:
RecyclerView items are clickable but invisible
In my case, all the fonts and colors are correct. I know that the Loader is deprecated as of sdk 28. However, it's not even working in 27. Thank you in advance.
Activity where the data is added to the Content Provider:
public class StationListActivity extends AppCompatActivity implements StationsAdapter.StationsAdapterOnClickHandler, TubeStationAsyncTaskInterface,
LoaderManager.LoaderCallbacks<Cursor>
{
//Tag for the log messages
private static final String TAG = StationListActivity.class.getSimpleName();
#BindView(R.id.recyclerview_station)
RecyclerView mStationRecyclerView;
private StationsAdapter stationsAdapter;
private ArrayList<Stations> stationsArrayList = new ArrayList<>();
private static final String KEY_STATIONS_LIST = "stations_list";
private static final String KEY_LINE_NAME = "line_name";
Lines lines;
public String lineId;
private Context context;
private TextView lineNameStation;
private String lineNameToString;
#BindView(R.id.favorites_button)
Button favoritesButton;
/**
* Identifier for the favorites data loader
*/
private static final int FAVORITES_LOADER = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_station_list);
context = getApplicationContext();
// Bind the views
ButterKnife.bind(this);
stationsAdapter = new StationsAdapter(this, stationsArrayList, context);
mStationRecyclerView.setAdapter(stationsAdapter);
RecyclerView.LayoutManager mStationLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mStationRecyclerView.setLayoutManager(mStationLayoutManager);
lineNameStation = (TextView) findViewById(R.id.line_name_station);
//add to favorites
favoritesButton.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View view)
{
ContentValues values = new ContentValues();
values.put(TubeLineContract.TubeLineEntry.COLUMN_LINES_ID, lines.getLineId());
values.put(TubeLineContract.TubeLineEntry.COLUMN_LINES_NAME, lines.getLineName());
Uri uri = getContentResolver().insert(TubeLineContract.TubeLineEntry.CONTENT_URI, values);
if (uri != null)
{
Toast.makeText(getBaseContext(), uri.toString(), Toast.LENGTH_LONG).show();
Toast.makeText(StationListActivity.this, R.string.favorites_added, Toast.LENGTH_SHORT).show();
favoritesButton.setVisibility(View.GONE);
}
}
});
/*
* Starting the asyncTask so that stations load when the activity opens.
*/
if (getIntent() != null && getIntent().getExtras() != null)
{
if (savedInstanceState == null)
{
lines = getIntent().getExtras().getParcelable("Lines");
lineId = lines.getLineId();
TubeStationAsyncTask myStationTask = new TubeStationAsyncTask(this);
myStationTask.execute(lineId);
lineNameStation.setText(lines.getLineName());
} else
{
stationsArrayList = savedInstanceState.getParcelableArrayList(KEY_STATIONS_LIST);
stationsAdapter.setStationsList(stationsArrayList);
}
}
// Kick off the loader
getLoaderManager().initLoader(FAVORITES_LOADER, null, this);
}
#Override
public void returnStationData(ArrayList<Stations> simpleJsonStationData) {
if (null != simpleJsonStationData) {
stationsAdapter = new StationsAdapter(this, simpleJsonStationData, StationListActivity.this);
stationsArrayList = simpleJsonStationData;
mStationRecyclerView.setAdapter(stationsAdapter);
stationsAdapter.setStationsList(stationsArrayList);
}
}
#Override
public void onClick(Stations stations) {
Intent intent = new Intent(StationListActivity.this, StationScheduleActivity.class);
intent.putExtra("Stations", stations);
intent.putExtra("Lines", lines);
startActivity(intent);
}
#Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle)
{
String[] projection = {TubeLineContract.TubeLineEntry._ID, TubeLineContract.TubeLineEntry.COLUMN_LINES_ID,};
String[] selectionArgs = new String[]{lineId};
switch (loaderId)
{
case FAVORITES_LOADER:
return new CursorLoader(this, // Parent activity context
TubeLineContract.TubeLineEntry.CONTENT_URI, // Provider content URI to query
projection, // Columns to include in the resulting Cursor
TubeLineContract.TubeLineEntry.COLUMN_LINES_ID + "=?",
selectionArgs,
null); // Default sort order
default:
throw new RuntimeException("Loader Not Implemented: " + loaderId);
}
}
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor)
{
if ((cursor != null) && (cursor.getCount() > 0))
{
//"Add to Favorites" button is disabled in the StationList Activity when the user clicks on a line stored in Favorites
favoritesButton.setEnabled(false);
}
}
public void onLoaderReset(Loader<Cursor> cursorLoader)
{
}
#Override
protected void onSaveInstanceState(Bundle outState) {
outState.putParcelableArrayList(KEY_STATIONS_LIST, stationsArrayList);
super.onSaveInstanceState(outState);
}
}
Activity with the menu option:
public class MainActivity extends AppCompatActivity implements LinesAdapter.LinesAdapterOnClickHandler, TubeLineAsyncTaskInterface,
LoaderManager.LoaderCallbacks<Cursor> {
// Tag for logging
private static final String TAG = MainActivity.class.getSimpleName();
#BindView(R.id.recyclerview_main)
RecyclerView mLineRecyclerView;
private LinesAdapter linesAdapter;
private ArrayList<Lines> linesArrayList = new ArrayList<>();
private Context context;
private static final String KEY_LINES_LIST = "lines_list";
CoordinatorLayout mCoordinatorLayout;
#BindView(R.id.pb_loading_indicator)
ProgressBar mLoadingIndicator;
private AdView adView;
private FavoritesAdapter favoritesAdapter;
private static final int FAVORITES_LOADER_ID = 0;
private int mPosition = RecyclerView.NO_POSITION;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
context = getApplicationContext();
// Bind the views
ButterKnife.bind(this);
mCoordinatorLayout = findViewById(R.id.coordinatorLayout);
favoritesAdapter = new FavoritesAdapter(this, context);
linesAdapter = new LinesAdapter(this, linesArrayList, context);
mLineRecyclerView.setAdapter(linesAdapter);
RecyclerView.LayoutManager mLineLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mLineRecyclerView.setLayoutManager(mLineLayoutManager);
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
#Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
#Override
public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
if (viewHolder instanceof LinesAdapter.LinesAdapterViewHolder) return 0;
return super.getSwipeDirs(recyclerView, viewHolder);
}
// Called when a user swipes left or right on a ViewHolder
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
// Here is where you'll implement swipe to delete
//Construct the URI for the item to delete
//[Hint] Use getTag (from the adapter code) to get the id of the swiped item
// Retrieve the id of the task to delete
int id = (int) viewHolder.itemView.getTag();
// Build appropriate uri with String row id appended
String stringId = Integer.toString(id);
Uri uri = TubeLineContract.TubeLineEntry.CONTENT_URI;
uri = uri.buildUpon().appendPath(stringId).build();
// TODO (2) Delete a single row of data using a ContentResolver
int rowsDeleted = getContentResolver().delete(uri, null, null);
Log.v("CatalogActivity", rowsDeleted + " rows deleted from the movie database");
// TODO (3) Restart the loader to re-query for all tasks after a deletion
getSupportLoaderManager().restartLoader(FAVORITES_LOADER_ID, null, MainActivity.this);
}
}).attachToRecyclerView(mLineRecyclerView);
/*
* Starting the asyncTask so that lines load upon launching the app.
*/
if (savedInstanceState == null)
{
if (isNetworkStatusAvailable(this))
{
TubeLineAsyncTask myLineTask = new TubeLineAsyncTask(this);
myLineTask.execute(NetworkUtils.buildLineUrl());
} else {
Snackbar
.make(mCoordinatorLayout, "Please check your internet connection", Snackbar.LENGTH_INDEFINITE)
.setAction("Retry", new MyClickListener())
.show();
}
} else {
linesArrayList = savedInstanceState.getParcelableArrayList(KEY_LINES_LIST);
linesAdapter.setLinesList(linesArrayList);
}
getSupportLoaderManager().initLoader(FAVORITES_LOADER_ID, null, MainActivity.this);
mLineRecyclerView.setAdapter(favoritesAdapter);
}
public class MyClickListener implements View.OnClickListener {
#Override
public void onClick(View v) {
// Run the AsyncTask in response to the click
TubeLineAsyncTask myLineTask = new TubeLineAsyncTask(MainActivity.this);
myLineTask.execute();
}
}
#Override
public void returnLineData(ArrayList<Lines> simpleJsonLineData) {
mLoadingIndicator.setVisibility(View.INVISIBLE);
if (null != simpleJsonLineData) {
linesAdapter = new LinesAdapter(this, simpleJsonLineData, MainActivity.this);
linesArrayList = simpleJsonLineData;
mLineRecyclerView.setAdapter(linesAdapter);
linesAdapter.setLinesList(linesArrayList);
} else {
showErrorMessage();
}
}
#Override
public void onClick(Lines lines) {
Intent intent = new Intent(MainActivity.this, StationListActivity.class);
intent.putExtra("Lines", lines);
startActivity(intent);
}
//Display if there is no internet connection
public void showErrorMessage() {
Snackbar
.make(mCoordinatorLayout, "Please check your internet connection", Snackbar.LENGTH_INDEFINITE)
.setAction("Retry", new MyClickListener())
.show();
mLineRecyclerView.setVisibility(View.INVISIBLE);
mLoadingIndicator.setVisibility(View.VISIBLE);
}
public static boolean isNetworkStatusAvailable(Context context) {
ConnectivityManager cm =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return activeNetwork != null &&
activeNetwork.isConnectedOrConnecting();
}
#Override
public Loader<Cursor> onCreateLoader(int id, final Bundle loaderArgs)
{
return new AsyncTaskLoader<Cursor>(this)
{
// Initialize a Cursor, this will hold all the task data
Cursor mFavoritesData = null;
// onStartLoading() is called when a loader first starts loading data
#Override
protected void onStartLoading()
{
if (mFavoritesData != null)
{
// Delivers any previously loaded data immediately
deliverResult(mFavoritesData);
}
else
{
// Force a new load
forceLoad();
}
}
// loadInBackground() performs asynchronous loading of data
#Override
public Cursor loadInBackground()
{
// Will implement to load data
// Query and load all task data in the background; sort by priority
// [Hint] use a try/catch block to catch any errors in loading data
try
{
return getContentResolver().query(TubeLineContract.TubeLineEntry.CONTENT_URI,
null,
null,
null,
TubeLineContract.TubeLineEntry.COLUMN_LINES_ID);
}
catch (Exception e)
{
Log.e(LOG_TAG, "Failed to asynchronously load data.");
e.printStackTrace();
return null;
}
}
// deliverResult sends the result of the load, a Cursor, to the registered listener
public void deliverResult(Cursor data)
{
mFavoritesData = data;
super.deliverResult(data);
}
};
}
/**
* Called when a previously created loader has finished its load.
*
* #param loader The Loader that has finished.
* #param data The data generated by the Loader.
*/
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data)
{
favoritesAdapter.swapCursor(data);
if (mPosition == RecyclerView.NO_POSITION) mPosition = 0;
mLineRecyclerView.smoothScrollToPosition(mPosition);
}
/**
* Called when a previously created loader is being reset, and thus
* making its data unavailable.
* onLoaderReset removes any references this activity had to the loader's data.
*
* #param loader The Loader that is being reset.
*/
#Override
public void onLoaderReset(Loader<Cursor> loader)
{
favoritesAdapter.swapCursor(null);
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
/* Use AppCompatActivity's method getMenuInflater to get a handle on the menu inflater */
MenuInflater inflater = getMenuInflater();
/* Use the inflater's inflate method to inflate our menu layout to this menu */
inflater.inflate(R.menu.main, menu);
/* Return true so that the menu is displayed in the Toolbar */
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item)
{
TubeLineAsyncTask myLineTask = new TubeLineAsyncTask(this);
switch (item.getItemId())
{
case R.id.most_frequented_favorites:
getSupportLoaderManager().restartLoader(FAVORITES_LOADER_ID, null, MainActivity.this);
favoritesAdapter = new FavoritesAdapter(this, MainActivity.this);
mLineRecyclerView.setAdapter(favoritesAdapter);
return true;
case R.id.line_list:
myLineTask.execute();
return true;
default:
return super.onOptionsItemSelected(item);
}}
#Override
protected void onSaveInstanceState(Bundle outState) {
outState.putParcelableArrayList(KEY_LINES_LIST, linesArrayList);
super.onSaveInstanceState(outState);
}
}
RecyclerViewAdapter class:
public class FavoritesAdapter extends RecyclerView.Adapter<FavoritesAdapter.FavoritesAdapterViewHolder>
{
private static final String TAG = FavoritesAdapter.class.getSimpleName();
private Context context;
private Cursor cursor;
private LinesAdapter.LinesAdapterOnClickHandler mClickHandler;
public FavoritesAdapter(LinesAdapter.LinesAdapterOnClickHandler clickHandler, Context context)
{
mClickHandler = clickHandler;
this.context = context;
}
public class FavoritesAdapterViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
#BindView(R.id.line_name)
TextView lineName;
public FavoritesAdapterViewHolder(View view) {
super(view);
ButterKnife.bind(this, view);
view.setOnClickListener(this);
}
#Override
public void onClick(View v) {
cursor.moveToPosition(getAdapterPosition());
String lineName = cursor.getString(cursor.getColumnIndexOrThrow(TubeLineContract.TubeLineEntry.COLUMN_LINES_NAME));
String lineId = cursor.getString(cursor.getColumnIndexOrThrow(TubeLineContract.TubeLineEntry.COLUMN_LINES_ID));
Lines line = new Lines(lineName, lineId);
mClickHandler.onClick(line);
}
}
#Override
public void onBindViewHolder(FavoritesAdapter.FavoritesAdapterViewHolder holder, int position)
{
// get to the right location in the cursor
cursor.moveToPosition(position);
// Determine the values of the wanted data
int lineIdIndex = cursor.getColumnIndexOrThrow(TubeLineContract.TubeLineEntry.COLUMN_LINES_ID);
final int id = cursor.getInt(lineIdIndex);
holder.itemView.setTag(id);
}
#Override
public FavoritesAdapter.FavoritesAdapterViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType)
{
Context context = viewGroup.getContext();
int layoutIdForListItem = R.layout.line_list_item;
LayoutInflater inflater = LayoutInflater.from(context);
boolean shouldAttachToParentImmediately = false;
View view = inflater.inflate(layoutIdForListItem, viewGroup, shouldAttachToParentImmediately);
return new FavoritesAdapter.FavoritesAdapterViewHolder(view);
}
public Cursor swapCursor(Cursor c)
{
// check if this cursor is the same as the previous cursor (mCursor)
if (cursor == c)
{
return null; // bc nothing has changed
}
Cursor temp = cursor;
this.cursor = c; // new cursor value assigned
//check if this is a valid cursor, then update the cursor
if (c != null)
{
this.notifyDataSetChanged();
}
return temp;
}
#Override
public int getItemCount()
{
if (null == cursor)
return 0;
return cursor.getCount();
}
}

Attempting to access a closed CursorWindow.Most probable cause: cursor is deactivated prior to calling this method

How to resolve given error? It happens when I try to refresh cursor from actionbar and sometimes when I try to delete again after long click on list item.
MainActivity.java
public class MainActivity extends AppCompatActivity {
private final String QUERY_SELECTALL = "SELECT * FROM " + RjecnikDB.TABLE;
private RjecnikCursorAdapter adapter;
private RjecnikDB dbRjecnik;
private SQLiteDatabase db;
private Cursor cursor;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
#Override
protected void onRestart() {
super.onRestart();
refresh();
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.search_bar) {
return true;
} else if (id == R.id.refresh) {
refresh();
}
return super.onOptionsItemSelected(item);
}
public void refresh() {
dbRjecnik = RjecnikDB.getInstance(this);
db = dbRjecnik.getReadableDatabase();
cursor = db.rawQuery(QUERY_SELECTALL, null);
adapter = RjecnikCursorAdapter.getInstance(this, cursor);
adapter.changeCursor(cursor);
}
WordsListFragment.java
public class WordsListFragment extends Fragment {
private final String QUERY_SELECTALL = "SELECT * FROM " + RjecnikDB.TABLE;
private RjecnikCursorAdapter adapter;
private RjecnikDB dbRjecnik;
private SQLiteDatabase db;
private Cursor cursor;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.words_list_fragment_layout, container, false);
ListView listView = (ListView) view.findViewById(R.id.listView);
fabAddWord = (FloatingActionButton) view.findViewById(R.id.fabAddWord);
dbRjecnik = RjecnikDB.getInstance(getActivity());
db = dbRjecnik.getWritableDatabase();
cursor = db.rawQuery(QUERY_SELECTALL, null);
adapter = RjecnikCursorAdapter.getInstance(getActivity(), cursor);
listView.setAdapter(adapter);
listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
#Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
int _id = cursor.getInt(cursor.getColumnIndex(RjecnikDB.COLUMN_ID));
DiscardDialogFragment newFragment = new DiscardDialogFragment(_id);
newFragment.show(getFragmentManager(), "discard");
return true;
}
});
return view;
}
public void changeCursor() {
dbRjecnik = RjecnikDB.getInstance(getActivity());
db = dbRjecnik.getReadableDatabase();
cursor = db.rawQuery(QUERY_SELECTALL, null);
adapter.changeCursor(cursor);
}
public void deleteOnLongClick(int id) {
db = dbRjecnik.getWritableDatabase();
db.delete(RjecnikDB.TABLE, RjecnikDB.COLUMN_ID + " = ?", new String[] { Integer.toString(id) } );
}
public class DiscardDialogFragment extends DialogFragment {
int colID;
DiscardDialogFragment() {}
DiscardDialogFragment(int colID) { this.colID = colID; }
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage("Izbriši riječ?")
.setPositiveButton("IZBRIŠI",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
deleteOnLongClick(colID);
changeCursor();
Toast.makeText(getActivity(),
"Riječ je izbrisana iz baze", Toast.LENGTH_SHORT).show();
}
})
.setNegativeButton("OTKAŽI", null);
return builder.create();
}
}
Does anyone have an idea to grasp this changeCursor or refresh in one class, I tried many ways, but always it was errors.
I was experiencing this same error when returning to my app (from another app, which my app opened).
The solution for me was to replace my spinnerAdapter.changeCursor(cursor) code with spinnerAdapter.swapCursor(cursor).
I suspect that we both had similar issues. I had a listener on a CheckBox in each list item. The listener was firing while the cursor was closing. Post your code for RjecnikCursorAdapter or you can test the listeners yourself. Check the listeners set in the code above or any set in RjecnikCursorAdapter.bindView or RjecnikCursorAdapter.getView.
This comment is incorrect:
I have not any cursor.close() call.
adapter.changeCursor(cursor) closes the old cursor for you.
I had same problem in my case clean and rebuild the project worked for me .
In addition i even deleted the app data in my mobile and changing the
adapter.close() to adapter.swapCursor(cursor);

Listview does not refresh when underlying Loader data changes

First, I'll preface my question with the fact that I'm not using a CursorLoader.
I'm pulling in data from a SQLlite database to populate a listview in a ListFragment. The initial load works well, but once the data is manipulated (i.e. an addition is made to the list), the listview NEVER refreshes to show the new data. I am implementing the Loader callbacks like so:
public class BillListingFragment extends ListFragment implements LoaderManager.LoaderCallbacks<List<Bill>> {
private billListAdapter mAdapter;
private static final int LOADER_ID = 1;
private SQLiteDatabase mDatabase;
private BillsDataSource mDataSource;
private BillsStoreDatabaseHelper mDbHelper;
/**
* The fragment argument representing the fragment type (archive or outstanding)
*/
private static final String ARG_FRAGMENT_TYPE = "fragment_type";
/**
* Returns a new instance of this fragment based on type
*/
public static BillListingFragment newInstance(String type) {
// TODO: Make the fragment type an enum
BillListingFragment fragment = new BillListingFragment();
Bundle args = new Bundle();
args.putString(ARG_FRAGMENT_TYPE, type);
fragment.setArguments(args);
return fragment;
}
public BillListingFragment() {
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.bill_view_layout, container, false);
return rootView;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mDbHelper = new BillsStoreDatabaseHelper(getActivity());
mDatabase = mDbHelper.getWritableDatabase();
mDataSource = new BillsDataSource(mDatabase);
mAdapter = new billListAdapter(getActivity(), R.layout.bill_row_layout);
setListAdapter(mAdapter);
getLoaderManager().initLoader(LOADER_ID, null, this);
}
#Override
public Loader<List<Bill>> onCreateLoader(int id, Bundle args) {
BillDataLoader loader = new BillDataLoader(getActivity(), mDataSource);
return loader;
}
#Override
public void onLoadFinished(Loader<List<Bill>> loader, List<Bill> data) {
for(Bill bill: data){
mAdapter.add(bill);
}
setListAdapter(mAdapter);
}
#Override
public void onLoaderReset(Loader<List<Bill>> loader) {
mAdapter.clear();
}
#Override
public void onDestroy() {
super.onDestroy();
mDbHelper.close();
mDatabase.close();
mDataSource = null;
mDbHelper = null;
mDatabase = null;
}
public void reload(){
getLoaderManager().restartLoader(LOADER_ID, null, this);
}
private class billListAdapter extends ArrayAdapter<Bill> {
Context context;
public billListAdapter(Context context, int resourceID){
super(context, resourceID);
this.context = context;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = getActivity().getLayoutInflater().inflate(R.layout.bill_row_layout, parent, false);
}
TextView payToField = (TextView) convertView.findViewById(R.id.nameField);
TextView dueDateField = (TextView) convertView.findViewById(R.id.overdueField);
payToField.setText(getItem(position).getPayTo());
// calculate days until due
Bill bill = getItem(position);
// TODO: Add how many days until bill in overdue field + add color
JodaTimeAndroid.init(getActivity());
DateTime dueDateDt = new DateTime(bill.getDateDue());
DateTime currentDt = new DateTime();
int daysDifference = Days.daysBetween(currentDt.toLocalDate(), dueDateDt.toLocalDate()).getDays();
// depending on what that differential looks like set text / color
if (daysDifference > 1) {
dueDateField.setText(Integer.toString(daysDifference) + " Days");
} else {
if (daysDifference == 0) {
dueDateField.setText("DUE TODAY");
} else {
if (daysDifference < 0) {
}
}
}
return convertView;
}
}
}
I have debugged my code so I know that the onLoadFinished callback is being made after the data has been manipulated. I also know that adapter contains the updated data at this point. I have tried resetting the adapter via setListAdapter(mAdatper) and every notifyDataChanged-like method I can find, but to no avail. What is going on here and how can I get the listview to update?

Calling notifyDataSetChanged() outside of the Fragment it is in

I'm writing an application that allows for the sharing of recipes. When one receives a recipe, they can save it to their phone and it will appear as a fragment of a list.
Problem is, when I save it, I get an IllegalStateException for not calling NotifyDataSetChanged(), which I can't find a way to do in the Activity that I am in. If anyone knows how I can find a way to get access to the adapter that calls this, that would be greatly appreciated. Making it static didn't seem to be an option.
public class SmsViewActivity extends Activity {
private static final String TAG = "SmsViewActivity";
private static final String SMS_FOOD = "food_recieved";
private FoodJSONSerializer mSerializer;
public Button mSaveButton, mDismissButton;
public int mNotificationId;
public String message;
private EditText mTitleField;
private EditText mServingsField;
private EditText mDirectionsField;
Food mFood;
private String msg;
private Activity mActivity;
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Log.i(TAG, "OnCreate");
mActivity = this;
setContentView(R.layout.sms_view);
mSaveButton = (Button) findViewById(R.id.save_button_sms);
mDismissButton = (Button) findViewById(R.id.dismiss_button_sms);
// ------------------------------------------------------------
// Get extras and display information in view
//String sender = getIntent().getStringExtra("sender");
this.msg = getIntent().getStringExtra("message");
try {
JSONObject jsonRecipe = new JSONObject(this.msg);
this.mFood = new Food(jsonRecipe);
Log.i(TAG, "Food = " + mFood);
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// -----------------------------------------------------------------------
mNotificationId = getIntent().getIntExtra("notificationid", 0);
if (mNotificationId == 0) {
Log.e(TAG, "Could not retrieve notification ID.");
Toast.makeText(this, "A fatal error has occurred in SMS viewer.",
Toast.LENGTH_LONG).show();
finish();
}
// Cancel the notification
String ns = Context.NOTIFICATION_SERVICE;
NotificationManager notificationMgr = (NotificationManager) getSystemService(ns);
notificationMgr.cancel(mNotificationId);
// --------------------------------------------------
this.mTitleField = (EditText) findViewById(R.id.food_title_sms);
this.mTitleField.setText(mFood.getTitle());
this.mServingsField = (EditText) findViewById(R.id.food_servings_sms);
this.mServingsField.setText(Integer.toString(mFood.getServings()));
this.mDirectionsField = (EditText) findViewById(R.id.directions_text_sms);
this.mDirectionsField.setText(mFood.getDirections());
// --------------------------------------------------
// Listener for Save button click
this.mSaveButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
FoodStorage.get(mActivity).addFood(mFood);
//NEED TO CALL notifyDataSetChanged(); HERE
finish();
}
});
// Listener for Dismiss button click
this.mDismissButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
backToList();
finish();
}
});
}
public void backToList() {
Intent i = new Intent(this, FoodListActivity.class);
startActivity(i);
}
}
Here is the Fragment where the adapter lives and where every other instance of adding or deleting occurs.
public class FoodListFragment extends ListFragment{
private ArrayList<Food> mFood;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
getActivity().setTitle(R.string.food_title);
mFood = FoodStorage.get(getActivity()).getFood();
FoodAdapter adapter = new FoodAdapter(mFood);
setListAdapter(adapter);
}
#TargetApi(11)
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
View v = super.onCreateView(inflater, parent, savedInstanceState);
ListView listView = (ListView)v.findViewById(android.R.id.list);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
// Use floating context menus on Froyo and Gingerbread
registerForContextMenu(listView);
} else {
// Use contextual action bar on Honeycomb and higher
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
listView.setMultiChoiceModeListener(new MultiChoiceModeListener() {
public void onItemCheckedStateChanged(ActionMode mode, int position,
long id, boolean checked) {
// Required, but not used in this implementation
}
// ActionMode.Callback methods
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.food_list_item_context, menu);
return true;
}
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
// Required, but not used in this implementation
}
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_delete_food:
FoodAdapter adapter = (FoodAdapter)getListAdapter();
FoodStorage foodStorage = FoodStorage.get(getActivity());
for (int i = adapter.getCount() - 1; i >= 0; i--) {
if (getListView().isItemChecked(i)) {
foodStorage.deleteFood(adapter.getItem(i));
}
}
mode.finish();
adapter.notifyDataSetChanged();
return true;
default:
return false;
}
}
public void onDestroyActionMode(ActionMode mode) {
// Required, but not used in this implementation
}
});
}
return v;
}
public void onListItemClick(ListView l, View v, int position, long id) {
// get the Food from the adapter
Food c = ((FoodAdapter)getListAdapter()).getItem(position);
// start an instance of CrimePagerActivity
Intent i = new Intent(getActivity(), FoodPagerActivity.class);
i.putExtra(FoodFragment.EXTRA_FOOD_ID, c.getId());
startActivityForResult(i, 0);
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
((FoodAdapter)getListAdapter()).notifyDataSetChanged();
}
#Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.fragment_food_list, menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_new_food:
Food food = new Food();
FoodStorage.get(getActivity()).addFood(food);
Intent i = new Intent(getActivity(), FoodPagerActivity.class);
i.putExtra(FoodFragment.EXTRA_FOOD_ID, food.getId());
startActivityForResult(i, 0);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
#Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
getActivity().getMenuInflater().inflate(R.menu.food_list_item_context, menu);
}
#Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo)item.getMenuInfo();
int position = info.position;
FoodAdapter adapter = (FoodAdapter)getListAdapter();
Food food = adapter.getItem(position);
switch (item.getItemId()) {
case R.id.menu_item_delete_food:
FoodStorage.get(getActivity()).deleteFood(food);
adapter.notifyDataSetChanged();
return true;
}
return super.onContextItemSelected(item);
}
private class FoodAdapter extends ArrayAdapter<Food> {
public FoodAdapter(ArrayList<Food> food) {
super(getActivity(), android.R.layout.simple_list_item_1, food);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// if we weren't given a view, inflate one
if (null == convertView) {
convertView = getActivity().getLayoutInflater()
.inflate(R.layout.list_item_food, null);
}
// configure the view for this recipe
Food c = getItem(position);
TextView titleTextView =
(TextView)convertView.findViewById(R.id.food_list_item_titleTextView);
titleTextView.setText(c.getTitle());
TextView servingsTextView =
(TextView)convertView.findViewById(R.id.food_list_item_servingsTextView);
servingsTextView.setText("Makes " + c.getServings() + " servings");
TextView ingredientsTextView = (TextView) convertView
.findViewById(R.id.food_list_item_ingredientsTextView);
JSONArray j = c.jIngredients;
String display = "";
for (int i = 0; i < j.length(); i++) {
try {
display = display + j.get(i) + "\n";
} catch (JSONException e) {
// Do nothing.
e.printStackTrace();
}
}
ingredientsTextView.setText("Ingredients:\n" + display);
TextView directionsTextView =
(TextView)convertView.findViewById(R.id.food_list_item_directionsTextView);
directionsTextView.setText("Directions:\n " + c.getDirections());
return convertView;
}
}
}
You may use Eventbus from greenrobot. It provides you with a way to post events anywhere to any subscriber who can receive the event and call any method.
In the onCreate method of your fragment, call this EventBus.getDefault().register(this); to register your fragment as a subscriber.
Create a new method in your fragment: public void onEvent(AnyEventType event) {}. When an event is posted on the event bus, this method will be called. you may call adapter.notifyDatasetChanged() in the onEvent method. AnyEventType can be just any class (can be a POJO). You may use this class to pass any info to your fragment.
In your activity, you can call EventBus.getDefault().post(event); to notify your fragment. The onEvent method in your fragment will be executed when the event is posted.
Refer to the EventBus readme for more info.

Populating a ListView using AsyncTaskLoader

I'm having problems using AsyncTaskLoader. This is my first attempt populating a ListView from a SQLite database using a loader.
Everything seems ok, when I rotate the screen the data is cached and no query is done again. But when I press the home button and launch my app again, the data is loaded again.
Note: Usuario means User, so I'm populating the ListView with a list of users.
public class Main extends SherlockFragmentActivity
implements LoaderManager.LoaderCallbacks<ArrayList<Usuario>> {
UsuarioAdapter adapter;
ListView listView;
Database db;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
listView = (ListView) findViewById(R.id.lista);
db = new Database(this);
adapter = new UsuarioAdapter(this, new ArrayList<Usuario>());
listView.setAdapter(adapter);
getSupportLoaderManager().initLoader(0, null, this);
}
#Override
public Loader<ArrayList<Usuario>> onCreateLoader(int id, Bundle args) {
return new UsuariosLoader(this, db);
}
#Override
public void onLoadFinished(Loader<ArrayList<Usuario>> loader,
ArrayList<Usuario> usuarios) {
//adapter.notifyDataSetChanged();
listView.setAdapter(new UsuarioAdapter(this, usuarios));
// ((BaseAdapter) listView.getAdapter()).notifyDataSetChanged();
}
#Override
public void onLoaderReset(Loader<ArrayList<Usuario>> loader) {
listView.setAdapter(null);
}
}
// THE LOADER
class UsuariosLoader extends AsyncTaskLoader<ArrayList<Usuario>> {
private ArrayList<Usuario> usuarios;
private Database db;
public UsuariosLoader(Context context, Database db) {
super(context);
this.db = db;
}
#Override
protected void onStartLoading() {
if (usuarios != null) {
deliverResult(usuarios); // Use the cache
}
forceLoad();
}
#Override
protected void onStopLoading() {
// The Loader is in a stopped state, so we should attempt to cancel the
// current load (if there is one).
cancelLoad();
}
#Override
public ArrayList<Usuario> loadInBackground() {
db.open(); // Query the database
ArrayList<Usuario> usuarios = db.getUsuarios();
db.close();
return usuarios;
}
#Override
public void deliverResult(ArrayList<Usuario> data) {
usuarios = data; // Caching
super.deliverResult(data);
}
#Override
protected void onReset() {
super.onReset();
// Stop the loader if it is currently running
onStopLoading();
// Get rid of our cache if it exists
usuarios = null;
}
#Override
public void onCanceled(ArrayList<Usuario> data) {
// Attempt to cancel the current async load
super.onCanceled(data);
usuarios = null;
}
}
And I think this snippet is not well done. I'm creating a new Adapter instead of updating the data.
#Override
public void onLoadFinished(Loader<ArrayList<Usuario>> loader,
ArrayList<Usuario> usuarios) {
//adapter.notifyDataSetChanged();
listView.setAdapter(new UsuarioAdapter(this, usuarios));
//((BaseAdapter) listView.getAdapter()).notifyDataSetChanged();
}
Why adapter.notifyDataSetChanged() does not work?
So, basically, my app does not crash but all my data is reloaded again every time I restart the app.
Edit: This is my Adapter code:
class UsuarioAdapter extends BaseAdapter {
private ArrayList<Usuario> usuarios;
private LayoutInflater inflater;
public UsuarioAdapter(Context context, ArrayList<Usuario> usuarios) {
this.usuarios = usuarios;
this.inflater = LayoutInflater.from(context);
}
#Override
public int getCount() { return usuarios.size(); }
#Override
public Object getItem(int pos) { return usuarios.get(pos); }
#Override
public long getItemId(int pos) { return pos; }
#Override
public View getView(int pos, View convertView, ViewGroup arg) {
LinearLayout itemView;
if (convertView == null) {
itemView = (LinearLayout) inflater.inflate(R.layout.list_item, null);
} else {
itemView = (LinearLayout) convertView;
}
ImageView avatar = (ImageView) itemView.findViewById(R.id.avatar);
TextView nombre = (TextView) itemView.findViewById(R.id.nombre);
TextView edad = (TextView)itemView.findViewById(R.id.edad);
// Set the image ... TODO
nombre.setText(usuarios.get(pos).getNombre());
edad.setText(String.valueOf(usuarios.get(pos).getEdad()));
return itemView;
}
}
The call to notifyDataSetChanged() won't change the data your adapter is using. You need to update the data the adapter has, then call that method.
NotifyDataSetChanged() will only tell the adapter it needs to create it's views, but it does not change the data. You need to handle that yourself.
In your adapter add:
public void setUsuario(List<Usuario> usuarios) {
this.usuarios = usuarios;
}
Then in onLoadFinished() call the new method, then notifyDataSetChanged().
listView.getAdapter().setUsuario(usuarios);
listView.getAdapter().notifiyDataSetChanged();
I've found the solution. The onStartLoading was the guilty:
#Override
protected void onStartLoading() {
if (usuarios != null) {
deliverResult(usuarios); // Use cache
} else {
forceLoad();
}
}
In my original post forceLoad was always called. It must be in the else branch.

Categories

Resources