Saturday, January 2, 2016

Using LocationSensor on Android Service in Delphi Seattle

Hi all,
this is a very great news!
Me and my CEO (Daniele Tetibit Time Professionals) have figured out how to implement and use the LocationSensor on Android Service.
Some months ago, I found out that there is a malfunction in Delphi: If you put LocationSensor component on Android Service DM, the service crashes. So I opened an issue on the quality portal, which was recognized and classified with majority prior.
However we need to retrieve the Device position from a background Service not by a foreground Activity.
We have investigated into the code and found this mistake in System.Android.Sensors unit:
  • For the majority of the code, it is assumed that the context of Application is an Activity 
Here is the code:

FActivity := TJNativeActivity.Wrap(System.DelphiActivity)^.clazz);

The code above works fine if you are in Android Activity context, like our case. If you are in a service context though, this doesn't work (the DelphiActivity pointer is null, rightly). If there is not necessity to have a reference to the Activty, but only the context of Application, this code is to be preferred:

LContext := TJContextWrapper.Wrap(System.JavaContext);

Below you can find the System.Sensors and System.Android.Sensors units (opportunely renamed System.SensorsDD and System.Android.SensorsDD), with the fixes explained earlier (the code contains comments for any changes):
System.SensorsDD:
// about line 748
implementation
uses
  System.Variants, System.Math, System.Character,
{$IFDEF ANDROID}
  // -- patch dd
  // include the modified System.Android.SensorsDD
  System.Android.SensorsDD;
{$ENDIF ANDROID}

System.Android.SensorsDD:
...
//about line 12
uses
  // -- patch dd
  // use the modified System.SensorsDD
  System.SensorsDD;
...
// about line 70
class constructor TPermission.Create;
var
  PackageInfo: JPackageInfo;
  PackageManager: JPackageManager;
  Activity: JActivity;
  LContext: JContext;
begin
  // -- patch dd
  // Activity := TJNativeActivity.Wrap
  // (PANativeActivity(System.DelphiActivity)^.clazz)
  LContext := TJContextWrapper.Wrap(System.JavaContext);
  PackageManager := LContext.getPackageManager();
  PackageInfo := PackageManager.getPackageInfo
    (LContext.getApplicationContext.getPackageName,
    TJPackageManager.JavaClass.GET_PERMISSIONS);
  FPermissions := PackageInfo.requestedPermissions;
end;
...
// about line 100
type
  TAndroidGeocoder = class(TGeocoder)
  private type
    TGeocoderRunnable = class(TJavaLocal, JRunnable)
    private
      FCoord: TLocationCoord2D;
      FLGeocoder: JGeocoder;
    public
      constructor Create(ACoord: TLocationCoord2D; AGeocoder: JGeocoder);
      procedure run; cdecl;
    end;
  private
  class var
    FGeocoder: JGeocoder;
    // FActivity: JActivity; // -- patch dd
    FActivity: JContextWrapper; // -- patch dd
...
// about line 130
  TUIAndroidLocationSensor = class(TCustomLocationSensor)
  private
    FPermitted: Boolean;
    // FActivity: JNativeActivity; // -- patch dd
    FActivity: JContext; // -- patch dd
    FLastValue: JLocation;
    FLocationManager: JLocationManager;
    FAccuracy: TLocationAccuracy;
    FDistance: TLocationDistance;
...
// about line 1600
constructor TUIAndroidLocationSensor.Create(AManager: TSensorManager);
var
  LocationService: JObject;
begin
  inherited;
  // FActivity := TJNativeActivity.Wrap
  // (PANativeActivity(System.DelphiActivity)^.clazz); // -- patch dd
  FActivity := TJContext.Wrap(System.JavaContext); // -- patch dd
  LocationService := FActivity.getSystemService
    (TJContext.JavaClass.LOCATION_SERVICE);
  if Assigned(LocationService) then
    FLocationManager := TJLocationManager.Wrap((LocationService as ILocalObject)
      .GetObjectID);
end;
...
// about line 1630
  function RunIfPossible(var ARunnable: TLocationRunnable;
    var AListener: TLocationListener; AProviderName: JString): Boolean;
  var
    Provider: JLocationProvider;
    LHandler: JHandler;
  begin
    Result := False;
    if FLocationManager.isProviderEnabled(AProviderName) then
    begin
      if AListener = nil then
        AListener := TLocationListener.Create(Self);
      Provider := FLocationManager.getProvider(AProviderName);
      if Provider <> nil then
      begin
        ARunnable := TLocationRunnable.Create(FLocationManager, AListener,
          AProviderName);
        // FActivity.runOnUiThread(ARunnable); // --patch dd
        // -- patch dd
        // You can use post method of Handler instead runOnUiThread in this case.
        // more info here: http://developer.android.com/guide/topics/fundamentals/processes-and-threads.html
        LHandler := TJHandler.JavaClass.init;
        LHandler.post(ARunnable);
        Result := True;
      end;
    end;
  end;
...
// about line 1830
class constructor TAndroidGeocoder.Create;
begin
  // -- patch dd
  // FActivity := TJNativeActivity.Wrap
  // (PANativeActivity(System.DelphiActivity)^.clazz);
  FActivity := TJContextWrapper.Wrap(System.JavaContext);
  FGeocoder := TJGeocoder.JavaClass.init(FActivity);
end;


Now you are able to using LocationSensor in this way:
uses
  System.SensorsDD, System.Android.SensorsDD
...
...
var
  FSensors: TSensorArray;
  Sensor: TCustomSensor;
begin
  TSensorManager.Current.Active := true;
  FSensors := TSensorManager.Current.GetSensorsByCategory(TSensorCategory.Location);

  FSensor := nil;
  for Sensor in FSensors do
  begin
    if TCustomLocationSensor(Sensor).SensorType = TLocationSensorType.GPS then
    begin
      FSensor := TCustomLocationSensor(Sensor);
      Break;
    end;
  end;

  if not Assigned(FSensor) then
    Exit; { no location sensor is available }

  { start the sensor if it is not started }
  if not FSensor.Started then
    FSensor.Start;

You can find a complete demo here. Hopefully this solution will be helpful to you the way it was for us.

P.S.
Stay tuned as because of this fix a very interesting application/project is about to see the light of day :)

23 comments :

  1. Great as usual Daniele Spinetti a.k.a. "Danielino" J

    ReplyDelete
  2. Hi... I had spoken at length about this issue to Jim McKeeth along with some other minor bugs in Android services. I am glad that this is resolved.

    Looking forward to the demo.

    ReplyDelete
  3. Hello.
    I made your changes on System.Android.SensorsDD.pas and System.SensorsDD.pas and put it in folder "LocationSensorPatch\patch"
    Then i compile project, i get errors:
    "[DCC Error] System.SensorsDD.pas(805): E2010 Incompatible types: 'TSensorManager.TSensorManagerType' and 'class of TPlatformSensorManager'
    [DCC Error] System.SensorsDD.pas(1144): E2361 Cannot access private symbol TGeocoder.GeocoderImplementer
    [DCC Error] System.SensorsDD.pas(1449): E2362 Cannot access protected symbol TPlatformGpsStatus.GetGpsStatusImplementer"

    Please help :)

    ReplyDelete
    Replies
    1. It seems strange.. Are you sure that you have done all fixes as shown ?

      Delete
    2. Yes, i've done all fixes as shown. The different is that in your post fixes i see "// about line 1600", but in my System.Android.Sensors.pas it's near line 1500 and "// about line 1630" is near line 1530.
      May be we have different Delphi versions?
      I work in Delphi 10 Seattle (without any updates).

      Sorry for english.

      Best regards, Ruslan

      Delete
    3. Thank you for solving my problem )

      Delete
  4. Olá, estou aplicando sua rotina de programação, más estou com um problema, o programa funciona corretamente, más as coordenadas Latitude e longitude estão em branco, pode me ajudar, muito grato

    ReplyDelete
  5. Olá.. Preciso de sua ajuda, estou tenando fazer rodar o programa mas acusa a falta dos arquivos System.Android.SensorsDD.pas e System.SensorsDD.pas ..
    Poderia disponibilizar o projeto completo com estas units?

    Desde já agradeço.

    ReplyDelete
    Replies
    1. Hello, the code of System.Android.Sensors.pas and System.Sensors.pas are properties of Embarcadero that have the copyrights, so I cannot redistribute the entire code/unit but only show my changes.
      Ps. For future comments please use english language, thanks.

      Delete
  6. Hi!
    Do you have an idea how to use a similar solution for receiving CallStateChanges from the Telephony? Similar to the OnCallStateChanged solution? Is there a similar way like this, to use the IFMXPhoneDialerService interface on an AndroidService?
    Thanks,
    Laszlo

    ReplyDelete
    Replies
    1. Hi, you can use the TPlatformServices with IFMXPhoneDialerService to detect the call state changes as described here http://docwiki.embarcadero.com/RADStudio/Berlin/en/Mobile_Tutorial:_Using_the_Phone_Dialer_on_Mobile_Devices_(iOS_and_Android). It should work also on Android Service. Have you tried ?

      Delete
    2. Hi!
      Yes, well actually putting any FMX. unit into the service freezes it.
      So for example: uses FMX.Platform, FMX.PhoneDialer;
      FMX stuff doesn't seem to work on Android Service, as far as I know.

      Delete
    3. Ok! Now I understand... I'll do a check ASAP

      Delete
  7. can i use this demo in delphi berlin 10.1 ??

    ReplyDelete
  8. Hi Daniele, i dowload all the code from here https://github.com/spinettaro/DelphiDemos. I open the project DemosPG.groupproj and i run (F9) the i get this error: [DCC Error] MainFMX.pas(69): E2035 Not enough actual parameter. I too try to open LocationSensorPatch.groupproj and when i run this project i get this error: [DCC Fatal Error] LocationServiceU.pas(10): F2613 Unit '..\patch\System.Android.SensorsDD.pas' not found. How i can to test this example?

    ReplyDelete
  9. For me, the demo runs great, but after 30-40 minutes, the service stops. How far did you test it? Did it work for at least a full day for you?

    ReplyDelete
    Replies
    1. I tested it for several hours, not a full day, but for that time it works fine for me

      Delete