Displaying Deezer user playlists in Android app - android

I'm working on a small Android player I have found on github.
I managed to compile the code, but I'm using the 0.10.16 SDK. it seems the player on github was written for a previous version.
I can log in but when I click on Playlists on the home screen and the code bellow executes, I get a blank screen:
private void getUserPlaylists() {
DeezerRequest request = DeezerRequestFactory.requestCurrentUserPlaylists();
AsyncDeezerTask task = new AsyncDeezerTask(mDeezerConnect,
new JsonRequestListener() {
#SuppressWarnings("unchecked")
#Override
public void onResult(final Object result, final Object requestId) {
mPlaylistList.clear();
try {
mPlaylistList.addAll((List<Playlist>) result);
}
catch (ClassCastException e) {
handleError(e);
}
if (mPlaylistList.isEmpty()) {
Toast.makeText(UserPlaylistsActivity.this, getResources()
.getString(R.string.no_results), Toast.LENGTH_LONG).show();
}
mPlaylistAdapter.notifyDataSetChanged();
}
#Override
public void onComplete(final String response, Object requestId) {
//TODO
Toast.makeText(UserPlaylistsActivity.this, "Playlist_onComplete",
Toast.LENGTH_LONG).show();
}
#Override
public void onUnparsedResult(final String response, Object requestId) {
//TODO
}
#Override
public void onException(Exception exception, Object requestId) {
if(exception instanceof OAuthException){
handleError(exception);
}
else if(exception instanceof MalformedURLException){
handleError(exception);
}
else if(exception instanceof IOException){
handleError(exception);
}
else if(exception instanceof DeezerError){
handleError(exception);
}
else if(exception instanceof JSONException){
handleError(exception);
}
else{
//do nothing
}
}
});
task.execute(request);
}
I think the reason is, that the code above was written for the previous SDK version, which apparently worked with "onResult". The latest SDK however works with "onComplete", which returns an unparsed JSON string.
My questions are:
is there a class built into the SDK that will parse the JSON response
is there a class that will accept the parsed response
is there a function that will display this on screen
I was looking through the documentation, but did not find anything useful.
Did anyone implement this with the latest SDK?
EDIT:
private void getUserPlaylists() {
DeezerRequest request = DeezerRequestFactory.requestCurrentUserPlaylists();
AsyncDeezerTask task = new AsyncDeezerTask(mDeezerConnect,
new JsonRequestListener() {
#SuppressWarnings("unchecked")
#Override
public void onResult(final Object result, final Object requestId) {
mPlaylistList.clear();
try {
mPlaylistList.addAll((List<Playlist>) result);
}
catch (ClassCastException e) {
handleError(e);
}
if (mPlaylistList.isEmpty()) {
Toast.makeText(UserPlaylistsActivity.this, getResources()
.getString(R.string.no_results), Toast.LENGTH_LONG).show();
}
mPlaylistAdapter.notifyDataSetChanged();
}
#Override
public void onUnparsedResult(final String response, Object requestId) {
//TODO
}
#Override
public void onException(Exception exception, Object requestId) {
if(exception instanceof OAuthException){
handleError(exception);
}
else if(exception instanceof MalformedURLException){
handleError(exception);
}
else if(exception instanceof IOException){
handleError(exception);
}
else if(exception instanceof DeezerError){
handleError(exception);
}
else if(exception instanceof JSONException){
handleError(exception);
}
else{
//do nothing
}
}
});
task.execute(request);
}
This works now with 0.10.16 SDK. Removed onComplete() and all data is now beeing parsed correctly. Menus are OK, playback is successful.

The issue is that you're overriding the onComplete(String, Object) method. This method is already present in the JsonRequestListener implementation, so you should not rewrite it yourself, at least not without calling super.onComplete(response, requestId).
When overriding the JsonResultListener class, you should only implement the onResult(Object, Object) as you did, the onUnparsedResult(String, Object) method in case the json can't be parsed automatically, and the onException(Exception, Object) in case an exception occurs.

Related

returning subscriber in RxJava after storing data fetch from webservice

I am trying to call the web service to fetch the data and storing it into database using following code. I have created a separate class to perform following operation.
Now , the issue is i want to notify my activity when i successfully fetch and store data in database. if some error occurs then i want to show that on UI itself.
somehow i am able to write a code to fetch the data using pagination but not sure how would i notify UI where i can subscribe catch the update related to progress and error if any.
public Flowable<Response> getFitnessData() {
Request request = new Request();
request.setAccess_token("d80fa6bd6f78cc704104d61146c599bc94b82ca225349ee68762fc6c70d2dcf0");
Flowable<Response> fitnessFlowable = new WebRequest()
.getRemoteClient()
.create(FitnessApi.class)
.getFitnessData("5b238abb4d3590001d9b94a8",request.toMap());
fitnessFlowable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.takeUntil(response->response.getSummary().getNext()!=null)
.subscribe(new Subscriber<Response>() {
#Override
public void onSubscribe(Subscription s) {
s.request(Long.MAX_VALUE);
}
#Override
public void onNext(Response response) {
Log.e(TAG, "onNext" );
if(response !=null){
if(response.getFitness()!=null && response.getFitness().size()!=0){
Realm realm = Realm.getDefaultInstance();
realm.executeTransactionAsync(new Realm.Transaction() {
#Override
public void execute(Realm realm) {
realm.copyToRealmOrUpdate(response.getFitness());
}
}, new Realm.Transaction.OnSuccess() {
#Override
public void onSuccess() {
Log.i(TAG, "onSuccess , fitness data saved");
}
}, new Realm.Transaction.OnError() {
#Override
public void onError(Throwable error) {
Log.i(TAG, "onError , fitness data failed to save"+error.getMessage() );
}
});
}else{
Log.i(TAG, "onError , no fitness data available" );
}
}else{
Log.i(TAG, "onError , response is null" );
}
}
#Override
public void onError(Throwable t) {
Log.e(TAG, "onError" +t.getMessage());
}
#Override
public void onComplete() {
Log.e(TAG, "onComplete");
}
});;
return null;
}
Updated
Created RxBus to propagate events and error on UI
public class RxBus {
private static final RxBus INSTANCE = new RxBus();
private RxBus(){}
private PublishSubject<Object> bus = PublishSubject.create();
public static RxBus getInstance() {
return INSTANCE;
}
public void send(Object o) {
bus.onNext(o);
}
public void error(Throwable e){bus.onError(e);}
public Observable<Object> toObservable() {
return bus;
}
}
in activity
FitnessRepo fitnessRepo = new FitnessRepo();
fitnessRepo.getFitnessData();
RxBus.getInstance().toObservable().subscribe(new Observer<Object>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(Object o) {
if(o instanceof RealmList ){
RealmList<Fitness> realmList = (RealmList<Fitness>) o;
Log.e(TAG,"Fitness data size "+realmList.size());
}
}
#Override
public void onError(Throwable e) {
Log.e(TAG,e.getMessage()+ "");
if (e instanceof HttpException) {
ResponseBody body = ((HttpException) e).response().errorBody();
try {
Response response= LoganSquare.parse(body.byteStream(),Response.class);
if(response.getErrors() !=null)
if(response.getErrors().size() > 0)
Log.e(TAG, "Error "+response.getErrors().get(0).getErrors());
} catch (IOException t) {
t.printStackTrace();
}
}
}
#Override
public void onComplete() {
}
});
in a web service call
public void getFitnessData() {
Request request = new Request();
request.setAccess_token("d80fa6bd6f78cc704104d61146c599bc94b82ca225349ee68762fc6c70d2dcf0");
request.setEnd_date("2018-07-01T00:00:00");
Flowable<Response> fitnessFlowable = new WebRequest()
.getRemoteClient()
.create(FitnessApi.class)
.getFitnessData("5b238abb4d3590001d9b94a8",request.toMap());
fitnessFlowable.subscribeOn(Schedulers.io())
.takeUntil(response->response.getSummary().getNext()!=null)
.doOnNext((response) -> {
if(response ==null || response.getFitness() == null || response.getFitness().isEmpty()) {
Log.e(TAG, " Error ");
return;
}
RxBus.getInstance().send(response.getFitness());
try(Realm r = Realm.getDefaultInstance()) {
r.executeTransaction((realm) -> {
realm.copyToRealmOrUpdate(response.getFitness());
});
}
}).subscribe(item ->{
},
error ->{
RxBus.getInstance().error(error);
});
}
I have good news for you! You can delete almost all of that code and just make it generally better as a result!
public void fetchFitnessData() {
Request request = new Request();
request.setAccess_token("d80fa6bd6f78cc704104d61146c599bc94b82ca225349ee68762fc6c70d2dcf0");
Flowable<Response> fitnessFlowable = new WebRequest()
.getRemoteClient()
.create(FitnessApi.class)
.getFitnessData("5b238abb4d3590001d9b94a8",request.toMap());
fitnessFlowable.subscribeOn(Schedulers.io())
.takeUntil(response->response.getSummary().getNext()!=null)
.doOnNext((response) -> {
if(response ==null || response.getFitness() == null || response.getFitness().isEmpty()) return;
try(Realm r = Realm.getDefaultInstance()) {
r.executeTransaction((realm) -> {
realm.insertOrUpdate(response.getFitness());
});
}
}
}).subscribe();
}
This method is on a background thread now and returns void, so the way to emit stuff out of this method would be to use either a PublishSubject (one for success, one for failure) or an EventBus.
private PublishSubject<Object> fitnessResults;
public Observable<Object> observeFitnessResults() {
return fitnessResults;
}
public static class Success {
public Success(List<Fitness> data) {
this.data = data;
}
public List<Fitness> data;
}
public static class Failure {
public Failure(Exception exception) {
this.exception = exception;
}
public Exception exception;
}
public void fetchFitnessData() {
...
fitnessResults.onNext(new Success(data));
} catch(Exception e) {
fitnessResults.onNext(new Failure(e));
And then
errors = observeFitnessResults().ofType(Error.class);
success = observeFitnessResults().ofType(Success.class);
There are different ways to achieve this. I will never handle the subscriptions on my own out of a lifecycle scope as it creates a possibility of memory leak. In your case it seems that both success and failure is bound to the UI so you can simply do this.
public Completable fetchFitnessData() {
Request request = new Request();
request.setAccess_token("d80fa6bd6f78cc704104d61146c599bc94b82ca225349ee68762fc6c70d2dcf0");
Flowable<Response> fitnessFlowable = new WebRequest()
.getRemoteClient()
.create(FitnessApi.class)
.getFitnessData("5b238abb4d3590001d9b94a8",request.toMap());
return fitnessFlowable.subscribeOn(Schedulers.io())
.takeUntil(response->response.getSummary().getNext()!=null)
.doOnNext((response) -> {
if(response ==null || response.getFitness() == null || response.getFitness().isEmpty()) return;
try(Realm r = Realm.getDefaultInstance()) {
r.executeTransaction((realm) -> {
realm.insertOrUpdate(response.getFitness());
});
}
}
}).ignoreElements();
}
At UI level, you can just handle your subscription with both success and failure. In case you need success model can replace Completable with Single or Flowable.
fetchFitnessData.subscrible(Functions.EMPTY_ACTION, Timber::d);
The major advantage with this approach is that you handle your subscription lifecycles.

RxJava Chaining two Completables

I am very new to RxJava and have a problem. I try to chain two Completables. So the scenario is to update some Category Objects and afterwards update the UI. Both tasks will be executed within a Completable. I chained those two with the andThen operator but sometimes the UI Completable does not get executed and i dont get any error.
Disposable d = FilterController.getInstance().updateChosenCategoriesCompletable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.andThen(FilterController.getInstance().updateChosenCategoriesInterfaceCompletable())
.subscribeWith(new DisposableCompletableObserver() {
#Override
public void onComplete() {
Log.d("MultiselectDialog", "Categories Update Success");
}
#Override
public void onError(Throwable e) {
e.printStackTrace();
ExceptionHandler.saveException(e, null, null);
Log.d("MultiselectDialog", "error");
}
});
I am not sure but i think this is caused by executing the first Completable on the IO Thread. Is this forbidden when i try to chain two Completables? The weird thing about it is that the second Completable does not get execute sometimes (1 out of 10 Times for example). When i insert a delay or do everything on the Main Thread then everything works. I just want to know if its forbidden to chain two Completables with two different threads.
Thanks for your help in advance!
/edit
The updateChosenCategoriesCompletable() is using this Completable:
public Completable setChosenCategoryCompletable(final ArrayList<CategoryHolder> category, final boolean callFilterOptionsChanged) {
return Completable.fromAction(new Action() {
#Override
public void run() throws Exception {
try {
Log.d("Workflow", "setChosenCategoryCompletable " + category.size());
Log.d("Workflow", "setChosenCategoryCompletable " + Thread.currentThread().getName());
mChosenCategories = category;
if (callFilterOptionsChanged) {
mFilterOptionsChangedListener.filterOptionsChanged();
}
} catch (Exception e){
Log.d("Workflow", "error set");
e.printStackTrace();
}
}
});
}
and the updateChosenCategoriesCompletable() is using this Completable
public Completable updateCategoryLabelTextCompletable(final ArrayList<CategoryHolder> categories) {
return Completable.fromAction(new Action() {
#Override
public void run() throws Exception {
Log.d("Workflow", "updateCategoryLabelTextCompletable " + categories.size());
Log.d("Workflow", "updateCategoryLabelTextCompletable " + Thread.currentThread().getName());
TextView selectedValue = mLabels.get(mActivity.getString(R.string.CATEGORY));
ImageView categoryIcon = (ImageView) mRootView.findViewById(R.id.category_icon);
if(categories.size() > 1) {
selectedValue.setText(String.valueOf(categories.size()));
categoryIcon.setVisibility(View.GONE);
}
else if(categories.isEmpty()) {
selectedValue.setText(mActivity.getString(R.string.CATEGORY_EMPTY));
categoryIcon.setVisibility(View.GONE);
}
else {
selectedValue.setText(categories.get(0).getCategory());
// Set category image if only one category is selected
categoryIcon.setVisibility(View.VISIBLE);
String categoryImage = categories.get(0).getCategoryImage();
if(!categoryImage.isEmpty()){
int imageResource = mActivity.getResources().getIdentifier(categoryImage, "drawable", mActivity.getPackageName());
Drawable image = null;
// getDrawable(int) is deprecated since API 21
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
image = mActivity.getResources().getDrawable(imageResource, null);
} else {
image = mActivity.getResources().getDrawable(imageResource);
}
}catch (Exception e){
e.printStackTrace();
}
if(image != null){
categoryIcon.setImageDrawable(image);
}
}else {
categoryIcon.setVisibility(View.GONE);
}
}
}
});
}
Is it mandatory to use fromCallable instead?
This is the method that updates the button i mentioned:
public void setResultButtonText(final String text, final boolean buttonEnabled) {
if(mShowResultsButton != null) {
mActivity.runOnUiThread(new Runnable() {
#Override
public void run() {
try {
mShowResultsButton.setText(text);
mShowResultsButton.setEnabled(buttonEnabled);
if (buttonEnabled) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mShowResultsButton.setBackground(mActivity.getDrawable(R.drawable.ripple));
} else {
mShowResultsButton.setBackground(mActivity.getResources().getDrawable(R.drawable.ripple));
}
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mShowResultsButton.setBackground(mActivity.getDrawable(R.drawable.ripple_grey));
} else {
mShowResultsButton.setBackground(mActivity.getResources().getDrawable(R.drawable.ripple_grey));
}
}
} catch (Exception e){
e.printStackTrace();
Log.d("Filterinterface", "ERROR");
}
}
});
}
}

eventbus inside recursive function

i am posting event using EventBus inside a recursive function that fetches pagination data from the webservice.
public void getCallsData(final UserRequest userRequest){
serviceCall.enqueue(new Callback<UserResponseInfo>() {
#Override
public void onResponse(Call<UserResponseInfo> call, Response<UserResponseInfo> response) {
if(response.isSuccessful()) {
UserResponseInfo userResponseInfo = response.body();
if (userResponseInfo != null) {
try {
Log.e(TAG, "getCallsData response " + LoganSquare.serialize(userResponseInfo));
} catch (IOException e) {
e.printStackTrace();
}
int currentPage = userRequest.getUserRequestInfo().get(0).getPage();
int totalPages = userResponseInfo.getTotalPages();
if(currentPage < totalPages){
userRequest.getUserRequestInfo().get(0).setPage(++currentPage);
Log.e(TAG, "getCallsData fetching next page "+currentPage);
userResponseInfo.setCurrentPage(currentPage);
userResponseInfo.setRequestType(GET_CALL_REQUEST);
EventBus.getDefault().postSticky(userResponseInfo);
getCallsData(userRequest);
}
} else {
}
}else{
}
}
#Override
public void onFailure(Call<UserResponseInfo> call, Throwable t) {
}
});
}
The issue is EventBus.getDefault().postSticky(userResponseInfo); when getCall executes in recursive fashion its not posting the event properly as in only first event is getting called and it misses the last one.

Retrofit Periodic call with Pagination

I am currently using the Retrofit2.0 to poll the server .I am getting the result in x second but the problem is page number is not updating in the API.Lets come to the code for better clarification
private void startPolling() throws Exception {
Log.e("APP CONSTANT","" + current_page);
MrSaferWebService service = ServiceFactory.createRetrofitService(MrSaferWebService.class, AppConstants.BASE_URL);
final Observable<ReportResponse> reportResponseObservable = service.getListOfInciden("get_report", current_page, 5, incident_lat, incident_long);
Observable.interval(0,20,TimeUnit.SECONDS)
.flatMap(new Func1<Long, Observable<ReportResponse>> () {
#Override
public Observable<ReportResponse> call(Long aLong) {
return reportResponseObservable;
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<ReportResponse>() {
#Override
public void call(ReportResponse response) {
Log.i("HEARTBEAT_INTERVAL", "Response from HEARTBEAT");
ActivityUtils.showProgress(false, mRootView, mProgressView, mContext);
if (response.getStatus() == 1) {
current_page = current_page + 1;
if (!response.getReportList().isEmpty()) {
addItems(response.getReportList());
}
else{
//do nothing
}
} else {
Log.e("MY ERROR", "" + "SOME ERROR OCCURED");
}
}
}, new Action1<Throwable>() {
#Override
public void call(Throwable throwable) {
ActivityUtils.showProgress(true, mRootView, mProgressView, mContext);
// TODO: 22/03/16 ADD ERROR HANDLING
}
});
}
As you can see i have incremented the current_page by 1 every time on
SuccessFull Response but when i check the Log the current_page value are increased only once and after that log are not there and hence there value is also not increasing..So it taking the the same page number every time and giving me the Duplicate response.
Please help me to find what i am missing.
After spending more than a day i just changed Action with Subscriber and everything seems to be working .I don't know what happen internally but it works . I am still trying to figure it out what the difference between Action and Subscriber.
Below are my updated code which did the tricks.
private void startPolling() throws Exception {
final MrSaferWebService service = ServiceFactory.createRetrofitService(MrSaferWebService.class, AppConstants.BASE_URL);
Observable
.interval(0,20,TimeUnit.SECONDS)
.flatMap(new Func1<Long, Observable<ReportResponse>>() {
#Override
public Observable<ReportResponse> call(Long aLong) {
Log.e("PAGE", "" + current_page);
return service.getListOfInciden("get_report", current_page, 5, incident_lat, incident_long);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<ReportResponse>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
e.printStackTrace();
if (mProgressView !=null && mRootView !=null) {
ActivityUtils.showProgress(false, mRootView, mProgressView, mContext);
}
}
#Override
public void onNext(ReportResponse response) {
if (mProgressView !=null && mRootView !=null) {
ActivityUtils.showProgress(false, mRootView, mProgressView, mContext);
}
if (response.getStatus() == 1) {
if (!response.getReportList().isEmpty()){
current_page = current_page + 1;
addItems(response.getReportList());
}
else{
//do nothing
}
} else {
Log.e("MY ERROR", "" + "SOME ERROR OCCURED");
}
}
});
}

Retrofit 2.0 + RxJava + Error JSON body

I'm pretty new to RxJava and Retrofit and am trying to write my API calls with it. All the API calls return a JSON body on error which is in the general format as,
{"errors":[{"code":100, "message":"Login/Password not valid", "arguments":null}]}
Currently my code for the login API call (others are also similar) is,
mConnect.login(id, password)
.subscribe(new Subscriber<Token>() {
#Override
public void onCompleted() {
Log.d(TAG, "onCompleted()");
}
#Override
public void onError(Throwable e) {
Log.e(TAG, "onError(): " + e);
if (e instanceof HttpException) {
// dump e.response().errorBody()
}
}
#Override
public void onNext(Token token) {
Log.d(TAG, "onNext(): " + token);
}
});
When I get an error at the onError(), I would like to automatically decode the JSON in the error body to a POJO instead and use that. Is there a way to do this preferably in one place for all other API calls. Any help is appreciated.
I would suggest the use of a reusable Transformer along with the onErrorResumeNext operator to encapsulate your logic. It'd look something like this:
<T> Observable.Transformer<T, T> parseHttpErrors() {
return new Observable.Transformer<T, T>() {
#Override
public Observable<T> call(Observable<T> observable) {
return observable.onErrorResumeNext(new Func1<Throwable, Observable<? extends T>>() {
#Override
public Observable<? extends T> call(Throwable throwable) {
if (throwable instanceof HttpException) {
HttpErrorPojo errorPojo = // deserialize throwable.response().errorBody();
// Here you have two options, one is report this pojo back as error (onError() will be called),
return Observable.error(errorPojo); // in this case HttpErrorPojo would need to inherit from Throwable
// or report this pojo back as part of onNext()
return Observable.just(errorPojo); //in this case HttpErrorPojo would need to inherit from <T>
}
// if not the kind we're interested in, then just report the same error to onError()
return Observable.error(throwable);
}
});
}
};
}
Pay attention to the comments in the code, since you have to make the decision whether you want to report the parsed response onError() or onNext().
Then you can use this transformer anywhere in your API calls like this:
mConnect.login(id, password)
.compose(this.<Token>parseHttpErrors()) // <-- HERE
.subscribe(new Subscriber<Token>() {
#Override
public void onCompleted() {
Log.d(TAG, "onCompleted()");
}
#Override
public void onError(Throwable e) {
Log.e(TAG, "onError(): " + e);
if (e instanceof HttpErrorPojo) {
// this will be called if errorPojo was reported via Observable.error()
}
}
#Override
public void onNext(Token token) {
Log.d(TAG, "onNext(): " + token);
if (token instanceof HttpErrorPojo) {
// this will be called if errorPojo was reported via Observable.just()
}
}
});
Deserialize may be an issue too. You can use the retrofit converter to deserialize it (or do it yourself).
My solution adds a bit to the one from murki:
<T> Observable.Transformer<T, T> parseHttpErrors() {
return new Observable.Transformer<T, T>() {
#Override
public Observable<T> call(Observable<T> observable) {
return observable.onErrorResumeNext(new Func1<Throwable, Observable<? extends T>>() {
#Override
public Observable<? extends T> call(Throwable throwable) {
if ( throwable instanceof HttpException ) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(SERVER_URL) // write your url here
.addConverterFactory(GsonConverterFactory.create())
.build();
Converter<ResponseBody, Error> errorConverter =
retrofit.responseBodyConverter(Error.class, new Annotation[0]);
// Convert the error body into our Error type.
try {
Error error = errorConverter.convert(((HttpException) throwable).response().errorBody());
// Here you have two options, one is report this pojo back as error (onError() will be called),
return Observable.error(new Throwable(error.getMessage()));
}
catch (Exception e2) {
return Observable.error(new Throwable());
}
}
// if not the kind we're interested in, then just report the same error to onError()
return Observable.error(throwable);
}
});
}
};
}
and then at the onError(),
#Override
public void onError(Throwable e) {
progressBar.setVisibility(View.GONE); // optional
if ( !TextUtils.isEmpty(e.getMessage()) ) {
// show error as you like
return;
}
// show a default error if you wish
}

Categories

Resources