Bidirectional Sync Fails after device goes to sleep

4715
25
06-08-2016 04:46 PM
AaronDick
Occasional Contributor

Using the offline editor sample I consistently get the error on download portion of bidirectional sync "File opened that is not a database file".  The error presents itself during the following situation.

1.  Local runtime GDB created from feature service.

2.  Android app is put in background or device goes to sleep.

3.  Feature collected on device and another feature is created either on AGOL or on another device in the same extent.

4.  GeodatabaseSyncTask is run as per OfflineEditor sample.

5.  Upload is successful but features from other device or AGOL do not show up.  Error generated "File opened that is not a database file".

This behavior does not occur if device is not allowed to go to sleep and application is kept active in the forefront on device.  Error is generated in the syncResponseCallback.

CallbackListener<Map<Integer, GeodatabaseFeatureTableEditErrors>> syncResponseCallback = new CallbackListener<Map<Integer, GeodatabaseFeatureTableEditErrors>>() {

   @Override
   public void onCallback(Map<Integer, GeodatabaseFeatureTableEditErrors> objs) {

   showProgress(activity, false);

   if (objs != null) {

   if (objs.size() > 0) {

  S1ViewerActivity.downloadProgressBar.setVisibility(View.GONE);

   showMessage(activity, "Sync Completed With Errors");

  } else {

  S1ViewerActivity.downloadProgressBar.setVisibility(View.GONE);

   showMessage(activity, "Sync Completed Without Errors");

  }

  } else {

   showMessage(activity, "Sync Completed Without Errors");

  }

  }

   @Override
   public void onError(Throwable e) {

   showMessage(activity, e.getMessage());

   //showProgress(activity, false);
  //e.printStackTrace();

   }

};

Here is the full code for this class (very similar to OfflineEditor GDBUtility class)...

package gov.s1.s1mobile;

import android.app.AlertDialog;

import android.content.Context;

import android.content.DialogInterface;

import android.content.Intent;

import android.view.View;

import android.widget.Toast;

import com.esri.core.ags.FeatureServiceInfo;

import com.esri.core.geodatabase.GeodatabaseFeatureTableEditErrors;

import com.esri.core.io.UserCredentials;

import com.esri.core.map.CallbackListener;

import com.esri.core.tasks.geodatabase.GeodatabaseStatusCallback;

import com.esri.core.tasks.geodatabase.GeodatabaseStatusInfo;

import com.esri.core.tasks.geodatabase.GeodatabaseSyncTask;

import com.esri.core.tasks.geodatabase.SyncGeodatabaseParameters;

import java.util.Map;

public class SyncUtility {

   private static GeodatabaseSyncTask gdbTask;

   public static final String TAG = "SyncUtility";

   public static UserCredentials creds;

   //public static String dirPathHidden;
  //public static ProgressBar downloadProgressBar;
   public static boolean Completed = false;

   // upload and synchronize local geodatabase to the server
   static void synchronize(final S1ViewerActivity activity, UserCredentials AgencyCredentials) {

   showProgress(activity, true);

   String url = S1ViewerActivity.localGDB.getServiceURL();

   creds = AgencyCredentials;

   gdbTask = new GeodatabaseSyncTask(url, AgencyCredentials);

   gdbTask.fetchFeatureServiceInfo(new CallbackListener<FeatureServiceInfo>() {

   @Override
   public void onError(Throwable e) {

   // TODO Auto-generated method stub


   showMessage(activity, e.getMessage());

   showProgress(activity, false);

  String errorMessage = e.getMessage();

   if (errorMessage.contains("Error while generating a token")) {



   Intent newActivity = new Intent(activity,FeatureServiceLogin.class);

  String errorLabel = "error";

  newActivity.putExtra("errorLabel",

  errorLabel);

  newActivity.putExtra("sync", true);

   activity.startActivity(newActivity);

  }else if (errorMessage.contains("Unauthorized access to a secure service")) {

   showMessage(activity, "You are not logged in as the correct user to Sync this database. Please log in as the User who downloaded the database.");

  }

  }

   @Override
   public void onCallback(FeatureServiceInfo objs) {

  Boolean test = objs.getSyncCapabilities().isRollbackOnFailureSupported();

   if (objs.isSyncEnabled()) {

   doSyncAllInOne(activity);

  }

  }

  });

  }

   /**
  * All-in-one method used...
  *
  * @throws Exception
  */
   private static void doSyncAllInOne(final S1ViewerActivity activity) {

   try {

   // get sync parameters from geodatabase
   final SyncGeodatabaseParameters syncParams = S1ViewerActivity.localGDB.getSyncParameters();


   CallbackListener<Map<Integer, GeodatabaseFeatureTableEditErrors>> syncResponseCallback = new CallbackListener<Map<Integer, GeodatabaseFeatureTableEditErrors>>() {

   @Override
   public void onCallback(Map<Integer, GeodatabaseFeatureTableEditErrors> objs) {

   showProgress(activity, false);

   if (objs != null) {

   if (objs.size() > 0) {

  S1ViewerActivity.downloadProgressBar.setVisibility(View.GONE);

   showMessage(activity, "Sync Completed With Errors");

  } else {

  S1ViewerActivity.downloadProgressBar.setVisibility(View.GONE);

   showMessage(activity, "Sync Completed Without Errors");

  }

  } else {

   showMessage(activity, "Sync Completed Without Errors");

  }

  }

   @Override
   public void onError(Throwable e) {

   showMessage(activity, e.getMessage());

   //showProgress(activity, false);
  //e.printStackTrace();

   }

  };

  GeodatabaseStatusCallback statusCallback = new GeodatabaseStatusCallback() {

   @Override
   public void statusUpdated(GeodatabaseStatusInfo status) {

  S1ViewerActivity.appendLog(status.getStatus().toString());

   showMessage(activity, status.getStatus().toString());

  String x = "";

/* // TODO Auto-generated method stub

   gdbTask.syncGeodatabase(syncParams, S1ViewerActivity.localGDB, statusCallback, syncResponseCallback);

   showMessage(activity, "Starting Sync");

  } catch (Exception e) {

   //S1ViewerActivity.featureServiceError = e.getMessage();
   showMessage(activity, e.getMessage());

   showProgress(activity, false);

   //e.printStackTrace();
   }

  }

   static void showProgress(final S1ViewerActivity activity, final boolean showProgressbar) {

  activity.runOnUiThread(new Runnable() {

   @Override
   public void run() {

   if (!showProgressbar) {

   activity.setProgressBarIndeterminateVisibility(showProgressbar);

   activity.downloadProgressBar.setVisibility(View.GONE);

  }else{

   activity.downloadProgressBar.setVisibility(View.VISIBLE);

  }

  }

  });

  }

   static void showMessage(final S1ViewerActivity activity, final String message) {

  activity.runOnUiThread(new Runnable() {

   @Override
   public void run() {

   //if (message != null || message )
   try{

  S1ViewerActivity.appendLog(message);

  String pMessage = "";

   if (message.contains("Sync Completed Without Errors")) {

   S1ViewerActivity.AfterSync();

   if (S1ViewerActivity.numberPhotos != null) {

  pMessage = "Sync has completed. \n\n" + S1ViewerActivity.numFeaturesAddedInteger + " feature(s) have been synchronized. \n\n" + S1ViewerActivity.numberPhotos + " photos have been synchronized.";

  }else{

  pMessage = "Sync has completed. \n\n" + S1ViewerActivity.numFeaturesAddedInteger + " feature(s) have been synchronized.";

  }

   showPrompt(pMessage, activity);

  }else if (message.contains("Sync Completed With Errors")) {

 

   pMessage = "An error occured. Sync has NOT completed. " + message;

   showPrompt(pMessage, activity);

  }else if (message.contains("Untrusted server certificate")) {



   pMessage = "Security Error Connecting to Feature Service because of Web Application Firewall. Put in help desk ticket.: " + message;

   showPrompt(pMessage, activity);

  }else if (message.contains("Not Found")) {



   pMessage = "Either Internal Feature Service or ArcGIS.com is down. Error details: " + message;

   showPrompt(pMessage, activity);

  }else if (message.contains("current license level")) {

   //S1ViewerActivity.downloadProgressBar.setVisibility(View.GONE);

   pMessage = "Your credentials have expired. You must Log Off of S1 and Log back in. To do this go to Options menu and choose option to Log Off. When prompted type in your username and password.";

   showPrompt(pMessage, activity);

  }else{

   //temp bandaid solution regarding sync issues
  //if(message.contains("database file")) {
  //pMessage = "Sync Upload Successful, but Download of other new features not possible due to Server Issues.";
  //showPrompt(pMessage, activity);

  //}else{
   Toast.makeText(activity, message, Toast.LENGTH_LONG).show();

   //}


   }

/* if (message.contains("current license level")) {
  Toast.makeText(activity, "Your Login Credentials have expired.", Toast.LENGTH_LONG).show();
  }*/

   if (pMessage != ""){

  S1ViewerActivity.appendLog(pMessage);

  }

   //S1ViewerActivity.downloadProgressBar.setVisibility(View.GONE);

   } catch (Exception e) {

  e.printStackTrace();

  }

  }

  });

  }

   static void showPrompt(String message, Context activity ){

  DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {

   @Override
   public void onClick(DialogInterface dialog,

   int which) {

   switch (which) {

   case DialogInterface.BUTTON_POSITIVE:

   break;

  }

  }

  };

  AlertDialog.Builder builder = new AlertDialog.Builder(activity);

  builder.setMessage(message).setPositiveButton("OK",dialogClickListener).show();

  }

}

0 Kudos
25 Replies
AlexanderNohe1
Regular Contributor II

I believe a good solution to this would be to create a background service and place the sync call in there.  This is most likely occurring due to the activity life-cycle of an app.  When the app goes to sleep or the screen is turned off / semi closed, the activity is destroyed and with it all running processes in that activity.  The background service will ensure that the sync lives beyond the lifecycle of the app.

Here is a quick example from Stack Overflow:

android - Run upload process in background even when phone is asleep - Stack Overflow

EDIT:

This may be the best resource here:

https://developer.android.com/training/run-background-service/create-service.html

EDIT2:

You may also want to consider looking at chapter 26 in the Big Nerd Ranches Android Programming Guide.

0 Kudos
AaronDick
Occasional Contributor

Hey Alex, That does sound like a good concept, especially if syncs take awhile.  I realize I probably did not explain the circumstances very well.  The behavior we see happens if the app goes to sleep or the app goes into the background.  At the point in time when the device goes to sleep or into the background we do not have a sync process running.  Basically once the activity is more or less killed by the system all syncs you try to do after that point in time fail.  It seems like this is happening because connections to the Geodatabase are not being closed as they should properly, I am guessing because in the case of the Android system taking over the onDestroy event is no longer called.  Thus I am guessing that there is still a connection hanging in the main activity map view that is causing a conflict with sync when it is trying to insert the new records into the Geodatabase.

AlexanderNohe1
Regular Contributor II

activity_lifecycle.png

Graphic came from:

Activity | Android Developers

What I would do is that when onPause() is called in your activity, run the Dispose() command which will close your geodatabase.  Then when onResume() is called, reinstantiate your geodatabase.  The onPause() may be a better location to dispose of your geodatabase rather than onDestroy since onDestroy will be called when the activity is completely closed, not just placed into the background.

Geodatabase | ArcGIS Android 10.2.8 API

I hope this helps!

0 Kudos
AaronDick
Occasional Contributor

Hi Alex, Thank you for the advice.  I am now pretty much at a loss for what is happening.  I now have the code dispose of the localGDB prior to sync.  Additionally I close the Map Activity and Recycle the mapview.  None of this seems to have any impact.  The upload of new features does work, but insert of new features fails with error "File opened that is not a database file".  Very strange.

0 Kudos
AlexanderNohe1
Regular Contributor II

What is your code snippet for the opening the geodatabase?

Can you use the Android Device Monitor to confirm that your path to the geodatabase is correct?

Is the insert of new features to the existing geodatabase or is it coming from the server attempting to sync?

0 Kudos
AaronDick
Occasional Contributor

Hi Alex, 

1)  What is your code snippet for the opening the geodatabase? We are adding in Polygon, Polyline and then Points because the SDK has no ability to reorder layers after they are added.

localGDB = new Geodatabase(priorGDBString);

FeatureInformation.ReCreateInstance();

for (GeodatabaseFeatureTable gdbFeatureTable : localGDB.getGeodatabaseTables()) {

   if (gdbFeatureTable.hasGeometry()) {

   if(gdbFeatureTable.getGeometryType().equals(Geometry.Type.POLYGON)) {

   mMapView.addLayer(new FeatureLayer(gdbFeatureTable));

  }

  }

}

for (GeodatabaseFeatureTable gdbFeatureTable : localGDB.getGeodatabaseTables()) {

   if (gdbFeatureTable.hasGeometry()) {

   if(gdbFeatureTable.getGeometryType().equals(Geometry.Type.POLYLINE)) {

   mMapView.addLayer(new FeatureLayer(gdbFeatureTable));

   //gdbLayerStatus = "Added";
  //FeatureLayer x = new FeatureLayer(gdbFeatureTable);
   }

  }

}

for (GeodatabaseFeatureTable gdbFeatureTable : localGDB.getGeodatabaseTables()) {

   if (gdbFeatureTable.hasGeometry()) {

   if(gdbFeatureTable.getGeometryType().equals(Geometry.Type.POINT)) {

   mMapView.addLayer(new FeatureLayer(gdbFeatureTable));

   //gdbLayerStatus = "Added";
  //FeatureLayer x = new FeatureLayer(gdbFeatureTable);
   }

  }

}

2)  Can you use the Android Device Monitor to confirm that your path to the geodatabase is correct?  Yes in logcat path is always showing up correctly.  Even when stepping into ESRI Runtime SDK GeodatabaseSync Task all paths and parameters look correct.

3)  Is the insert of new features to the existing geodatabase or is it coming from the server attempting to sync?  When using an internal feature service I get the same behavior and I see no errors on the ArcGIS Service side.  New features from my device always upload correctly.  It is the insert of new features collected by others that do not appear to insert properly.  There are a bunch of files in the GDB directory generated by the SDK.  There appears to be a temporary Geodatabase in the folder that I never see if the process is successful.

0 Kudos
AlexanderNohe1
Regular Contributor II

Before I address your other concerns in this post, one last thing I would like to confirm.  Are your users adding features outside of the initial extent of the features that already exist within your geodatabase?  That is, if I have features in Maine, are other users adding points in Washington and they are not getting synced to the geodatabase?

0 Kudos
AaronDick
Occasional Contributor

HI Alex, The features are all in the same extent.  I have an extent set as Portland, Oregon metro area on both devices.

0 Kudos
AlexanderNohe1
Regular Contributor II

As far as reordering layers goes, I did it this way.  I keep layer[0] in this position since it is the basemap:

public class MainActivity extends AppCompatActivity {

  MapView mapView;

  // https://developers.arcgis.com/android/guide/maps-and-layers.htm#ESRI_SECTION1_3CE2C5FDDFA443A78D9284...

  @Override protected void onCreate(Bundle savedInstanceState) {

   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);

   mapView = (MapView) findViewById(R.id.map);
   Layer[] x = new Layer[] { new ArcGISFeatureLayer("https://sampleserver6.arcgisonline.com/arcgis/rest/services/Wildfire/FeatureServer/0",
   ArcGISFeatureLayer.MODE.ONDEMAND),
  new ArcGISFeatureLayer("https://sampleserver6.arcgisonline.com/arcgis/rest/services/Wildfire/FeatureServer/1",
   ArcGISFeatureLayer.MODE.ONDEMAND),
  new ArcGISFeatureLayer("https://sampleserver6.arcgisonline.com/arcgis/rest/services/Wildfire/FeatureServer/2",
   ArcGISFeatureLayer.MODE.ONDEMAND)};
   mapView.addLayers(x);

   Button button = (Button) findViewById(R.id.x);
   button.setOnClickListener(new View.OnClickListener() {

   @Override public void onClick(View v) {

   for (Layer layer : mapView.getLayers()) {

  Log.e("NOHE", layer.getName().toString());
   }

  Layer[] z = mapView.getLayers();
   mapView.removeAll();
   Layer[] q = new Layer[] {z[0], z[3], z[2], z[1]};
   mapView.addLayers(q);
   //mapView.refreshDrawableState();
   }

  });
  }

}

As far as your other concerns, can you show me with a screenshot where the new points are getting drawn in comparison to your older points?

0 Kudos