java.io.FileNotFoundException after update on-demand package on Play asset delivery - android

I follow this document to setup on-demand asset delivery like this:
// Get asset path
private fun getAbsoluteAssetPath(assetPack: String, relativeAssetPath: String): String? {
val assetPackPath: AssetPackLocation =
assetPackManager.getPackLocation(assetPack)
// asset pack is not ready
?: return null
val assetsFolderPath = assetPackPath.assetsPath()
return FilenameUtils.concat(assetsFolderPath, relativeAssetPath)
}
// download
assetPackManager.registerListener(downloadListener)
assetPackManager.fetch(listOf(packageName)).addOnCompleteListener{ res ->
Timber.i("Download $packageName ${res.isSuccessful}")
}
// access the file
var filePath = File(getAbsoluteAssetPath("myAssetPack", "test.png"));
// it throws java.io.FileNotFoundException when open this file.
There are a lot of crash reports from my users on Crashlytics, and look like it happens when they update the app with new asset package files on google play.
The problem is that I can't reproduce it on my devices.
Is there anyone know why this is happened?
P/s: before open the file, I already check the package existing and it is valid.
if(assetPackManager.getPackLocation(packageName) != null)

Related

Error when I try to read a file using Capacitor in Android 11

I have an app using Ionic and Capacitor with target API 29, but I need to change to API 30 because of PlayStore rules.
My problem with this transition is with capacitor filesystem, which is not finding files in my device. I need to read these files to import in the map in my app.
I tried to read files and directories, tried to create some random files and use stat to get files information, but sometimes I can read these files and sometimes I don't. I tried to find a pattern in this errors with some tests, but I couldn't find or understand the pattern.
Initially I thought that this problem was caused by external storage permissions, so I added in the manifest the relevant parameters, even so the errors persisted in my app. I started to think that this can be related to Android Unix Permissions, but I can't say if that is true. I tried to use checkPermissions() of Capacitor, but I received the 'granted' response.
External Storage parameters in AndroidManifest.xml:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
I tried to research for permission problems related to capacitor filesystem, but the only question I found is my own about the same subject, created a while ago. This question have an answer, but it is but not applicable for API 30.
I also tried to research about filesystem android permissions too, I found one answer about this subject, but this only tell me that I can't modify the FS permission properties.
Some things I tried to do:
First I tried to use readFile() in a file on external storage:
this.fs.readFile('Download/test.kml', true).then(response => {
console.log(response)
});
readFile() function inside FS service:
/** Read a file and returns a string as base 64 */
readFile(path: string, needDirectory: boolean = false, dir: string = 'ExternalStorage'): Promise<string> {
let options: ReadFileOptions;
if (needDirectory) {
options = {
path: path,
directory: Directory[dir]
}
} else {
options = {
path: path
}
}
return Filesystem.readFile(options).then(result => {
return result.data;
});
}
But it didn't work, then I tried to create some files using writeFile(), in tree different directories:
this.fs.writeFile('Download/', 'test.kml', "<xml version='1.0' encoding='utf-8'></xml>", true, undefined, true);
this.fs.writeFile('/', 'test.kml', "<xml version='1.0' encoding='utf-8'></xml>", true, 'External', true);
this.fs.writeFile('/', 'test.kml', "<xml version='1.0' encoding='utf-8'></xml>", true, 'Documents', true);
writeFile() function inside FS service:
/** Write a file, content in base64, Pass encoding to true to write data as string */
writeFile(path: string, filename: string, content: any, recursive: boolean = false, dir: string = 'ExternalStorage', encoding: boolean = false): Promise<boolean> {
let options: WriteFileOptions = {
path: path + filename,
data: content,
directory: Directory[dir],
recursive: recursive
}
if (encoding) {
options.encoding = Encoding.UTF8
}
return Filesystem.writeFile(options).then(result => {
if (result) {
return true;
} else {
return false;
}
})
}
This worked! Then I tried to read all these newly created files:
this.fs.readFile('Download/test.kml', true);
this.fs.readFile('test.kml', true, 'External');
this.fs.readFile('test.kml', true, 'Documents');
This worked too!, Now, in my head the problem is with the files that aren't created by my app, but the files that already exist in FS or are moved by the user. So I tried to use readDir to test what the app sees:
this.fs.readDir('/', undefined, true)
this.fs.readDir('/', 'External', true)
this.fs.readDir('/', 'Documents', true)
readDir function into de fs service:
/** Read dir and list this files */
readDir(path: string, dir: string = 'ExternalStorage', needDirectory: boolean = true): Promise<ReaddirResult> {
let options: ReaddirOptions;
if (needDirectory) {
options = {
path: path,
directory: Directory[dir]
}
} else {
options = {
path: path
}
}
return Filesystem.readdir(options).then(files => {
return files;
}).catch(e => {
console.log(e);
let resultError: ReaddirResult = {
files: []
};
return resultError;
})
}
The readDir() returns me only the folders that my app created, but there are others files in this directory, those that were created manually didn't appear. Then I tried to use the stat() function in one of this files that my app didn't saw:
this.fs.getStat('USO.kml', 'Documents')
this.fs.getStat('USO.kml')
getStat() function inside FS service:
/**
* Get file information's: type, size, ctime(Create time), mtime(Last modification), uri
* #param path path of file
* #param dir directory where is file
*/
getStat(path: string, dir: string = 'ExternalStorage'): Promise<StatResult> {
return Filesystem.stat({
path: path,
directory: Directory[dir]
}).then(result => {
return result;
});
}
I can't understand, because the file USO.kml in Documents directory appear in stat function, but another one in external storage returns me an error File does not exist. I don't understand why it's happening. How can I correctly read the files using Capacitor FS and API 30?
Edit
Just to clarify: The file is moved externally by the user and saved inside this directory through Android explorer. I need to import this file to my app to get the information inside.

Android - Append additional pdf page to PrintedPdfDocument

in my App I print some parts to a pdf for the user. I do this by using a PrintedPdfDocument.
The code looks in short like this:
// create a new document
val printAttributes = PrintAttributes.Builder()
.setMediaSize(mediaSize)
.setColorMode(PrintAttributes.COLOR_MODE_COLOR)
.setMinMargins(PrintAttributes.Margins.NO_MARGINS)
.build()
val document = PrintedPdfDocument(context, printAttributes)
// add pages
for ((n, pdfPageView) in pdfPages.withIndex()) {
val page = document.startPage(n)
Timber.d("Printing page " + (n + 1))
pdfPageView.draw(page.canvas)
document.finishPage(page)
}
// write the document content
try {
val out: OutputStream = FileOutputStream(outputFile)
document.writeTo(out)
out.close()
Timber.d("PDF written to $outputFile")
} catch (e: IOException) {
return
}
It all works fine. However now I want to add another page at the end. Only exception is that this will be a pre-generated pdf file from the assets. I only need to append it so no additional rendering etc. should be necessary.
Is there any way of doing this via the PdfDocument class from the Android SDK?
https://developer.android.com/reference/android/graphics/pdf/PdfDocument#finishPage(android.graphics.pdf.PdfDocument.Page)
I assumed it might be a similar question like this here: how can i combine multiple pdf to convert single pdf in android?
But is this true? The answer was not accepted and is 3 years old. Any suggestions?
Alright, I gonna answer my own question here.
It looks like there are not many options. At least I couldn't find anything native. There are some pdf libraries in the Android framework but they all seem to support only creating new pages but no operations on existing documents.
So this is what I did:
First of all there don't seem to be any good Android libraries. I found that one here which prepared the Apache PDF-Box for Android. Add this to your Gradle file:
implementation 'com.tom_roush:pdfbox-android:1.8.10.3'
In code you can now import
import com.tom_roush.pdfbox.multipdf.PDFMergerUtility
Where I added a method
val ut = PDFMergerUtility()
ut.addSource(file)
val assetManager: AssetManager = context.assets
var inputStream: InputStream? = null
try {
inputStream = assetManager.open("appendix.pdf")
ut.addSource(inputStream)
} catch (e: IOException) {
...
}
// Write the destination file over the original document
ut.destinationFileName = file.absolutePath
ut.mergeDocuments(true)
That way the appendix page is loaded from the assets and appended at the end of the document.
It then gets written back to the same file as it was before.

Unable to get external files dir. External Storage State is mounted on Android

I have been struggling with creating a File on API 23 of Android.
I am getting the following error:
Caused by: java.io.IOException: cannot get external files dir, external storage state is mounted
I am trying to setup a Voice Recognizer, and for that I have to open the file for the words. I set up the Voice Recognizer this way.
private fun runRecognizerSetupAsync(): String? {
try {
Log.d("Voice", "${isExternalStorageWritable()} gg ")
val assets = Assets(activity)
val assetDir = assets.syncAssets()
setupVoiceRecognizer(assetDir)
} catch ( e : IOException){
Log.v("VoiceCommands", "$e + Error ")
return e.toString()
}
return null
}
private fun setupVoiceRecognizer(assetsDir: File) {
speechRecognizer = SpeechRecognizerSetup.defaultSetup()
.setAcousticModel(File(assetsDir, "es-ptm"))
.setDictionary(
File(assetsDir, "cmudict-es.dict")
)
.recognizer
speechRecognizer.addListener(this)
// Create keyword-activation search.
//recognizer.addKeyphraseSearch(KWS_SEARCH, KEYPHRASE);
// Create your custom grammar-based search
val activationWords = File(assetsDir, "activation.gram")
speechRecognizer.addKeywordSearch(ACTIVATION_SEARCH, activationWords)
}
It works properly on most phones, but there are some exceptions that throws this error.
Every bit of guidance is highly appreciated. Thanks in advance for taking the time. :)

How do I download a file on both Android X and iOS that is publicly available, using Xamarin.Forms?

Looking at this issue xamarin/Essentials#1322, how do I download a file on both Android ( versions 6-10, Api 23-29 ) and iOS ( version 13.1+ ) that is publicly available (share-able to other apps, such as Microsoft Word). I don't need to give write access to the other apps, just read-only is ok if it must be restricted.
I get the following exception:
[Bug] Android.OS.FileUriExposedException: file:///data/user/0/{AppBundleName}/cache/file.doc exposed beyond app through Intent.getData()
With the following code.
public static string GetCacheDataPath( string fileName ) => Path.Combine(Xamarin.Essentials.FileSystem.CacheDirectory, fileName);
public static FileInfo SaveFile( string filename, Uri link )
{
using var client = new WebClient();
string path = GetCacheDataPath(filename);
DebugTools.PrintMessage(path);
client.DownloadFile(link, path);
return new FileInfo(path);
}
public async Task Test(Uri link)
{
LocalFile path = await SaveFile("file.doc", link).ConfigureAwait(true);
var url = new Uri($"ms-word://{path.FullName}", UriKind.Absolute);
await Xamarin.Essentials.Launcher.OpenAsync(url).ConfigureAwait(true);
}
With this answer, I created a FileService interface and it works with local private files but I am unable to share the files. Starting with Android Q (10 / Api 29), the following is deprecated.
string path = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads).AbsolutePath; // deprecated
I get the following exception:
System.UnauthorizedAccessException: Access to the path '/storage/emulated/0/Download/file.doc' is denied. ---> System.IO.IOException: Permission denied
I haven't found any way yet to get a public path for Android 10 with Xamarin.Forms. I've looked at the Android Docs for Content providers but it's in Java, and I can't get it working in C# yet.
Any help would be greatly appreciated.
I did find a Solution
Found a fix
For Android
public Task<System.IO.FileInfo> DownloadFile( Uri link, string fileName )
{
if ( link is null )
throw new ArgumentNullException(nameof(link));
using System.Net.WebClient client = new System.Net.WebClient();
// MainActivity is the class that loads the application.
// MainActivity.Instance is a property that you set "Instance = this;" inside of OnCreate.
Java.IO.File root = MainActivity.Instance.GetExternalFilesDir(MediaStore.Downloads.ContentType);
string path = Path.Combine(root.AbsolutePath, fileName);
client.DownloadFile(link, path);
return Task.FromResult(new System.IO.FileInfo(path));
}
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
internal static MainActivity Instance { get; private set; }
protected override void OnCreate(Bundle savedInstanceState)
{
...
Instance = this;
...
}
...
}
For iOS
public Task<System.IO.FileInfo> DownloadFile( Uri link, string fileName )
{
if ( link is null )
throw new ArgumentNullException(nameof(link));
using System.Net.WebClient client = new System.Net.WebClient();
string path = Path.Combine(Xamarin.Essentials.FileSystem.CacheDirectory, fileName)
client.DownloadFile(link, path);
return Task.FromResult(new System.IO.FileInfo(path));
}
public async Task Share()
{
// back in shared project, choose a file name and pass the link.
System.IO.FileInfo info = await DependencyService.Get<IDownload>().DownloadFile(new Uri("<enter site>", "file.doc").ConfigureAwait(true);
ShareFile shareFile = new ShareFile(info.FullName, "doc"); // enter the file type / extension.
var request = new ShareFileRequest("Choose the App to open the file", shareFile);
await Xamarin.Essentials.Share.RequestAsync(request).ConfigureAwait(true);
}
Note that for iOS, due to Apple's infinite wisdom... I cannot share the file directly with another app as I can on Android. Sandboxing is good for security but in this case, how they implemented it, it limits options. Both Applications must be pre-registered / pre-allocated in an "App Group" to share files directly. See this Article and the Apple Docs for more information.

Cannot get web PDFs to view in Ionic 4 on Android devices

I have an Ionic 4 app developed using InAppBrowser. The app is, in fact, almost all purely done via InAppBrowser:
Splash Screen > URL passed to InAppBrowser > Site loads.
User logins in, clicks links and navigates through site.
Some links the user may click are PDFS (content type = application/pdf) but does not have a .pdf extension at the end of the filename.
In iOS, these load fine. In Android, the PDFs never show....
I've read this is a known workaround. I cannot use the Google Docs viewer embed workaround as the URL to the pdf files are based on being authenticated into our site to see and access the PDFs.
The other option I'm reading is to get the URL, download the file to the user's local system browser; then open it up - all of this using FileSystem, File, FileTransfer, and FileOpener. Below is my code block and output from Android Studio LOGCAT:
downloadPDF(pUrl: string) {
console.log("==== in downloadOpenPDF function ====");
if(this.platform.is('android')) {
var permissions = cordova.plugins.permissions;
permissions.hasPermission(permissions.WRITE_EXTERNAL_STORAGE , function( status ){
if ( status.hasPermission ) {
console.log("We DO have access to write to external storage");
}
else {
console.warn("We DO NOT have access to write to external storage");
permissions.requestPermission(permissions.WRITE_EXTERNAL_STORAGE, this.success, this.error);
}
});
permissions.hasPermission(permissions.READ_EXTERNAL_STORAGE , function( status ){
if ( status.hasPermission ) {
console.log("We DO have access to read to external storage");
}
else {
console.warn("We DO NOT have access to read to external storage");
permissions.requestPermission(permissions.READ_EXTERNAL_STORAGE, this.success, this.error);
}
});
}
let downloadUrl = pUrl;
console.log("downloadUrl is ================"+downloadUrl);
let path = this.file.dataDirectory;
let filepath = path+'mcfrenderPDF.pdf';
console.log("path is =================="+path);
const transfer: FileTransferObject = this.fileTransfer.create();
transfer.download(downloadUrl, filepath,true).then(entry => {
let url = entry.toUrl();
console.log("url is ================="+url);
if(this.platform.is('ios') || this.platform.is('android')) {
console.log("================ Use Document Viewer");
this.document.viewDocument(url, 'application/pdf',{});
} else {
//console.log("Device is Android = Use Google Docs");
//this.openGoogleDocsPDF(pUrl);
console.log("local path to file is: "+path+'mcfrenderPDF.podf');
this.fileOpener.open(entry.URL, 'application/pdf');
}
})
}
My output of my log is as follows:
2019-11-03 13:26:05.401 5213-5333/com.cat.fpd.mobile.mcf D/FileTransfer: Saved file: file:///data/user/0/com.cat.fpd.mobile.mcf/files/mcfrenderPDF.pdf
2019-11-03 13:26:05.401 5213-5333/com.cat.fpd.mobile.mcf E/FileTransfer: File plugin cannot represent download path

Categories

Resources