FlexCel Android Guide
Introduction
As it is the case with iOS and most of the platforms we support, the FlexCel code you have to write for Android itself is very similar to the code for normal "Full-Framework" .NET apps in Windows. We’ve reused most of the code from FlexCel in our Android implementation, and you can still use APIMate, the Demos, everything that you could in Windows when coding for Android.
Fonts
The biggest difference with Windows apps is that Android by default has a single font: “Roboto”. When exporting documents to PDF the lack of extra fonts can be a problem.
There are multiple ways to workaround this:
1. Replacing all fonts in the document by internal fonts.
If you are only using ANSI Windows-1252 characters and a single font, and you don’t care about the exact font used, you can use the code:
FlexCelPdfExport.FontMapping = TFontMapping.ReplaceAllFonts;
before exporting to PDF.
A PDF viewer is required to understand 3 basic fonts: A Sans-Serif (Arial or Helvetica like), a Serif (Times or Times New Roman like), and a proportional font (Courier or Courier New like).
So if you don’t embed any font and are fine with using the internal fonts, you can get a smaller file and forget about other issues by replacing the fonts by internal ones. However, note that this will only work if you are not using characters outside of the ANSI Windows-1252 range: The internal fonts don’t support Unicode, so if you have Unicode characters, you need to provide a font that has the needed characters.
2. Deploying the fonts with your app.
You can provide FlexCel the fonts you need in the place where FlexCel expects them. The simplest way to do that is to deploy the font files needed as internal assets in your application.
To do it, in the project manager of Visual Studio, right click in the Assets folder and create a “fonts” folder behind it:
Warning
The path is case sensitive: “Fonts” instead of “fonts” will not work.
Important
At the time of this writing there is a bug in Visual Studio 2017 which will raise an error * MSB3025: The source file Assets/fonts is actually a directory* when you add a folder to the assets.
If you are getting this error, you need to manually edit the csproj after adding the fonts folder to the Assets, and remove the line:
<AndroidAsset Include="Assets\fonts\" />
For more information take a look at https://forums.xamarin.com/discussion/87052/compiling-error-when-assets-folder-containing-subfolder
Note
If you are using .NET Maui, there is no need to setup the Assets. You can copy the fonts directly in Resources/Fonts:
And FlexCel will pick them from there. This way you can use the fonts in both FlexCel and your app.
Also note that all assets should have their “Build action” set to “AndroidAsset”:
Important
Make sure you have the rights to distribute the fonts that you deploy.
3. Providing the font data via an event.
If you deploy your fonts to a “fonts” folder inside your app assets as explained in the point above, then you have nothing more to do.
But if your app already deploys the fonts to some other folder, you can use the FlexCelPdfExport.GetFontFolder event to change where FlexCel will look for the fonts. For example, if your fonts happen to be inside a folder in your assets named Shared Fonts, you could use the following code:
using (FlexCelPdfExport pdf = new FlexCelPdfExport(xls, true))
{
pdf.GetFontFolder += (sender, e) =>
{
e.FontPath = "@Shared Fonts";
};
pdf.Export("result.pdf");
}
Take a look at the “@” sign the code above. To use fonts that you included as assets, you need to return a string starting with a “@” sign in the GetFontFolder event.
So for example to specify that the fonts are in the "fonts" folder asset, you would return "@fonts" on the OnGetFontFolder event. (“@fonts” is the default used by FlexCel and covered in point 2 of this document).
Tip
Of course you can specify any folder in the Android device, not only assets. If the fonts are not in assets, just specify the full folder without a starting @.
A note about Encodings
When you create a Xamarin iOS or Android application, by default it will come with a limited number of encodings. This is to keep application size small. Those encodings include for example ASCII and Unicode, but no Win1252 (encoding used in western Windows machines) or IBM 437 (encoding used in zip files).
FlexCel will work in most cases with the reduced number of encodings, but there are some rare cases where we need the full list of encodings. An example is when reading an Excel 95 file, and there are a couple of other cases more.
So in order to not have problems with non-existing encodings, it might be a good idea to click in your project properties and add "west" encodings.
- In iOS: Go to Build->iOS Build and select the "Advanced" tab. There in the "Internationalization" section choose "west".
- In Android: Go to Build->Android Build and select the "Linker" tab. There in the "Internationalization" section choose "west".
If you are worried about the extra size you can skip this step. FlexCel will still work in most of the cases without the additional encodings.
Saving files
Android has 2 types of storage: "Internal storage" and "External storage". While historically internal storage might have been in the device and external outside it, today there is normally no such difference. What makes internal different from external is the permissions: Your app has full permissions to the internal storage, and those files belong to your app. If you uninstall the app, the files on its internal storage will be deleted with it. On the other side, you need permissions for external storage, and those files are available for other apps too.
Internal storage
For internal storage there is not much more to say: You can create, modify and delete those files as you like. To get the path to the internal storage from your code, you can use
System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal)
If using Xamarin Essentials, you can also use Xamarin.Essentials.FileSystem.AppDataDirectory
Note
Xamarin Essentials and the .NET API shown above are cross-platform, so they can also be used in for example iOS and they will point to similar folders there. On the other hand, there are Android-specific APIs like Application.Context.FilesDir or Android.Content.Context.GetExternalFilesDir(string type) which will only work in Android.
External storage
External storage can on its own be divided into 2 different types: Private and Public external storage. Both are publicly available for the rest of the apps, but conceptually private files are expected to be used by your app, while public files are to be freely shared.
To save a file in external storage, you can use
For private external storage: Android.Content.Context.GetExternalFilesDir(string type)
For public external storage: Android.OS.Environment.GetExternalStoragePublicDirectory(string directoryType)
Note
The APIs for external storage are not cross-platform, likely due to the fact that external storage is an Android-only concept.
You can find a good description of external storage here : https://docs.microsoft.com/en-us/xamarin/android/platform/files/external-storage
Also, you can check https://developer.android.com/training/data-storage/files for more information about the kind of folders where you can save the data.
Getting permissions to external storage
In order to read or save a file into external storage, you first need to get permissions from Android. In older Android versions, adding those permissions to the manifest was enough. But if you target Android 6 or newer, you also need to dynamically ask for the permissions when you want to use them.
You can find the code needed to ask for the permissions here:
https://docs.microsoft.com/en-us/xamarin/android/app-fundamentals/permissions
Sharing files
A tale of two APIs
A long, long time ago, the way to share files in Android was to store the file in external storage and then call an intent to share the file. This had multiple problems, one of those being that both the app sharing the file and the app consuming it should have permissions to read/write external storage.
To fix this, Android introduced the FileProvider class which was made available via a support library to Android 4 (Ice Cream Sandwich) and later.
But there was little reason to change the FileProvider API, since the old API was still working despite its problems. That changed with Android 7 (Nougat), which deprecated the old way to share files: https://developer.android.com/about/versions/nougat/android-7.0-changes#perm
And still, there was no need to change the old ways: you could still target an older Android version and have it run in Android 7 without issues. The new behavior only happened if you explicitly targeted Android 7.
Note
The versions in Android can be a little confusing, and it makes sense that we clarify them a little before continuing.
The target version in Android is not the minimum Android version your app will run. The minimum version is given by minSdkVersion. The target version instead is the version your app is optimized for. You can target Android 7 and still have a minimum version of Android 4, but if you target Android 7, the Android 7 rules will apply to your code so the old way to share files won't work. If you target Android 4 (and have a minimum version of Android 4 too), then the Android 4 rules will apply, even if you are running in Android 7.
This all means that if you target Android 4 and have a minimum SDK of Android 4, you don't have to care about the new Android 7 rules, and your app will still work in Android 7. If you target Android 7, even if your minimum supported version is still 4, you have to use the new way to share files.
You can read more about Android versioning here: https://developer.android.com/guide/topics/manifest/uses-sdk-element#ApiLevels
Now, going back to our story: in August 2018 this all changed again. Google started to ask for a target version of 8.0 or newer to be able to publish in the Play store. When you set a target version of 8.0 (no matter if your minimum version is lower), then the old-style way to share files won't work anymore in devices running Nougat or later. Old tricks won't work anymore, and it is time to change to the new system.
Important
Since Android 9, the support library is also deprecated, and now FileProvider is at https://developer.android.com/reference/androidx/core/content/FileProvider
So you need to decide what to use: If the not-deprecated androidx which only supports Android 9 or newer, or the old support library which isn't updated anymore but supports older devices.
In the examples bundled with FlexCel we will show AndroidX, but note the differences with Support Library. If you want a working example with Support Library, you can find it in GitHub: https://github.com/tmssoftware/TMS-FlexCel.NET-demos/releases/tag/v7.1.2.1
Converting to AndroidX
To convert your application to AndroidX, you need to install the AndroidX nuget package
You can find more information here: https://docs.microsoft.com/en-us/xamarin/android/platform/androidx
Once you've done that, you need to adapt the FileProvider from android.support.v4.content.FileProvider to androidx.core.content.FileProvider as shown below.
Using FileProvider to share files
To share files in the new way, you need to define a FileProvider.
You can read about the concepts here: https://developer.android.com/training/secure-file-sharing/
And in the rest of this section we will discuss how to share a simple file created by FlexCel. You can find a finished working example here: LangWars
Step 1. Install Xamarin Essentials.
In order to have access to the functionality we need, you will have to install Xamarin Essentials from nuget.
Step 2. Declare the file provider.
You can do this in two different ways:
You can manually edit your properties/AndroidManifest.xml file, and add the following <provider> below the <application> tag:
<application ...> ... <provider android:name="androidx.core.content.FileProvider" android:grantUriPermissions="true" android:exported="false" android:authorities="${applicationId}.fileprovider"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_provider_paths" /> </provider> </application>
Note
To use support library instead of AndroidX, set the provider to be:
<provider android:name="android.support.v4.content.FileProvider"
Also note that it is always "android.support.FILE_PROVIDER_PATHS", even in AndroidX
If you don't want to manually edit AndroidManifest.xml, you can use the ContentProviderAttribute instead. You need to define a class that inherits from AndroidX.Core.Content.FileProvider and add an attribute like:
[ContentProvider( new[] { FileProviderAuthority }, Name = "langwars.custom.FileProvider", GrantUriPermissions = true, Exported = false ), MetaData( name: "android.support.FILE_PROVIDER_PATHS", Resource = "@xml/file_provider_paths" ) ] public class LangWarsCustomFileProvider: AndroidX.Core.Content.FileProvider { }
> [!Note]
> To use support library instead of AndroidX, set the class to derive from:
> public class LangWarsCustomFileProvider: Android.Support.V4.Content.FileProvider
Where you would have to replace "FileProviderAuthority" with a constant that uniquely identifies your provider among all installed applications. Normally you use "your_app_id.fileprovider" here, and whatever name you use, it is the name you will have to use below in step 4. In method 2.1, we used "${applicationId}.fileprovider" as value for the authority.
The second approach can be cleaner in that it doesn't require to manually modify the app manifest, but on the other hand it needs to define a new custom provider when you could just use the built-in FileProvider. In our demo, we choose to simply modify the manifest.
Step 3. Declare the paths that the FileProvider can use.
In step 2 we defined a resource:
Resource = "@xml/file_provider_paths"
Now we need to create this resource, where we will tell Android which are the paths that the provider will use. To do so, we create an "xml" folder under the "Resources" folder, and inside that folder, we create a file named file_provider_paths.xml
For our example, we are going to store the file in the root data folder in the internal memory, so we will add this to the provider paths. The file file_provider_paths.xml will be:
<?xml version="1.0" encoding="utf-8" ?>
<paths>
<files-path name="files" path="." />
</paths>
If you need to share a file in external storage or on a different path, you should change this file accordingly. For more information about what you can write in this file, please read https://developer.android.com/reference/androidx/core/content/FileProvider#SpecifyFiles
Step 4. Write the code.
Once the provider is correctly declared, the last step is to write the code to share the file. The code we used in the example is as follows:
Intent Sender = new Intent(Intent.ActionSend);
Sender.SetType(StandardMimeType.Xls);
Java.IO.File xlsFile = new Java.IO.File(TempXlsPath);
var contentUri = AndroidX.Core.Content.FileProvider.GetUriForFile(
this,
ApplicationContext.PackageName + ".fileprovider",
xlsFile);
Sender.PutExtra(Intent.ExtraStream, contentUri);
Sender.SetFlags(ActivityFlags.GrantReadUriPermission);
StartActivity(Intent.CreateChooser(Sender, "Select application"));
The authority we used for GetUriForFile (ApplicationContext.PackageName + ".fileprovider") is the same as the authority we defined in step 2.
Note
If you get a Java.Lang.NullPointerException error when trying your app, you most likely didn't define the provider right. Make sure that the name of the provider in the xml definition is the same as the name you use in your code, and that the provider is correctly embedded in the manifest.