Access a database from a DialogFragment and toast a message - android

I have a DialogFragment which displays a simple yes/no question.
When the user presses "yes", I perform a database request (which basicaly deletes an entry).
I then toast a message to report a success or failure to the user.
I try to avoid calling the database from the UI thread, so I created a thread which will delete the entry, and from that thread I call a handler in the DialogFragment to display the toast message.
My problem is that when the user presses the button, the thread is started and the dialog is closed. As the thread is started, the data is deleted from the database. But when I toast my message from the handler, the DialogFragment is already detached from the parent Activity so I don't have a context anymore to call Toast.makeText().
My question is how can I toast the message ?
I know I could create a Service to handle the database operation, but wouldn't it be too much hassle ?
Is there a simpler way ?
Thanks !
EDIT : here is my code, to help you understand my problem :
public class EraseHistoryDialogFragment extends DialogFragment {
private HistoryDatabaseHandler mHistoryDbHandler;
private final static int MSG_NOTIFY_EMPTYDB = 1;
private final static int MSG_NOTIFY_DELENTRY = 2;
private final static int MSG_NOTIFY_NODELETION = 3;
private Context mContext;
private Handler mHandler = new Handler() {
#Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_NOTIFY_DELENTRY:
Toast.makeText(mContext,
getS tring(R.string.historytab_erased_entry),
Toast.LENGTH_SHORT).show();
break;
case MSG_NOTIFY_EMPTYDB:
Toast.makeText(mContext,
getS tring(R.string.historytab_history_cleared),
Toast.LENGTH_SHORT).show();
break;
case MSG_NOTIFY_NODELETION:
Toast.makeText(mContext,
getS tring(R.string.historytab_erase_failed),
Toast.LENGTH_SHORT).show();
break;
}
};
};
private Runnable mEraseHistoryRunnable = new Runnable() {
#Override
public void run() {
if (mHistoryDbHandler.clearAllTables()) {
mHandler.sendEmptyMessage(MSG_NOTIFY_EMPTYDB);
} else {
mHandler.sendEmptyMessage(MSG_NOTIFY_NODELETION);
}
}
};
private class EraseEntryRunnable implements Runnable {
private String mEntryId;
public EraseEntryRunnable(String entryID) {
mEntryId = entryID;
}
#Override
public void run() {
if (mHistoryDbHandler.deleteEntry(mEntryId)) {
mHandler.sendEmptyMessage(MSG_NOTIFY_DELENTRY);
} else {
mHandler.sendEmptyMessage(MSG_NOTIFY_NODELETION);
}
}
};
public static EraseHistoryDialogFragment newInstance(String message,
String entryID, boolean eraseAll) {
EraseHistoryDialogFragment frag = new EraseHistoryDialogFragment();
Bundle args = new Bundle();
args.putString("message", message);
args.putString("entryid", entryID);
args.putBoolean("eraseall", eraseAll);
frag.setArguments(args);
return frag;
}
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
mHistoryDbHandler = HistoryDatabaseHandler.getInstance(getActivity());
mContext = getActivity().getApplicationContext();
String message = getArguments().getString("message");
final String entryID = getArguments().getString("entryid");
final boolean eraseAll = getArguments().getBoolean("eraseall");
return new AlertDialog.Builder(getActivity())
.setMessage(message)
.setPositiveButton(R.string.groupstab_yes,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
if (eraseAll) {
Thread emptyDbT = new Thread(
mEraseHistoryRunnable);
emptyDbT.start();
} else {
Thread deleteEntryT = new Thread(
new EraseEntryRunnable(entryID));
deleteEntryT.start();
}
}
})
.setNegativeButton(R.string.groupstab_no,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
getDialog().dismiss();
}
}).create();
}
}

try with getActivity(); instead of getApplicationContext();
like this
Toast.makeText(getActivity(), "Your Message", Toast.LENGTH_SHORT).show();

Try getActivity().getApplicationContext() to get the ApplicationContext

Related

Unable to dismiss dialog in runOnUiThread

I'm scanning QR code using Google's Vision. I got help from this link. I want to show AlertDialog after getting the value from QR code. If I directly show alertdialog inside receiveDetections() method I'm getting "Can't create looper" error. So I called the alertdialog inside runOnUiThread(). But now I'm unable to dismiss the dialog. Would like to know what's the reason for this.
#Override
public void receiveDetections(Detector.Detections<Barcode> detections){
final SparseArray<Barcode> barcodes = detections.getDetectedItems();
if (barcodes.size() != 0) {
runOnUiThread(new Runnable() {
#Override
public void run() {
try {
String val = barcodes.valueAt(0).displayValue;
showAlert(val);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
private void showAlert(String val){
try {
if (!val.equals("")) {
AlertDialog.Builder builder = new AlertDialog.Builder(ScannedBarcodeActivity.this);
builder.setTitle(emp.getName())
.setPositiveButton(newStatus, new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
emp.setStatus(newStatus);
viewModel.updateEmployee(emp);
dialog.dismiss();
}
})
.setNegativeButton("CANCEL", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
AlertDialog alertDialog = builder.create();
alertDialog.show();
}
}catch (Exception e){
e.printStackTrace();
}
}
Even though now the alertdialog is showing, when I click CANCEL button, the alert does not gets dismissed.
I think receiveDetections is being called more than once, which is resulting in multiple instances of AlertDialog. So when you press CANCEL button you might be cancelling only one of those multiple opened dialogs.
You can add logs to see how many times receiveDetections is being called, or just hit CANCEL button multiple times and all the previously opened alerts will be cancelled
*Possible Reason:
receiveDetections will be executed every-time your camera passes image to QRScanner,
and as your camera is continuously streaming images, QRScanner is calling receiveDetections again and again.
The reason why you alert dialog is not dismissing because you are new object of AlertDialog each time when receiveDetections called and in your case receiveDetections multiple time.
You have to create a Singleton class object:
public class singleTonDialogExample {
static TextToSpeech t1;
private static singleTonExample ourInstance = new singleTonDialogExample();
private Context appContext;
private ICallBack iCallBack;
public interface ICallBack{
void onYesPressed();
void onNoPressed();
}
private singleTonDialogExample() { }
public static Context get() {
return getInstance().getContext();
}
public static synchronized singleTonDialogExample getInstance() {
return ourInstance;
}
public void init(Context context) {
if (appContext = = null) {
this.appContext = context;
}
}
private Context getContext() {
return appContext;
}
public void setICallBack(ICallBack callBack){
this.iCallBack=callBack;
}
public void AlertDialog(final MainActivity mainActivity,String title,String message) {
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(mainActivity);
alertDialogBuilder.setMessage(message);
alertDialogBuilder.setPositiveButton("yes",
new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface arg0, int arg1) {
iCallBack.onYesPressed();
}
});
alertDialogBuilder.setNegativeButton("No", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
iCallBack.onNoPressed();
}
});
AlertDialog alertDialog = alertDialogBuilder.create();
alertDialog.show();
}
}
Create the Dialog in Activity like this:
singleTonDialogExample dialog;
dialog = singleTonExample.getInstance();
dialog.init(getApplicationContext());
dialog.setICallBack(this);
dialog.AlertDialog(MainActivity.this,"Title","This is message");
Also implement the ICallBack interface in your activity or fragment.

Activity objects persisting in memory even after calling finish and making the references null

I am making a retrofit enqueue call from SplashActivity and passing data to next activity via bundles.Even after explicitly calling finish on splash activity it still has 48 references in retained heap as seen in MAT.
The app in general is also taking a lot of memory in the background. I debugged and found that my activity's onDestroy() method is getting called, but why are the objects still persistent in the memory? Is it somehow related to the data being passed by reference instead of value? I've been trying to search around the web but with hardly any luck. Please point me if I'm looking in the wrong direction here.
Here's a snapshot of my Splash Activity-
public class SplashActivity extends AppCompatActivity {
private String TAG = SplashActivity.class.getSimpleName();
List<Sport> mSportsList;
List<Event> mEventsList;
List<Carousel> mCarouselList;
WatchOnApiService mWatchOnApiService;
#BindString(R.string.package_name)
String mPackageName;
ProgressBar mProgressBar;
AlertDialog.Builder alertDialogBuilder;
AlertDialog alertDialog;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
ButterKnife.bind(this);
MobileAds.initialize(getApplicationContext(), "ca-app-pub-8433136449848959~7585529227");
mProgressBar = (ProgressBar) findViewById(R.id.progressBar);
mProgressBar.getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY);
}
#Override
protected void onResume() {
super.onResume();
getSportsAndEventsAndCarousel();
}
private void getSportsAndEventsAndCarousel() {
mProgressBar.setVisibility(View.VISIBLE);
WatchOnApiService mWatchOnApiService = RetrofitFactory.getRetrofit().create(WatchOnApiService.class);
final Call<WatchOnSportsEventsCarousel> mCall = mWatchOnApiService.getAllSportsAndEventsAndCarousel(TimeZone.getDefault().getID());
mCall.enqueue(new Callback<WatchOnSportsEventsCarousel>() {
#Override
public void onResponse(final Response<WatchOnSportsEventsCarousel> response, Retrofit retrofit)
{
if (response.isSuccess()) {
mProgressBar.setVisibility(View.INVISIBLE);
if (Float.parseFloat(BuildConfig.VERSION_NAME) < Float.parseFloat(response.body().getAndroidLatestVersion())) {
if (response.body().getAndroidForceUpdate().equalsIgnoreCase("1")) {
showForceUpdateDialog();
} else {
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(SplashActivity.this);
alertDialogBuilder.setTitle("WatchOn");
alertDialogBuilder.setMessage("WatchOn just got better.Would you like to upgrade ?");
alertDialogBuilder.setPositiveButton("Upgrade",
new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int arg1) {
dialog.dismiss();
try { SplashActivity.this.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + mPackageName)));
} catch (android.content.ActivityNotFoundException anfe) {
SplashActivity.this.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + mPackageName)));
}
}
});
alertDialogBuilder.setNegativeButton("Dismiss", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
beginFlow(response);
}
});
AlertDialog alertDialog = alertDialogBuilder.create();
alertDialog.show();
}
} else {
beginFlow(response);
}
}
//Response not received something went wrong
else {
showAlertDialogNoResponse();
}
}
private void beginFlow(final Response<WatchOnSportsEventsCarousel> response) {
Bundle mBundle = new Bundle();
mSportsList = CodeUtils.convertSportsListToLocalTime(response.body().getData().getSports());
Parcelable wrappedSports = Parcels.wrap(new ArrayList<>(mSportsList));
mBundle.putParcelable(AppConstants.SPORTS_LIST, wrappedSports);
mEventsList = CodeUtils.convertEventsListToLocalTime(response.body().getData().getEvents());
Parcelable wrappedEvents = Parcels.wrap(new ArrayList<>(mEventsList));
mBundle.putParcelable(AppConstants.EVENTS_LIST, wrappedEvents);
mCarouselList = response.body().getData().getCarousels();
Parcelable wrappedCarousels = Parcels.wrap(new ArrayList<>(mCarouselList));
mBundle.putParcelable(AppConstants.CAROUSELS_LIST, wrappedCarousels);
if ((response.body().getLiveSponsors() == 1)) {
mBundle.putBoolean(AppConstants.LIVE_SPONSORS_AVAILABLE, true);
} else {
mBundle.putBoolean(AppConstants.LIVE_SPONSORS_AVAILABLE, false);
}
Intent i = new Intent(SplashActivity.this, SelectSportEventActivity.class);
i.putExtras(mBundle);
mSportsList = null;
mEventsList = null;
mCarouselList = null;
startActivity(i);
SplashActivity.this.finish();
}
#Override
public void onFailure(Throwable t) {
showAlertDialogInternetFail();
alertDialogBuilder.setNegativeButton("Retry", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
getSportsAndEventsAndCarousel();
dialog.dismiss();
}
});
alertDialog = alertDialogBuilder.create();
alertDialog.show();
}
});
}
#Override
protected void onDestroy() {
ButterKnife.unbind(this);
Log.d(TAG, "onDestroy: " + mSportsList + mEventsList + mCarouselList);
alertDialog = null;
super.onDestroy();
}
}

Android testing that an alertDialog is shown in an activity

Hello and thank you for taking the time to read this question.
I am trying to write an instrumentation test for an activity. The scenario is the following:
If on create of the activity the GPS is not enabled, an alertDialog should be shown to the user to suggest turning on the sensor. The check is performed by an utility class that checks the connectivity and shows the alertDialog if necessary. I want to be able to test in my class if the dialog is shown to the user.
Now for some code:
The activity:
#Override
protected void onCreate(Bundle savedInstanceState) {
...
Utils.checkGPSProvider(this);
}
The Utility class:
public final class Utils {
private Utils() {
}
private static void createAlertDialog(final Context context, final String message, final String intentAction) {
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(context);
final Intent callSettingIntent = new Intent(intentAction);
alertDialogBuilder.setMessage(message).setPositiveButton(POSITIVE_BUTTON,
new DialogInterface.OnClickListener() {
#Override
public void onClick(final DialogInterface dialog, final int id) {
context.startActivity(callSettingIntent);
}
});
alertDialogBuilder.setNegativeButton(NEGATIVE_BUTTON, new DialogInterface.OnClickListener() {
#Override
public void onClick(final DialogInterface dialog, final int id) {
dialog.cancel();
}
});
final AlertDialog alert = alertDialogBuilder.create();
alert.show();
}
public static void checkGPSProvider(final Context context) {
String message;
message = "GPS message";
final LocationManager mlocManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
if (!mlocManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
createAlertDialog(context, message, android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
}
}
}
The test class:
public class UtilsTest extends ActivityInstrumentationTestCase2<MyActivity> {
private MyActivity activity;
#Mock
LocationManager mlocManager;
public UtilsTest() {
super(MyActivity.class);
}
public UtilsTest(Class<MyActivity> activityClass) {
super(activityClass);
}
#Override
public void setUp() throws Exception {
super.setUp();
activity = getActivity();
MockitoAnnotations.initMocks(this);
}
public void testWhenGPSIsDisabled_ShouldShowAlertDialog() {
when(mlocManager.isProviderEnabled(LocationManager.GPS_PROVIDER)).thenReturn(false);
if (mlocManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
Assert.fail("GPS should be disabled");
}
(insert code here that should test that the alertDialog is shown)
}
}
I know that the Dialog class has an isShown() method but I do not know how to get the dialog reference to test the isShown method.
If there is any other necessary information I will try to provide it to you.
Thank you.
You can return the reference of the AlertDialog from the method createAlertDialog
example:
private static AlertDialog createAlertDialog(final Context context, final String message, final String intentAction)
When you call this method you can then get the return reference value of the method same as your checkGPSProvider should also return AlertDialog.
sample:
public static AlertDialog checkGPSProvider(final Context context) {
String message;
message = "GPS message";
final LocationManager mlocManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
if (!mlocManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
return createAlertDialog(context, message, android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
}
}
And in your oncreate method of the activity create a Field for AlertDialog and instantiate with the Utils.checkGPSProvider(this);
solution:
your_alert_dialog = Utils.checkGPSProvider(this);

acces of local final variable in inner class broken

I got a method in which server-client communication is done "onClick" therefor i create a anonymous OnClickListener, and I want to publish a toast if the communication was successfull or not.
To do this I need the Acitivity in which context to publish the toast, and as I externalized the method, it must be given as a "this" argument to the Activity. But as I am inside an anonymous inner class I cannot access the this pointer of the Acitivity, and even though I stored it in a local final variable
private final Activity activity = this;
#Override
public void onCreate(Bundle savedInstanceState) {
lastResult = null;
super.onCreate(savedInstanceState);
setLayout(R.layout.main);
qrscan = (Button) findViewById(R.id.qrcodescan);
qrscan.setOnClickListener( new View.OnClickListener() {
public void onClick(View view) {
initiateScan(activity);
}
}
);
}
private AlertDialog initiateSend(Activity activity) {
if(lastResult != null) {
String[] arr = lastResult.content.split("/");
AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity);
String[] args = Util.filterString(arr,this);
downloadDialog.setTitle(args[0]);
downloadDialog.setMessage("Auftragsnummer:" + args[1]);
downloadDialog.setPositiveButton(getString(R.string.ja), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialogInterface, int i) {
try {
String send = lastResult.content;
send += "/uid/" + R.id.username + "/cid/" + R.id.password;
String result = Util.send(send);
//toaster(send);
Util.toaster(result,activity);
if(!(result.equals("OK") || result.equals("ok") || result.equals("Ok")))
throw new Exception("Bad Server Answer");
Util.toaster("Communication erfolgreich",activity);
} catch(Exception ex) {
ex.printStackTrace();
Util.toaster("Communication nicht erfolgreich",activity);
}
}
});
downloadDialog.setNegativeButton(getString(R.string.nein), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface arg0, int arg1) {}
});
return downloadDialog.show();
}
return null;
}
Any clue what i messed up?
declare variable before onCreate() like this
public class HelloAndroid extends Activity {
Activity activity = this; // declare here
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}
EDITED
Activity mainActivity;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setLayout(R.layout.main);
mainActivity = this;
lastResult = null;
qrscan = (Button) findViewById(R.id.qrcodescan);
qrscan.setOnClickListener( new View.OnClickListener() {
public void onClick(View view) {
initiateScan(mainActivity);
}
}
);
}
private AlertDialog initiateSend(final Activity activity) {
if(lastResult != null) {
String[] arr = lastResult.content.split("/");
AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity);
String[] args = Util.filterString(arr,this);
downloadDialog.setTitle(args[0]);
downloadDialog.setMessage("Auftragsnummer:" + args[1]);
downloadDialog.setPositiveButton(getString(R.string.ja), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialogInterface, int i) {
try {
String send = lastResult.content;
send += "/uid/" + R.id.username + "/cid/" + R.id.password;
String result = Util.send(send);
//toaster(send);
Util.toaster(result,activity);
if(!(result.equals("OK") || result.equals("ok") || result.equals("Ok")))
throw new Exception("Bad Server Answer");
Util.toaster("Communication erfolgreich",activity);
} catch(Exception ex) {
ex.printStackTrace();
Util.toaster("Communication nicht erfolgreich",activity);
}
}
});
downloadDialog.setNegativeButton(getString(R.string.nein), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface arg0, int arg1) {}
});
return downloadDialog.show();
}
return null;
}

android: displaying progress dialog when waiting for connection

I am trying to add a progress dialog when a new activity is launched that has to wait for a response from the internet. At the moment the screen just goes black while it is waiting. Does any one know where it needs to be placed to work?
this progressDialog:
ProgressDialog dialog = ProgressDialog.show(SearchActivity.this, "", "Loading. Please wait...", true);
dialog.dismiss();
this is in the overlayActivity extends ItemizedOverlay:
#Override
protected boolean onTap(int index) {
final OverlayItem item = (OverlayItem) items.get(index);
final Context mContext = context;
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setTitle(item.getTitle())
.setCancelable(true)
.setPositiveButton("View Details", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Intent intent = new Intent(mContext, Profile.class);
intent.putExtra("id", item.getSnippet());
mContext.startActivity(intent);
}
});
AlertDialog alert = builder.create();
alert.show();
return true;
}
and this is the Profile activity:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.profile);
Bundle extras = getIntent().getExtras();
String id;
if (extras != null) {
id = extras.getString("id");
String xml = XMLfunctions.getXMLFromBarId(id); // makes httpPost call
Document doc = XMLfunctions.XMLfromString(xml);
NodeList nodes = doc.getElementsByTagName("result");
Element e = (Element)nodes.item(0);
// rest of profile created here
}
}
You should use Progress dialog. Progress dialog should be used in the Profile activity.
You can use the following code:
final ProgressDialog dialog = ProgressDialog.show(MyProfileActivity.this, "","Loading..Wait.." , true);
dialog.show();
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
public void run() {
//your code here
dialog.dismiss();
}
}, 3000); // 3000 milliseconds
Doing network calls in the UI thread (the thread which calls "onCreate") is a bad idea. It will stall the refresh of the UI till the network operation is completed. Instead, spawn a new thread in onCreate like so:
Thread networkThread = new Thread() {
public void run() {
String xml = XMLfunctions.getXMLFromBarId(id); // makes httpPost call
Document doc = XMLfunctions.XMLfromString(xml);
NodeList nodes = doc.getElementsByTagName("result");
Element e = (Element)nodes.item(0);
....
}
}
networkThread.start();
Also, I'd recommend using a ProgressDialog to show progress (which you can dismiss, once the code in the thread is done). Tutorial: http://developer.android.com/guide/topics/ui/dialogs.html
Note: You cannot dismiss the dialog from the new thread, so you will have to use a Handler to post a message from the thread to the UI thread. Here a tutorial for that: http://www.tutorialforandroid.com/2009/01/using-handler-in-android.html
Example:
In your Profile activity class, add this:
class ProfileActivity extends Activity {
class ProfileHandler extends Handler {
private ProfileActivity parent;
public ProfileHandler(ProfileActivity parent) {
this.parent = parent;
}
public void handleMessage(Message msg) {
parent.handleMessage(msg);
}
}
private ProfileHandler handler;
public void onCreate(Bundle savedInstanceState) {
handler = new ProfileHandler(this);
Thread networkThread = new Thread() {
public void run() {
String xml = XMLfunctions.getXMLFromBarId(id); // makes httpPost call
Document doc = XMLfunctions.XMLfromString(xml);
NodeList nodes = doc.getElementsByTagName("result");
Element e = (Element)nodes.item(0);
....
ProfileActivity.this.handler.sendEmptyMessage(0);
}
}
networkThread.start();
}
public void handleMessage(msg) {
switch(msg.what) {
case 0:
// Update UI here
break;
}
}
}

Categories

Resources