In my last article, I discussed the new requirements for Android apps on Google Play to target Android 8 (API level 26) and higher, and described one possible way of handling permission requests at runtime, as required when targeting Android 6 (API level 23) and higher. In this article, I describe an alternative, and also cover the requirement for apps targeting Android 7.0 (API level 24) and higher when accessing “external” URIs.

As per the last article, this article and demo was produced using Delphi Tokyo 10.2.3. The code may or may not work with earlier versions. NOTE: If you’re using Delphi 10.3 or higher, you should use the built-in support for permissions and external URIs.

UPDATE (July 20th, 2018): The demo has been modified to allow for a workaround for local notifications when targeting API 26 or greater. See below for details.

TL;DR: The demo code for this article can be found in the KastriFree project, here.

Targeting an API level

There has been some confusion over what “targeting” a particular API level means. It does not mean which Android SDK you have configured in the SDK Manager in the Delphi IDE. It means what value is set for the targetSdkVersion value in the application manifest:

This is the API level at which Android will act in “compatibility mode” for. By default, Delphi sets this value to 14 (Android 4.0). The device can still have a later version of Android installed, and your application can still use API calls that apply for the installed version, however it will act as if it were at the target level. For example, an app targeting API level 14 on a device running Android 6 will not require requesting permissions at runtime.

At present, to target a particular API level, you need to manually modify the AndroidManifest.template.xml file, as described in the previous article.

A change in the method for responding to runtime permission requests

In the previous article, I devised a method of requesting permissions at runtime that required “overriding” FMXNativeActivity in the manifest. Unfortunately, that meant having to launch the application from the device manually in order to debug via the IDE. Thanks to a suggestion from Stephane Vanderclock, author of the Alcinoe library, I’ve devised a different method that removes the need to “override” the activity.

Through necessity (which will become clearer later), I have created a new class called TPermissionsRequester, which more accurately reflects what it does anyway. This replaces the TSystemHelper class, which I recommend you stop using altogether, if you choose to change to using this new solution.

When a runtime permissions request is made, the focus moves away from the application, and once the user has granted or denied permission, the application receives focus again, and the BecameActive event fires. TPermissionsRequester handles this event, and if a permissions request has been made, checks whether such permissions were granted:

[sourcecode language=”delphi”] procedure TPlatformPermissionsRequester.ApplicationEventMessageHandler(const Sender: TObject; const AMsg: TMessage);
begin
case TApplicationEventMessage(AMsg).Value.Event of
TApplicationEvent.BecameActive:
CheckPermissionsResults;
end;
end;

procedure TPlatformPermissionsRequester.CheckPermissionsResults;
var
LResults: TPermissionResults;
LIndex, I: Integer;
begin
if FRequestCode = -1 then
Exit;
SetLength(LResults, Length(FPermissions));
for I := Low(FPermissions) to High(FPermissions) do
begin
LIndex := I – Low(FPermissions);
LResults[LIndex].Permission := FPermissions[I];
LResults[LIndex].Granted := TOSDevice.CheckPermission(FPermissions[I]);
end;
TOpenPermissionsRequester(PermissionsRequester).DoPermissionsResult(FRequestCode, LResults);
FRequestCode := -1;
end;[/sourcecode]

So all the developer needs to be concerned with is requesting the actual permissions, and handling the OnPermissionsResult event:

[sourcecode language=”delphi”] constructor TForm1.Create(AOwner: TComponent);
begin
inherited;
FRequester := TPermissionsRequester.Create;
FRequester.OnPermissionsResult := PermissionsResultHandler;
end;

procedure TForm1.TakePhotoButtonClick(Sender: TObject);
begin
FRequester.RequestPermissions([cPermissionReadExternalStorage, cPermissionWriteExternalStorage, cPermissionCamera], cPermissionsCodeExternalStorage);
end;[/sourcecode]

As TPlatformPermissionsRequester (in DW.PermissionsRequester.Android) uses TApplicationEventMessage which comes from the FMX.Platform unit, it cannot be used in a service (at least at present). That’s why TPermissonsRequester was created, and CheckPermission method has been moved to TOSDevice, so that it can be used in a service.

Note that in this demo, permissions are requested only when absolutely required (i.e. when the user wants to take a photo). This makes the application a little more user friendly than if the permissions were requested as soon as the application starts.

In the code, I am passing the string representation of the permission, e.g.

cPermissionCamera = android.permission.CAMERA;

The format for all of the “dangerous” permissions is the same, i.e. it follows the pattern: android.permission.XXXX, where XXXX is one of the “Permissions” values in this list, so you can define your own values to be passed to the RequestPermissions method in the same manner.

Note that even though you’re requesting permissions at runtime, you still need to check the checkboxes for those permissions in the Uses Permissions section of the Project Options

Accessing “external” URIs

If you have tried to target API 26, and attempted to use the TTakePhotoFromCameraAction, you would have noticed that it fails, with the error:

android.os.FileUriExposedException: file:///storage/emulated/0/test.txt exposed beyond app through Intent.getData()

From Android 7.0 (API level 24), when accessing “external” URIs, it is necessary to use the FileProvider class. Unfortunately, the code that causes the problem for TTakePhotoFromCameraAction is actually within the Java binaries (FMX.jar) that Delphi uses, so rather than expect developers to patch and recompile FMX.jar, I’ve come up with an alternative solution.

TMediaLibrary is an alternative for (at least for the present), TTakePhotoFromCameraAction. I may or may not expand on it, depending on how useful it becomes, and how Embarcadero handle the required changes. It has a single method: TakePhoto, and has event handlers for when the image is captured successfully, or when the user canceled.

[sourcecode language=”delphi”] TMediaLibrary = class(TObject)
private
FPlatformMediaLibrary: TCustomPlatformMediaLibrary;
FOnCanceled: TNotifyEvent;
FOnReceivedImage: TReceivedImageEvent;
protected
procedure DoCanceled;
procedure DoReceivedImage(const AImagePath: string; const ABitmap: TBitmap);
public
constructor Create;
destructor Destroy; override;
procedure TakePhoto;
property OnCanceled: TNotifyEvent read FOnCanceled write FOnCanceled;
property OnReceivedImage: TReceivedImageEvent read FOnReceivedImage write FOnReceivedImage;
end;[/sourcecode]

Also it is currently useful only on Android, as there are no implementations for the other platforms.

TMediaLibrary makes use of a function from the newly created unit DW.Android.Helpers, called UriFromFile. This method checks what the targetSdkVersion value is, and if >= 24 it uses the FileProvider class to create a Uri from the file reference, passing in the “authority” which needs to be specified in the manifest. This is done by adding a Provider section, like this:

When using the UriFromFile function, the value specified in the manifest for android:authorities will need to match the application’s package name. This value appears in the Project Options under Version Info.

Note the entry: <meta-data android:name=”android.support.FILE_PROVIDER_PATHS” android:resource=”@xml/provider_paths”/>. This is a reference to a file that needs to be deployed with the application, called (in this case): provider_paths.xml. This is what the file looks like:

This file needs to be added to the application’s deployment using Deployment Manager, and the Remote Path value set to: res\xml\

Remember that when targeting API level 24 or greater, your app will need to request the appropriate permissions at runtime (as per the demo), before attempting to use the TakePhoto method. I had considered making TMediaLibrary handle all this automatically; perhaps in the future 🙂 You should also remember that there are a number of other “dangerous” permissions that may be used throughout Delphi (e.g. by TLocationSensor). Be sure to request permissions at runtime for any of those that require it, before you attempt to use them.

Remember also that the UriFromFile function can be used in your own code if your app needs to access “external” files. You will certainly know this is the case if your application throws the dreaded FileUriExposedException.

Taking care of the status bar

In the previous article, it had slipped past me that when changing the API target, the status bar was no longer visible!

In this demo, the workaround was to change the Fill property of the form, setting the Color to Null, and the Kind to Solid. In addition, a Rectangle is added to the form, the Align property set to Contents, and the Color property of Fill set to Whitesmoke. Now the status bar is visible again, however remember that this is just a workaround; hopefully an official solution will present itself in the next update.

Getting notified..

The demo has been updated (July 20th, 2018) to allow for a workaround for local notifications. In order for them to work however, you will need to patch the System.Android.Notification unit from the Delphi source. Details on how to do this are in the readme. Also, you will need to use new files added to KastriFree, namely:

DW.Androidapi.JNI.App
DW.Androidapi.JNI.Support
DW.NotificationReceiver
support-compat-26.1.0.jar

Note also in the demo that the original android-support-v4.dex.jar is disabled. This is because the demo uses a newer version of the Android support libraries:

Are there other issues that need to be accounted for when targeting later API levels?

I’m glad you asked! At present, I’m unaware of any other issues, however my plan is to expand the demo in the future if any others come to light. If you are aware of any that are not related to what I’ve already covered, please let me know in the comments.

The demo code for this article can be found in the KastriFree project, here.