transistorsoft / capacitor-background-geolocation

The most sophisticated background location-tracking & geofencing module with battery-conscious motion-detection intelligence for iOS and Android.
MIT License
95 stars 16 forks source link

Android crash sqlite #251

Closed GoethelHB closed 2 months ago

GoethelHB commented 4 months ago

Hello, got some crashreports during appstart from sqlite, while sdk is checking GeofenceDAO.exists.

Fatal Exception: java.lang.IllegalArgumentException the bind value at index 1 is null

Your Environment

Expected Behavior

App shouldn't crash, if there is an sqlite error or if queryargs are missing.

Actual Behavior

App crashed with: Fatal Exception: java.lang.IllegalArgumentException the bind value at index 1 is null

Debug logs

Logs ``` android.database.sqlite.SQLiteProgram.bindString (SQLiteProgram.java:184) android.database.sqlite.SQLiteProgram.bindAllArgsAsStrings (SQLiteProgram.java:220) android.database.sqlite.SQLiteDirectCursorDriver.query (SQLiteDirectCursorDriver.java:49) android.database.sqlite.SQLiteDatabase.rawQueryWithFactory (SQLiteDatabase.java:2254) android.database.sqlite.SQLiteDatabase.queryWithFactory (SQLiteDatabase.java:2101) android.database.sqlite.SQLiteDatabase.query (SQLiteDatabase.java:1972) android.database.sqlite.SQLiteDatabase.query (SQLiteDatabase.java:2178) com.transistorsoft.locationmanager.data.sqlite.GeofenceDAO.exists (Unknown Source:25) com.transistorsoft.locationmanager.geofence.TSGeofenceManager$f.run (Unknown Source:10) java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1145) java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:644) java.lang.Thread.run (Thread.java:1012) ```
christocracy commented 4 months ago

Show me all the code where you interact with BackgroundGeolocation

GoethelHB commented 4 months ago

Hello Chris, thank you for your fast reply. I sent you a mail with all the code, where interacting with the plugin.

christocracy commented 4 months ago

Post your code here. Not email.

I dont want ALL your code, only code that referenced BackgroundGeolocation

GoethelHB commented 4 months ago

Ok, so here is the related code.

Config:

const { settings } = await getGeofenceAccuracyLevel();

const params = new URLSearchParams({});
const refreshUrl = `url`;

return {
    ...settings,
    //desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
    //distanceFilter: 10,
    //stopTimeout: 5,
    logLevel: (process.env.BUILD_MODE === "production") ? BackgroundGeolocation.LOG_LEVEL_ERROR : BackgroundGeolocation.LOG_LEVEL_VERBOSE,
    logMaxDays: (process.env.BUILD_MODE === "production") ? 3 : 28,
    enableHeadless: true,
    stopOnTerminate: false,
    startOnBoot: true,
    locationAuthorizationAlert: i18n.t("geofence.iosLocationAuthorizationAlert", { returnObjects: true }),
    backgroundPermissionRationale: i18n.t("geofence.androidPopupAllowAllTheTime", { returnObjects: true }),
    notification: {
      smallIcon: "drawable/notification_icon",
      largeIcon: "", // empty string to remove (default) icon
      text: i18n.t("geofence.androidNotification.text"),
    },
    disableProviderChangeRecord: true,
    disableLocationAuthorizationAlert: false,
    //disableMotionActivityUpdates: true, if true will no longer ask the physical activity permission, which the plugin would need for better geofencing
    persistMode: BackgroundGeolocation.PERSIST_MODE_GEOFENCE,
    url: geofenceEventUrl,
    method: "POST",
    autoSync: true,
    geofenceTemplate: JSON.stringify({
      id: deviceId,
      gwId: "<%= geofence.identifier %>",
      loc: "<%= geofence.action %>",
      ts: "<%= timestamp %>",
    }),
    httpRootProperty: ".",
    headers: restHeaders,
    authorization: {
      strategy: "JWT",
      accessToken: accessToken,
      refreshToken: refreshToken,
      refreshUrl: refreshUrl,
      refreshPayload: {
        [name]: "{refreshToken}",
      },
      refreshHeaders: {
         ...
      },
    },
  }

Init / ready:

      if (Capacitor.isNativePlatform()) {
        try {
          const oldConfig = await BackgroundGeolocation.getState();
          await BackgroundGeolocation.ready(oldConfig);
        } catch (error) {
          console.warn("renderIfAuthenticated, BackgroundGeolocation.ready failed", error);
        }
      }
const registerGeofenceListeners = async (): Promise<void> => {
  await BackgroundGeolocation.removeListeners();

  BackgroundGeolocation.onAuthorization(async ({ status, success, error, response }) => {
    if (success) {
      if (typeof response === "object" && typeof response.access_token === "string" && typeof response.refresh_token === "string") { // && typeof response.old_token_id === "string"
        await setTokens({
          accessToken: response.access_token,
          refreshToken: response.refresh_token,
        });
      } else {
        console.warn("onAuthorization invalid body response", status, response);
      }
    } else {
      console.warn("onAuthorization unsuccessful", status, error);
    }
  });
};
export const readyGeofence = async (): Promise<void> => {
  try {
    const { accessToken, refreshToken } = await getTokens();

    if (accessToken !== "" && refreshToken !== "") {
      const config = await getDefaultGeofenceConfig(accessToken, refreshToken);
      const { enabled } = await BackgroundGeolocation.setConfig(config);

      await registerGeofenceListeners();

      if (enabled) {
        // TODO: fights the symptom of a rare bug, where the plugin is in a bugged state and can only be repaired by restarting it.
        void restartGeofencePlugin(); // long running task -> no await
      }
    } else {
      console.warn("tokens are empty in readyGeofence");
    }
  } catch (error) {
    console.error("readyGeofence catch", error);
  }
};
export const restartGeofencePlugin = async (): Promise<void> => {
  try {
    const { enabled } = await BackgroundGeolocation.getState();
    if (enabled) {
      await BackgroundGeolocation.stop();
    }
    const geofs = await BackgroundGeolocation.getGeofences();
    await enableDisableGeofence(geofs); // This sends exit events and enter events by readding all geofences
  } catch (error) {
    console.warn("Error while trying to restartGeofencePlugin");
  }
};

Check permissions:

export const checkAndRequestGeolocationPermission = async (requestEnable: boolean = true): Promise<AuthorizationStatus> => {
  try {
    const { status } = await BackgroundGeolocation.getProviderState();
    if (requestEnable && ![BackgroundGeolocation.AUTHORIZATION_STATUS_WHEN_IN_USE, BackgroundGeolocation.AUTHORIZATION_STATUS_ALWAYS].includes(status)) {
      try {
        return await BackgroundGeolocation.requestPermission();
      } catch (authStatus) {
        console.warn("Geolocation-Permissions not granted", authStatus);
        return authStatus as AuthorizationStatus;
      }
    } else {
      return status;
    }
  } catch (error) {
    console.error("checkAndRequestGeolocationPermission error", error);
    return BackgroundGeolocation.AUTHORIZATION_STATUS_NOT_DETERMINED;
  }
};

Enable / disable:

const enableDisableGeofence = async (newGeofences: Array<Geofence>): Promise<void> => {
  const { enabled, url } = await BackgroundGeolocation.getState();
  let geofenceUrl = url;
  let hasGeofences = false;
  try {
    const geofences = await BackgroundGeolocation.getGeofences();
    hasGeofences = (geofences.length + newGeofences.length) > 0;
  } catch (error) {
    console.warn("Exception in 'BackgroundGeolocation.getGeofences'", error);
  }

  if (enabled && !hasGeofences) {
    await BackgroundGeolocation.stop();
  } else if (!enabled && hasGeofences) {
    const authorizationStatus = await checkAndRequestGeolocationPermission();

    if ([BackgroundGeolocation.AUTHORIZATION_STATUS_WHEN_IN_USE, BackgroundGeolocation.AUTHORIZATION_STATUS_ALWAYS].includes(authorizationStatus)) {
      // fallback, if url is undefined
      const { locationTracking } = await getGeofenceAccuracyLevel(); // defaults to low
      let startGeofenceState: State;
      if (locationTracking) {
        startGeofenceState = await BackgroundGeolocation.start();
      } else {
        startGeofenceState = await BackgroundGeolocation.startGeofences();
      }
      if ((geofenceUrl === undefined || geofenceUrl.length === 0) && startGeofenceState.url) {
        geofenceUrl = startGeofenceState.url;
      }
    }
  }

  if (newGeofences.length > 0) {
    await BackgroundGeolocation.addGeofences(newGeofences);
  }

  const { status } = await BackgroundGeolocation.getProviderState();
  if (geofenceUrl && status === BackgroundGeolocation.AUTHORIZATION_STATUS_ALWAYS && newGeofences.length > 0) {
    await sendExitEvents(newGeofences, geofenceUrl);

    try {
      await BackgroundGeolocation.changePace(true);
    } catch {
      console.warn("enableDisableGeofence, could not changePace, error!");
    }
  }
};

const enableDisable = async (data, geofenceAllowed: boolean): Promise<void> => { // called by some events
  const geofenceExists = await BackgroundGeolocation.geofenceExists(data.id);
  if (geofenceAllowed && !geofenceExists) {
    const newGeofence = await makeGeofence(data.id, data.latitude!, data.longitude!, data.geofence_radius);
    if (newGeofence) {
      await enableDisableGeofence([newGeofence]);
    }
  } else if (!geofenceAllowed && geofenceExists) {
    await BackgroundGeolocation.removeGeofence(data.id);
    await enableDisableGeofence([]);
  }
};

export const checkGeofenceRemoved = async (id): Promise<void> => {
  try {
    if (await BackgroundGeolocation.geofenceExists(id)) {
      await BackgroundGeolocation.removeGeofence(id);
      await enableDisableGeofence([]);
    }
  } catch (error) {
    console.error("checkGeofenceRemoved error", error);
  }
};

change acc level:

export const setGeofenceAccuracyLevel = async (value: GeofenceAccuracyLevel["value"]): Promise<void> => {
  await Preferences.set({ key: "geofence_accuracy_level", value: value });
  const { locationTracking, settings } = await getGeofenceAccuracyLevel();
  const state = await BackgroundGeolocation.setConfig(settings);
  if (state.enabled) {
    if (state.trackingMode === 0 && locationTracking) {
      await BackgroundGeolocation.stop();
      await BackgroundGeolocation.start();
    } else if (state.trackingMode === 1 && !locationTracking) {
      await BackgroundGeolocation.stop();
      await BackgroundGeolocation.startGeofences();
    }
  }
};

logout:

      const { enabled } = await BackgroundGeolocation.getState();
      if (enabled) {
        await BackgroundGeolocation.removeGeofences();
        await BackgroundGeolocation.stop();
      }

And here the android native code:

public class RefreshWorker extends Worker {
  public Result doWork() {
    Context context = getApplicationContext();
    String url = getInputData().getString("url");

    int status = refreshTokens(url);
    if (status >= 200 && status <= 299) {
      CookieManager.getInstance().flush();

      String refreshToken = getRefreshCookieValue(context);
      String accessToken = getAccessCookieValue(context);

      TSAuthorization authorization = TSConfig.getInstance(context).getAuthorization();
      authorization.setRefreshToken(refreshToken);
      authorization.setAccessToken(accessToken);
      TSConfig.getInstance(context)
        .updateWithBuilder()
        .setAuthorization(authorization)
        .commit();
      BackgroundGeolocation.getInstance(context).sync();
  }
christocracy commented 4 months ago

// TODO: fights the symptom of a rare bug, where the plugin is in a bugged state and can only be repaired by restarting it.

what does “bugged state” mean?

christocracy commented 4 months ago

I can reproduce your error by calling .geofenceExists(null).

I suggest you check this code:

const geofenceExists = await BackgroundGeolocation.geofenceExists(data.id);

Ensure that your data.id contains a String.

GoethelHB commented 4 months ago

Hello Chris, thanks a lot for your fast reply. Changed the code to check for data.id . Is it possible to catch the error too, if this approach isn't helping?

For your first question about the rare bug: We had an issue, if the app is booting and the plugin isn't fully started, and you get a geofence event, it wasn't send out. So we just restarted the magic. It might be possible, we fixed this already, but not yet retested.

github-actions[bot] commented 3 months ago

This issue is stale because it has been open for 30 days with no activity.

github-actions[bot] commented 2 months ago

This issue was closed because it has been inactive for 14 days since being marked as stale.