I'm trying to get notifications from a bluetooth device sending records, and periodically update the UI to display the records. The while loop below sits in its own thread to handle UI updates while the rest of the module takes care of other tasks. gattCallback is an instance of a BluetoothGattCallback class that adds to a list of received records and returns that list when getHistory() is called.
My problem is that when I hit the foreach line, after so many iterations I get an error:
System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
As far as I can tell, history isn't being updated here or anywhere else in my code so I'm confused by the error. I specifically retrieve a copy of the record history through getHistory() to avoid modifying it during the foreach. Can anyone suggest what might be causing it or any tips to find out?
It might be relevant that this has only caused issues since switching to a Moto E4 on Android 7.1.1 from a Moto G Play on Android 6.0.1.
// Periodically check to see what needs updating
while (!finishedDisplayThread)
{
// See if there are any new records to display
int count;
List<Record> history = gattCallback.getHistory();
if (history == null)
{
count = 0;
}
else
{
count = history.Count;
}
// Only update the display if it has changed
if(count != prevCount)
{
prevCount = count;
List<string> recordList = new List<string>();
if (history == null)
{
recordList = new List<string>();
recordList.Add("No history.");
}
else
{
foreach (Record record in history)
{
recordList.Add(record.ToRow());
}
}
//Update the display
RunOnUiThread(() =>
{
ListAdapter = new ArrayAdapter<string>(this,
Resource.Layout.ListItemLayout,
recordList);
recordCountText.Text = "" + count;
});
}
Thread.Sleep(100);
}
I specifically retrieve a copy of the record history through getHistory() to avoid modifying it during the foreach.
Are you certain that you're getting a copy? Imagine this implementation:
public List<Record> getHistory() {
return history;
}
This will not return a copy of history, but a reference directly to history itself. If two different places call this method, any changes to one of the returned values will affect the other returned value.
If you want a copy, you have to do something like this:
public List<Record> getHistory() {
return new ArrayList<>(history);
}
Related
I have a service scheduling screen.
But there is a problem:
I need to check if the date and time the user is trying to schedule is available or reserved.
Structure DB:
Companies
-Company ID (Document)
--name
--phone
---Schedules (Collection)
------Event1
--------Hour: 08:30
--------Date: 01/01/2018
------Event2
--------Hour: 09:00
--------Date: 05/01/2018
------Event3
--------Hour: 10:30
--------Date: 01/002/2018
I access Scheduling data with this code:
String dateExample = "01/01/2018"
String hourExample = "08:30"
FirebaseFirestore mDB = FirebaseFirestore.getInstance();
CollectionReference mDBCompaniesSchedules = (CollectionReference) mDB.collection("Companies").document(mId_Company).collection("Schedules")
.get()
.addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
for (Schedules document : documentSnapshots.toObjects(Schedules.class)) {
String dtSchedules = document.getSchedules_date();
String hourSchedules = document.getSchedules_hour();
if ( dtSchedules.equals(dateExample) && hourSchedules.equals(hourExample) ){
//Execute a "Toast" and closes the operation
} else {
//Call up the scheduling function.
startScheduling();
}
}
}
})
Process:
I need to run this code and go through all the documents in that collection. I need to check and analyze whether the date and time of the schedule already exists.
If there is: Show a Toast and block.
If there is no: Executing a specific function for the schedule record ("startScheduling()").
Problem:
When the data exists (it will only be once) then it will work.
When there is no data, it falls into the ELSE loop. And it is executing several times the same function "startScheduling();".
I need some way to go through this collection and when I do not find any results, the function "startScheduling (), be executed only once.
This how a for loop works. It will continue iterate till the last element to see if the condition is true or not. With other words, your if-else statement is triggered for every iteration in the loop. It means that, if the condition is true it will go with the if part, if the condition is false it will go with else part, for each and every element.
There are two ways in which you can solve this. One would be to break the loop once the condition was fulfilled. But this means that will iterate till it gets that element. Second, would be to change the logic of your code. Use first the if statement and second iterate.
Edit: The best option in this case would be to query your database using whereEqualTo() method.
Query query = db
.collection("Companies")
.document(mId_Company)
.collection("Schedules")
.whereEqualTo("dtSchedules", dateExample)
.whereEqualTo("hourSchedules", hourExample);
In which dateExample and hourExample are the actual values with which you want to compare.
To count the number of documents in a Collection, please use the following code:
public void onSuccess(QuerySnapshot documentSnapshots) {
if(documentSnapshots.size() == 0) {
startScheduling();
}
for (Schedules document : documentSnapshots.toObjects(Schedules.class)) {
String dtSchedules = document.getSchedules_date();
String hourSchedules = document.getSchedules_hour();
}
}
I need to solve putting data to a realm database like this:
I have an object called obtained_code;
I have a realmList of Obtained codes in an object called Offer;
I download obtained codes separately, and by their offer id assign them to the lists of each object. The problem is that I can't add them because when I check the size, it's always 0.
Here is the code:
ObtainedCodes codes = response.body();
for (ObtainedCode c : codes.getObtainedCodes()) {
Offer offer = RealmController.with(SplashActivity.this).getOffer(c.getOffer_id());
if (offer != null) {
Log.d("Size", "Offer not null");
realm1.beginTransaction();
RealmList<ObtainedCode> list = offer.getObtained_codes();
if (!list) { // if the 'list' is managed, all items in it is also managed
RealmList<ObtainedCode> managedImageList = new RealmList<>();
for (ObtainedCode item : list) {
if (item) {
managedImageList.add(item);
} else {
managedImageList.add(realm1.copyToRealm(item));
}
}
list = managedImageList;
}
offer.setObtained_codes(obtainedCodes);
Log.d("Size", String.valueOf(offer.getObtained_codes().size()));
realm1.copyToRealmOrUpdate(offer);
realm1.commitTransaction();
}
offer = RealmController.with(SplashActivity.this).getOffer(c.getOffer_id());
Log.d("Size", String.valueOf(offer.getObtained_codes().size()));
}
1.) the Ravi Tamada tutorial on InfoHive is a terrible mess, please refer to my remake of that example instead.
If you managed to start using 0.82.1 because Ravi Tamada claimed that a 4 years old version is "stable", well I know that it's not. Use 1.2.0 instead (or the latest version which is 3.4.1)
And if you see a RealmController.with(), run, because it ignores thread-confinement. The moment you try to access it from a background thread, it'll crash.
On background threads, you'd need to do
#Override
public void run() {
try(Realm realm = Realm.getDefaultInstance()) {
repository.whatever(realm); // pass Realm instance to database methods
} // auto-close
// end of thread
}
2.) you are executing writes on the UI thread, that is bad, from UI thread you should use realm.executeTransactionAsync(), but in your case you should actually execute the Retrofit call on a background thread using Ęxecutors.newSingleThreadedPool() and call it with call.execute() instead of call.enqueue().
3.) You should write to Realm on the background thread, and on the UI thread you should use RealmChangeListener to listen to writes.
4.) your code doesn't work because you're setting an unmanaged list to a managed RealmObject.
You should modify the existing RealmList inside the RealmObject, and add only managed objects to it.
Executor executor = Executors.newSingleThreadExecutor(); // field variable
// ...
void someMethod() {
executor.execute(new Runnable() {
#Override
public void run() {
Response<ObtainedCodes> response = retrofitService.getObtainedCodes().execute(); // run on current thread
ObtainedCodes codes = response.body();
if(codes == null) return;
try(Realm r = Realm.getDefaultInstance()) {
r.executeTransaction(new Realm.Transaction() {
#Override
public void execute(Realm realm) {
for(ObtainedCode obtainedCode : codes.getObtainedCodes()) {
Offer offer = realmRepository.getOffer(realm, obtainedCode.getOfferId());
if(offer == null) {
offer = realm.createObject(Offer.class, obtainedCode.getOfferId());
// map properties to offer if possible
}
RealmList<ObtainedCode> offerCodes = offer.getObtainedCodes();
ObtainedCode managedObtainedCode = realm.where(ObtainedCode.class).equalTo("obtainedCodeId", obtainedCode.getId()).findFirst();
if(managedObtainedCode == null) {
managedObtainedCode = realm.createObject(ObtainedCode.class, obtainedCode.getId());
// map properties from obtained code to managed obtained code
}
if(!offerCodes.contains(managedObtainedCode)) {
offerCodes.add(managedObtainedCode);
}
}
}
});
}
}
});
}
I am using firebase database and I have a weird error with transactions. Basically I have a key called "users" with a parameter count and an userlist like this:
"users" : {
"count" : 1,
"userList" : {
"LBBgLkOp3bWbZeSfnKVsqkHeW8s1" : true
}
}
I use a transaction to increment the count and add the current user's id to the userList. The problem is that if there isn't a users key (the first time) the transaction completes succesfully but if there is already an users key the transaction fails for no reason...
My code is this
void AddUserAndIncrementCount()
{
FirebaseDatabase.DefaultInstance.GetReference("users")
.RunTransaction(usersData =>
{
Dictionary<string, object> users = usersData.Value as Dictionary<string, object>;
if (users == null)
{ // firstTime
users = new Dictionary<string, object>();
users.Add("count", 1);
users.Add("userList", new Dictionary<string, object>() { { auth.CurrentUser.UserId, true }});
}
else
{
/* printing users returns this
{"count":1,"userList":{"LBBgLkOp3bWbZeSfnKVsqkHeW8s1":true}}
*/
// INCREMENT COUNT
users["count"] = int.Parse(users["count"].ToString()) + 1;
// ADD USER TO LIST
Dictionary<string, object> userList = users["userList"] as Dictionary<string, object>;
userList.Add(auth.CurrentUser.UserId, true);
users["userList"] = userList;
}
// END TRANSACTION
/* printing users returns this now
{"count":2,"userList":{"LBBgLkOp3bWbZeSfnKVsqkHeW8s1":true,"AM2vI8K106XghEgEgRSkCIpJn0w2":true}}
*/
usersData.Value = users;
return TransactionResult.Success(usersData);
}).ContinueWith(OnAddUserIncrementCountTask);
}
void OnAddUserIncrementCountTask(Task<DataSnapshot> task)
{
if (task.IsCompleted && !task.IsCanceled && !task.IsFaulted)
{
//Success
}
else
{
Debug.Log(task.IsFaulted+" - "+task.Exception.Message);
// True - Exception of type 'System.AggregateException' was thrown.
}
}
As you can see I printed what I was receiving and sending in the transaction and everything seems fine. I tried to set rules to public (read true and write true) and it also failed, so I don't know if I am doing something bad or if there is something wrong with transactions. The things that bothers me is that if the users key doesn't exist in the database then it doesn't fail so I think it is failing for some strange reason I can't see...
I am testing on an Android device.
Thank you.
I finally found what was causing the transaction to fail.
It seems there is a bug that makes transactions fail if one of the existing children of the node, or the children of the children, or the value of the node itself is a boolean (true, false).
To solve it I just replaced the true boolean for a "true" string and now all transactions work.
So in my above code I replaced (this appears twice in the code)
auth.CurrentUser.UserId, true
for:
auth.CurrentUser.UserId, "true"
Hope this post saves some time to the next one running to this bug.
I have this query to update data already in my realm table;
for (MyGameEntrySquad squad : response.body().getSquad()) {
subscription = realm.where(RealmPlayer.class).equalTo("id", squad.getPlayer().getId())
.findFirstAsync()
.asObservable()
.subscribe(new Action1<RealmObject>() {
#Override
public void call(RealmObject realmObject) {
}
});
}
I would like to perform this query asynchronously then display the results on the UI.
Basically, whatever is been returned by response.body().getSquad() has an id matching a record already in the DB; and that is what am using in my equalTo method.
Based on the data received, I would like to update two columns on each of the record matching the IDs.
However, I am facing a few challenges on this:
The Action1 in subscribe is returning a RealmObject instead of a PlayerObject
How to proceed from here
Any guidance on this will be appreciated.
Thanks
Update
if (response.isSuccessful()) {
//asynchronously update the existing players records with my squad i.e is_selected
for (MyGameEntrySquad squad : response.body().getSquad()) {
realm.where(RealmPlayer.class).equalTo("id", squad.getPlayer().getId())
.findFirstAsync()
.<RealmPlayer>asObservable()
.filter(realmPlayer -> realmPlayer.isLoaded())
.subscribe(player -> {
realm.beginTransaction();
if (squad.getPlayer().getPosition().equals("GK")) {
player.setPlaygroundPosition("gk");
player.setIsSelected(true);
}
// pick the flex player
if (squad.isFlex()) {
player.setPlaygroundPosition("flex");
player.setIsSelected(true);
}
// pick the Goalie
if (squad.getPlayer().getPosition().equals("GK")) {
player.setPlaygroundPosition("gk");
player.setIsSelected(true);
}
// pick the DFs
if ((squad.getPlayer().getPosition().equals("DF")) && (!squad.isFlex())) {
int dfCounter = 1;
player.setPlaygroundPosition(String.format(Locale.ENGLISH, "df%d", dfCounter));
player.setIsSelected(true);
dfCounter++;
}
// pick the MFs
if ((squad.getPlayer().getPosition().equals("MF")) && (!squad.isFlex())) {
int mfCounter = 1;
player.setPlaygroundPosition(String.format(Locale.ENGLISH, "mf%d", mfCounter));
player.setIsSelected(true);
mfCounter++;
}
// pick the FWs
if ((squad.getPlayer().getPosition().equals("FW")) && (!squad.isFlex())) {
int fwCounter = 1;
player.setPlaygroundPosition(String.format(Locale.ENGLISH, "mf%d", fwCounter));
player.setIsSelected(true);
fwCounter++;
}
realm.copyToRealmOrUpdate(player);
realm.commitTransaction();
updateFieldPlayers();
});
}
hideProgressBar();
}
realm.where(RealmPlayer.class).equalTo("id", squad.getPlayer().getId())
.findFirstAsync()
.<RealmPlayer>asObservable()
.subscribe(new Action1<RealmPlayer>() {
#Override
public void call(RealmPlayer player) {
}
});
You should do like that.
Btw, it's bad idea to do it in a cycle - check in method of RealmQuery.
for (MyGameEntrySquad squad : response.body().getSquad()) { // btw why is this not `Observable.from()`?
subscription = realm.where(RealmPlayer.class).equalTo("id", squad.getPlayer().getId())
.findFirstAsync()
.asObservable()
This should not be on the UI thread. It should be on a background thread. On a background thread, you need to use synchronous query instead of async query.
Even on the UI thread, you'd still need to filter(RealmObject::isLoaded) because it's an asynchronous query, and in case of findFirstAsync() you need to filter for RealmObject::isValid as well.
For this case, you would not need asObservable() - this method is for observing a particular item and adding a RealmChangeListener to it. Considering this should be on a background thread with synchronous query, this would not be needed (non-looper background threads cannot be observed with RealmChangeListeners).
You should also unsubscribe from any subscription you create when necessary.
And yes, to obtain RealmPlayer in asObservable(), use .<RealmPlayer>asObservable().
In short, you should put that logic on a background thread, and listen for changes on the UI thread. Background thread logic must be done with the synchronous API. You will not need findFirstAsync for this.
ArrayList<User> users = User.getAll();
int n = users.size();
private void dowload(int i){
if(i >= n) return;
Sync.download(i, new OnDownloadListener(){
public void done(Data data){
data.save();
download(i+1);
}
});
}
I have the function download with recursive when i run download(0) the StackOverFlowError was thrown. I have a user list, each user have a few data info and it's downloaded from internet. The user list is stored in my database, i will load all user and call download user data as below, i need to download partials, ex download done for user data 1 and next to user data 2... etc. Have someone give me a suggest to resolve it. Thanks.