Best practice for eventbus with thread safety - android

My app has activities for the user interaction and a background service which is the only place where the data model is being modified. The background service listens to actions that where made by the user as well as incoming messages from the network. Therefore concurrency issues can arise which I try to prevent by using a handler.
For the event layer I use greenrobots Eventbus.
This is all working well but I wonder if there is a smarter/faster/less code extensive (and therefore less error prone) way to handle this use case?
To be more specific:
Is there a way to ensure serial execution of the onEvent methods
without a handler?
Is there an alternative to having onEvent methods
for each possible event?
Is there a better pattern for what I am
doing here?
This is my approach:
In the oncreate method I do register the service (in case of an activity I do this in onstart)
#Override
public void onCreate() {
super.onCreate();
...
EventBus.getDefault().register(this);
}
And in the onDestroy I do unregister again:
#Override
public void onDestroy() {
super.onDestroy();
....
EventBus.getDefault().unregister(this);
}
Whenever I react to an incoming event I want to ensure serial execution as there can be concurreny issues because there are incoming events from user interactions as well as from other users via networking. So I decided to work with a handler:
private Handler handler = new Handler(){
#Override
public void handleMessage(Message msg) {
Object receivedEvent = msg.obj;
if(receivedEvent instanceof EditUser)
{
processEditUserBroadcast((EditUser)receivedEvent);
}
else if(receivedEvent instanceof JoinParty)
{
processJoinPartyBroadcast((JoinParty)receivedEvent);
}
else if(receivedEvent instanceof LeaveParty)
{
processLeavePartyBroadcast();
}
else if(receivedEvent instanceof SendMessage)
{
processSendMessageBroadcast((SendMessage)receivedEvent);
}
else if(receivedEvent instanceof ReceivedMessage)
{
processReceivedMessageBroadcast((ReceivedMessage)receivedEvent);
}
else if(receivedEvent instanceof Reset)
{
processResetBroadcast();
}
else if(receivedEvent instanceof ImageDownloadFinished)
{
processImageDownloadFinishedBroadcast((ImageDownloadFinished)receivedEvent);
}
}
};
return handler;
}
For each event of interest I do have an onEvent method which is doing nothing but passing the event to the handler to ensure serial execution via a small "passToHandler" helper function
public void passToHandler(Handler handler, Object object)
{
Message message = handler.obtainMessage();
message.obj = object;
handler.sendMessage(message);
}
public void onEvent(EditUser editUser)
{
passToHandler(handler,editUser);
}
public void onEvent(JoinParty joinParty)
{
passToHandler(handler,joinParty);
}
public void onEvent(LeaveParty leaveParty)
{
passToHandler(handler,leaveParty);
}
public void onEvent(SendMessage sendMessage)
{
passToHandler(handler,sendMessage);
}
public void onEvent(ReceivedMessage receivedMessage)
{
passToHandler(handler,receivedMessage);
}
public void onEvent(Reset reset)
{
passToHandler(handler,reset);
}
public void onEvent(ImageDownloadFinished imageDownloadFinished)
{
passToHandler(handler,imageDownloadFinished);
}
The "process.." methods are where the "data magic" happens and shouldn´t be relevant for my question.
And of course for each possible event I did create a class which is usually quite slim like this:
public class JoinParty {
private String partyCode;
public JoinParty(String partyCode) {
super();
this.partyCode = partyCode;
}
public String getPartyCode() {
return partyCode;
}
}

Thank you for posting this Matthias! I think you bring up a very important point about thread safety with GreenRobot EventBus that can easily be missed by users of it.
I think you are quite possibly heading down the right path, though I'm new to GreenRobot EventBus and Android (but not Java). If I read the GreenRobot EventBus source code correctly, one other possible benefit to this approach is that post of the SendMessage event to your onEvent() method immediately returns (after calling sendMessage on the Handler) allowing the EventBus to continue posting it to any other subscribers without delay of the actual processing by your class. This may or may not be what you desire though.
With the approach that you have given, the other thing you need to ensure is that if you take an approach like this that there are no other public methods to your class that has all of your onEvent() methods and the methods such as processEditUserBroadcast(). Otherwise, while you have ensured that all of the processing of the events received from the EventBus are actually handled on a single thread (in a serial manner), some other class might call a public method of this class on a different thread and then cause you thread safety issues again.
If you know that you do need to support other public methods on this class, doing what you have done here at least gets all of the onEvent() methods handling onto a single thread (that of the Looper for the thread that creates the Looper from what I read in the doc for the Looper class) and that simplifies things at least some. You may also then need to apply some synchronization to the public methods and all of the other methods such as processEditUserBroadcast() so as to guarantee safe access to the data members of the class from multiple threads if you are going to have other public methods on this class. Alternatively, depending on what those data members are and what your needs are, you might be able to get by with simply making some of them volatile, atomic, or using the concurrent collections, etc. It all depends on what the read and write access needs are and also the needed granularity of those accesses.
Does this help at all? For those that are well versed with Android, Loopers, Handlers, GreenRobot EventBus, etc. have I misspoken at all?

Related

How to synchronously wait the Result callback from Android to Flutter using the invokeMethod?

I am trying to synchronously call some Flutter methods from Android (Java) using the Flutter MethodChannel.
invokeMethod(String method, #Nullable Object arguments, MethodChannel.Result callback)
I already tried CountDownLatch, locks and even Threads but would just stop after calling the countdown await or synchronize(lock) or even thread.join.
public class MainActivity extends AppCompatActivity {
// code/methods initializations..
public String testData(){
final String[] result = new String[1];
final CountDownLatch latch = new CountDownLatch(1);
flutterMethods.invokeMethod("getString", "abcdefg", new MethodChannel.Result() {
#Override
public void success(Object o) {
result[0] = o.toString();
Log.e(TAG,"success);
latch.countDown();
}
#Override
public void error(String s, String s1, Object o) {
latch.countDown();
}
#Override
public void notImplemented() {
latch.countDown();
}
}
Log.e(TAG,"about to wait forever!");
latch.await();
Log.e(TAG,"done!");
return result[0];
}
Of course, this will work asynchronously without the waits, but I want to make this into a synchronous library function at some point. How can this be done?
This cannot be done.
Flutter architecture is unique in the sense that communication with the OS always asynchronous.
The principle is that Flutter and the native side sends messages to each others by both listening to the same port on the device.
This allows increased performances as there is no "language bridge" but comes at the cost of being forced to be asynchronous.
First of all, this is a really terrible idea and you likely shouldn't do it. Callbacks are fine in 99% of use cases.
If you still really want to though, a simple way would be to just use a while(!messageRecieved) { // do something } style loop. The key important factor here is that you can't actually suspend the thread, because if you do it will be unable to receive any messages.
If you don't want to cause unneeded CPU usage, you could try to do a small IO operation in the body of the loop.

How to use network communication using greenrobot eventbus?

So , I would like to use the following feature mentioned on GreenRobots website,
EventBus can handle threading for you: events can be posted in threads different from the posting thread. A common use case is dealing with UI changes. In Android, UI changes must be done in the UI (main) thread. On the other hand, networking, or any time consuming task, must not run on the main thread.
What I wish to do is, in my android app i would like to create a event which will handle all my networking tasks(sending and receiving data from the server).
How do i exactly do this?
Should i make a network call in the event POJO and then use OnEvent to do post network call tasks.(I dont think this is correct or is it ?)
Edit : Using an event bus for threading may not be the best option because all your OnEvent call will run synchronously one after the other, which may result in blocking of the bus and also its not meant for that. But the answer below is the way it can be done if at all thats a requirement.
I would suggest using an architecture where an event bus might not be required. An event bus is still useful and I think you can find what you are looking for in their getting started guide.
Some example code:
public class EventBusExample extends Activity {
#Override protected void onStart() {
super.onStart();
EventBus.getDefault().register(this);
EventBus.getDefault().post(new BackgroundWorkEvent());
}
#Override protected void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
#Subscribe(threadMode = ThreadMode.ASYNC)
public void doBackgroundWork(BackgroundWorkEvent event) {
// do background work here
// when finished, post to ui thread
EventBus.getDefault().post(new UiWorkEvent());
}
#Subscribe(threadMode = ThreadMode.MAIN)
public void doUiWork(UiWorkEvent event) {
// on main thread. do ui stuff
}
public static class BackgroundWorkEvent {
}
public static class UiWorkEvent {
}
}

Communication between Android Services and Activities

I want to develop an Android App with three activities and two services.
The first Service, named WebClientService, calls a REST API every 30 seconds, using an Handler, and has to notify the active Activity with the result.
It also has to notify a second Service, named DatabaseService, in order to update a local DB.
The Database Service will be called just once onCreate of the activity (in case of app crash and restart) and just once at onRestart (in this way we have data to show in case there were connectivity issues). The activities will then keep themselves updated thanks to the WebClientService that notifies the "alive" activity every 30 seconds.
Questions are:
What's the best way to notify for an update both the active activity and the background DatabaseService?
My idea is to use sendBroadcast() within WebClientService and a BroadcastReceiver in every activity and within the DatabaseService, is it the right approach?
Should I use the same approach for the communication between AllMeetingRoomActivity and DatabaseService or should I use a Bound Service?
Thanks
UPDATE:
DatabaseService won't be a background service anymore but just a shared instance of the db layer between WebClientService and the activities.
So question now is: is it a good approach to just write my 30 seconds updates to the local db and allow the activities to update themselves every few seconds simply reading from the local db?
Would that affect the performance too much?
Context:
Follows what I've implemented so far but using SettableFutures and thus needs to be re-implemented using Services and Broadcasts once I've clear how to make them communicate effectively:
public class MainActivity extends AppCompatActivity {
private TextView meetingsTextView;
private EditText mEdit, editSubject;
private final ConnectorInitializer clientInitializer = new ConnectorInitializer();
private AppConnector genericClient; // can use OutlookClient or a test client to talk with a mock server
#Override
protected void onCreate(Bundle savedInstanceState) {
// initializes client based on the settings in "config.json"
genericClient = clientInitializer.create(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
meetingsTextView = (TextView) findViewById(R.id.NowMeeting);
mEdit = (EditText)findViewById(R.id.editText);
editSubject = (EditText)findViewById(R.id.editSubject);
Futures.addCallback(genericClient.logon(this, scopes), new FutureCallback<Boolean>() {
#Override
public void onSuccess(Boolean result) {
Log.d("APP", "-- Logged in. --");
databaseConnector.synchronouslyGetBackupFromLocalDatabase() // FUTURE
// callback here
// onSuccess, onFailure
}
#Override
public void onFailure(#NonNull Throwable t) {
Log.e("\n ~~~~>> logon \n", t.getMessage());
meetingsTextView.setText(R.string.Login_Failed);
}
});
}
/** At the moment the UI is not updated automatically every 30 seconds
* but manually using a refresh button
*/
public void getBookings(#SuppressWarnings("UnusedParameters") View view){
Log.d("APP", "Retrieve button clicked: "+(DateTime.now())+". Calling async getCalendar.");
meetingsTextView.setText(R.string.retrieving_events);
try{
Futures.addCallback( genericClient.getCalendarEvents(), new FutureCallback<String>(){
#Override
public void onSuccess(final String resultCalendars) {
Log.d("APP", "Success. Result: "+resultCalendars);
runOnUiThread(new Runnable() {
#Override
public void run() {
Log.d("APP", "Calendars SUCCESSFULLY retrieved.");
String meetingsRetrieved = getString(R.string.calendar)+resultCalendars;
meetingsTextView.setText(meetingsRetrieved);
Toast.makeText(getApplicationContext(), "Success!", Toast.LENGTH_LONG).show();
}
});
databaseConnector.asyncUpdateLocalDbWithResults(); // FUTURE
// callback here
// onSuccess, onFailure
}
#Override
public void onFailure(#NonNull Throwable t) {
Log.e( "APP", "Calendar error. Cause: "+t.getLocalizedMessage() );
String retrieveError = "Retrieve error. \n\n\n"+t.getLocalizedMessage();
meetingsTextView.setText(retrieveError);
Toast.makeText(getApplicationContext(), "Fail!", Toast.LENGTH_LONG).show();
}
});
}catch(Exception ex){
Log.e("APP","Something went wrong in your code. Cause:"+ex);
}
}
Best option ever:
Use LocalBroadcastManager. More reference here.
MyService.java:
private LocalBroadcastManager localBroadcastManager;
private final String SERVICE_RESULT = "com.service.result";
private final String SERVICE_MESSAGE = "com.service.message";
#Override
public void onCreate() {
super.onCreate();
// Other stuff
localBroadcastManager = LocalBroadcastManager.getInstance(this);
}
Add below method in service, whenever you want to update data from service to Activity, call method by passing Arguments.
private void sendResult(String message) {
Intent intent = new Intent(SERVICE_RESULT);
if(message != null)
intent.putExtra(SERVICE_MESSAGE, message);
localBroadcastManager.sendBroadcast(intent);
}
HomeActivity.java:
private BroadcastReceiver broadcastReceiver;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.setContentView(R.layout.activity_home);
broadcastReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
String s = intent.getStringExtra(MyService.SERVICE_MESSAGE);
// do something here.
}
};
}
#Override
protected void onStart() {
super.onStart();
LocalBroadcastManager.getInstance(this).registerReceiver((broadcastReceiver),
new IntentFilter(MyService.SERVICE_RESULT));
}
#Override
protected void onStop() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver);
super.onStop();
}
Hope this will help you.
I think your approach is ok with BroadCastReceiver. However, BroadCastReceiver should be used for a global purpose (like communicating between 2 applications). If you intend to use BroadCastReceiver for your app only, I prefer using LocalBroadcastManager instead. Using LocalBroadcastManager is faster and more security when it can be caught only by your app.
There's another way to communicate between your activitys and your services is using EventBus. It will be much easier than using BroadCastReceiver (especially in passing data between them).
Update: About your update question:
is it a good approach to just write my 30 seconds updates to the local db and allow the activities to update themselves every few seconds simply reading from the local db? --> Of course NO. You should let your activities update themselves when they need. When you update your local db, you should know that is there any changes or not. If there is any change, use LocalBroadcastmanager to notify your activity to update.
Would that affect the performance too much? --> Yes, that do. The db connection will take time to execute and it will block your UI in some cases. in that case, you should use a thread with ExecutorService for each execute (insert, update...). One more thing to consider is updating that frequently will drain your phone battery very, very fast.
You can bind the services to the activities and update your UI.
Or you can use libraries like Otto or EventBus to create a publisher/subscriber dependency and notify your activities everytime your services publish an update of information.
Use event bus for this communication. EventBus allows publish-subscribe-style communication between components without requiring the components to explicitly register with one another (and thus be aware of each other). It is designed exclusively to replace traditional Java in-process event distribution using explicit registration.
There are a lot of them:
http://square.github.io/otto/
https://github.com/greenrobot/EventBus
This is an example of Otto usage:
Bus bus = new Bus();
bus.post(new AnswerAvailableEvent(42));
#Subscribe public void answerAvailable(AnswerAvailableEvent event) {
// TODO: React to the event somehow!
}
bus.register(this); // In order to receive events, a class instance needs to register with the bus.
To post from any thread (main or background), in you case a Service and receive events on the main thread:
public class MainThreadBus extends Bus {
private final Handler mHandler = new Handler(Looper.getMainLooper());
#Override
public void post(final Object event) {
if (Looper.myLooper() == Looper.getMainLooper()) {
super.post(event);
} else {
mHandler.post(new Runnable() {
#Override
public void run() {
MainThreadBus.super.post(event);
}
});
}
}

greenrobot EventBus post event in Android

By using EventBus, I need to post an event(MyEvent) in an Activity and receive the event in another Activity in Android. I tried the greenrobot EventBus performance test project but could not get how to do it.
I tried in ActivitySubscriber
MyEvent event = new MyEvent();
EventBus.getDefault().post(event);
and tried to receive the event in ActivityReceiver as
EventBus.getDefault().register(this);
public void onEvent(MyEvent event){
....
}
but I am unable to receive the event. Can anyone let me know where am I doing wrong?
Since they are two activities, ActivitySubscriber posts the event while ActivityReceiver is still not created, or is in stall mode (onStop()). You need to use sticky events, i.e.
ActivitySubscriber.postSticky(...)
And for ActivityReceiver you have two options:
EventBus.getDefault().register(this) and somewhere after that EventBus.getDefault().getStickyEvent()
EventBus.getDefault().registerSticky() and then using regular EventBus.getDefault().onEvent(...)
Updated:
EventBus 3.0 changes the way to subscribe.
There is no need of method names which end up with specific suffixes but rather annotations.
How to use version 3:
//// in your build.gradle
compile 'de.greenrobot:eventbus:3.0.0-beta1'
// alternatively you can target latest whatever currently
// compile 'de.greenrobot:eventbus:+'
//// from a class which needs to dispatch an event
// posting an event is as before, no changes
// here we dispatch a sticky event
EventBus.getDefault().postSticky(myStickyEvent);
//// from your class which needs to listen
// method name can be any name
// any of subscribe params is optional, i.e. can use just #Subscribe
#Subscribe(threadMode = ThreadMode.MainThread, sticky = true, priority = 1)
public void onEventBusEvent(#Nullable final StickyEvent stickyEvent) {
if (stickyEvent != null) {
...
// optionally you can clean your sticky event in different ways
EventBus.getDefault().removeAllStickyEvents();
// EventBus.getDefault().removeStickyEvent(stickyEvent);
// EventBus.getDefault().removeStickyEvent(StickyEvent.class);
}
}
For more details and comparison of version 3:
http://androiddevblog.com/eventbus-3-droidcon/
http://androiddevblog.com/wordpress/wp-content/uploads/EventBus3.pdf
Some details extracted from the sources:
ThreadMode.PostThread
Subscriber will be called in the same thread, which is posting the event. This is the default. Event delivery implies the least overhead because it avoids thread switching completely. Thus this is the recommended mode for simple tasks that are known to complete is a very short time without requiring the main thread. Event handlers using this mode must return quickly to avoid blocking the posting thread, which may be the main thread.
ThreadMode.MainThread
Subscriber will be called in Android's main thread (sometimes referred to as UI thread). If the posting thread is the main thread, event handler methods will be called directly. Event handlers using this mode must return quickly to avoid blocking the main thread.
ThreadMode.BackgroundThread
Subscriber will be called in a background thread. If posting thread is not the main thread, event handler methods will be called directly in the posting thread. If the posting thread is the main thread, EventBus uses a single background thread, that will deliver all its events sequentially. Event handlers using this mode should try to return quickly to avoid blocking the background thread.
ThreadMode.Async
Event handler methods are called in a separate thread. This is always independent from the posting thread and the main thread. Posting events never wait for event handler methods using this mode. Event handler methods should use this mode if their execution might take some time, e.g. for network access. Avoid triggering a large number of long running asynchronous handler methods at the same time to limit the number of concurrent threads. EventBus uses a thread pool to efficiently reuse threads from completed asynchronous event handler notifications.
default values for #Subscribe
threadMode = ThreadMode.PostThread
sticky = false - If true, delivers the most recent sticky event (posted with de.greenrobot.event.EventBus.postSticky(Object) to this subscriber (if event available)
priority = 0 - Subscriber priority to influence the order of event delivery. Within the same delivery thread, higher priority subscribers will receive events before others with a lower priority. The default priority is 0. Note: the priority does NOT affect the order of delivery among subscribers with different thread modes.
Edit 2
There is a dedicated site now for any Greenrobot EventBus questions from the creator of the lib:
http://greenrobot.org/eventbus/
Add
dependencies {
..
compile 'org.greenrobot:eventbus:3.0.0'
..
}
into dependencies section of Modules Build gradle
Create a MessageEvent class like
public final class MessageEvent {
private MessageEvent() {
throw new UnsupportedOperationException("This class is non-instantiable");
}
public static class Message1{
public String str1;
public Message1(String str) {
str1 = str;
}
}
public static class Message2{
public String str2;
public Message2(final String str) {
str2 = str;
}
}
}
// so on
Assume that we have Fragment1 and there is a button that suppose to send messages to MainActivity
public class Fragment1 extends Fragment {
private View frView;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container, Bundle savedInstanceState) {
frView = inflater.inflate(R.layout.fragment1,
container, false);
btn = (Button) frView.findViewById(R.id.button);
btn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
frView.setBackgroundColor(Color.RED);
EventBus.getDefault().post(new MessageEvent.Message1("1st message"));
EventBus.getDefault().post(new MessageEvent.Message2("2nd message"));
}
});
return frView;
}
End finally MainActivity to listen and do action
public class MainActivity extends AppCompatActivity {
#Override
protected void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
#Override
protected void onStop() {
EventBus.getDefault().unregister(this);
super.onStop();
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Fragment1 Fragment1 = new Fragment1();
getFragmentManager().beginTransaction().replace(
R.id.activity_main, Fragment1,
"Fragment 1").commit();
}
#Subscribe(threadMode = ThreadMode.MAIN)
public void onMessage1(MessageEvent.Message1 event) {
Toast.makeText(getApplication(), event.str1,
Toast.LENGTH_LONG).show();
}
#Subscribe(threadMode = ThreadMode.MAIN)
public void onMessage2(MessageEvent.Message2 event) {
Toast.makeText(getApplication(), event.str2,
Toast.LENGTH_LONG).show();
}
}
Inside ActivityReceiver class, replace
EventBus.getDefault().register(this);
with
EventBus.getDefault().register(this, MyEvent.class);
It really depends when and where this code exists. Remember that you must register for the events before you can receive them, and registering takes place at runtime, not compile time.
So, you must ensure that you are posting the event after you have registered the second activity. I would simply put some breakpoints on the following lines and ensure that the debugger stops here:
EventBus.getDefault().register(this);
before you get to here:
EventBus.getDefault().post(event);

synchronous messages from service to UI layer

is the order of a broadcast intent guaranteed? that is, if i do,
sendBroadcast(intent1);
sendBroadcast(intent2);
are the receivers guaranteed to get intent1 before intent2? i suspect the answer to this is no, but in that case, i'm not quite sure how to solve my problem.
i'm trying to create a "busy" indicator for my app that shows busy when the device is talking on the network, and then goes away when the network communication is done. all network communication happens in an intent service.
my attempt at this was to send a BUSY_START intent when i begin network communication in the service, and a BUSY_STOP when network communication ends. this seems to mostly work, but i'm finding occasionally that i get the stop and start messages out of order.
is there a better way to solve this problem?
i'm thinking of adding an ID to each busy intent, so they can be paired. that way if i receive a start for which i've already received a stop, i can ignore it. or, perhaps more simply, add an integer sequence number into each broadcast. if i ever receive a broadcast for which the sequence of the current intent is less than the sequence of the last received intent, ignore it.
Have you considered using a Handler object to communicate from the background thread in the IntentService? The advantage of a Handler over the BroadcastReciver approach is that the Handler uses a message queue to sequence the Message objects.
(I'm assuming your Service is in the same process as the app's main thread).
At least one viable alternative to intents is to execute messaging through the application class, i.e.,
create a listener interface
Manager a collection of listener objects in the application / provide methods to add / remove listener
Interested entities call the application methods to add / remove themselves as listeners
Add "notify" methods in the application, that call the appropriate listener interface method on each of the registered listeners
Services call the application's notification methods to
For example,
public class MyApplication extends Application {
public interface MyListener {
void onEvent();
}
private Set<MyListener> listeners = new HashSet<Listener>();
public void addListener(MyListener l) {
listeners.add(l);
}
public void removeListener(MyListener l) {
listeners.remove(l);
}
public void sendEvent() {
for (MyListener l: listeners) { l.onEvent(); }
}
}
Now, from your activity (or fragment),
public class MyActivity extends Activity implements MyListener {
...
...
...
#Override
public void onEvent() {
// do something
}
#Override
protected void onResume() {
super.onResume();
((MyApplication)getApplication()).addListener(this);
}
#Override
protected void onPause() {
super.onPause();
((MyApplication)getApplication()).removeListener(this);
}
}
And in your service,
((MyApplication)getApplication()).sendEvent();
This provides synchronous messaging without using intents or static variables.

Categories

Resources