NEW Google Drive Android API - Metadata of unfinished file prematurely available - android

This problem is directed to the Google Drive Android API support team. It may be considered a bug or just heads-up note. When testing GDAA I've run into this problem:
I created a file asynchronyously
Before the file was ready, I performed a search and found the file by name
Managed to retrieve it's metadata with no sign of problems
Attempt to use the metadata blew up (obviously)
The point is: Shouldn't the file search or metadata retrieval indicate that file is not ready / does not exist yet? Or is there a method to check that?
Here are code snippets to demonstrate the problem (simplified - not a production level code)
primitive # 1 create a file asynch
void createFileAsync(final DriveFolder fldr, final String name,
final String mime, final byte[] buff) {
final DriveId dId = fldr.getDriveId();
Drive.DriveApi.newContents(_gac).setResultCallback(new ResultCallback<ContentsResult>() {
#Override public void onResult(ContentsResult rslt) {
if (!rslt.getStatus().isSuccess()) return;
DriveFolder folder = Drive.DriveApi.getFolder(_gac, dId);
MetadataChangeSet meta = new MetadataChangeSet.Builder()
.setTitle(name).setMimeType(mime).build();
folder.createFile(_gac, meta, rslt.getContents()).setResultCallback(
new ResultCallback<DriveFileResult>() {
#Override public void onResult(DriveFileResult rslt) {
_dFile = rslt.getStatus().isSuccess() ? rslt.getDriveFile() : null;
}
});
}
});
}
primitive # 2 find the file by name - with wait
public DriveFile findFirst(String name) {
Filter filtr = Filters.and(
Filters.eq(SearchableField.TRASHED, false),
Filters.eq(SearchableField.TITLE, name)
);
Query qry = new Query.Builder().addFilter(filtr).build();
MetadataBufferResult rslt = (Drive.DriveApi.query(_gac, qry).await()
if (rslt.getStatus().isSuccess()) {
MetadataBuffer mdb = null;
try {
mdb = rslt.getMetadataBuffer();
return Drive.DriveApi.getFile(_gac, mdb.get(0).getDriveId());
} finally { if (mdb != null) mdb.close(); }
}
return null;
}
test / problem scenario :
GoogleApiClient _gac;
DriveFolder fldr = Drive.DriveApi.getRootFolder(_gac);
byte[] buffer = ("FooBar ").getBytes();
// create a file async
DriveFile _dFile = null;
createFileAsync(fldr, "foo", "text/plain", buffer);
// file is not ready yet, but FOUND and it's metadata VALID (non-null)
Metadata md = findFirst("foo").getMetadata(_gac).await().getMetadata();
// any attempt to use Metadata methods
// md.isTrashed(), md.getTitle(), ...
// blows up until the createFileAsync() is finished

I am happy to report that the problem disappeared (by intervention ?). The query by name on a file that is being created (asynchronously) does not return valid metadata until the creation is complete. Simplified code chunk from primitive #2 above:
for (Metadata md : rslt.getMetadataBuffer())
; // NO md AVAILABLE until the file creation is completed!!!
works correctly now, not enumerating any metadata until 'the file is ready'.

Related

Android 11 + Kotlin: Reading a .zip File

I've got an Android app written in Kotlin targeting framework 30+, so I'm working within the new Android 11 file access restrictions. The app needs to be able to open an arbitrary .zip file in the shared storage (chosen interactively by the user) then do stuff with the contents of that .zip file.
I'm getting a URI for the .zip file in what I'm led to understand is the canonical way:
val activity = this
val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) {
CoroutineScope(Dispatchers.Main).launch {
if(it != null) doStuffWithZip(activity, it)
...
}
}
getContent.launch("application/zip")
My problem is that the Java.util.zip.ZipFile class I'm using only knows how to open a .zip file specified by a String or a File, and I don't have any easy way to get to either of those from a Uri. (I'm guessing that the ZipFile object needs the actual file rather than some kind of stream because it needs to be able to seek...)
The workaround I'm using at present is to turn the Uri into an InputStream, copy the contents to a temp file in private storage, and make a ZipFile instance from that:
private suspend fun <T> withZipFromUri(
context: Context,
uri: Uri, block: suspend (ZipFile) -> T
) : T {
val file = File(context.filesDir, "tempzip.zip")
try {
return withContext(Dispatchers.IO) {
kotlin.runCatching {
context.contentResolver.openInputStream(uri).use { input ->
if (input == null) throw FileNotFoundException("openInputStream failed")
file.outputStream().use { input.copyTo(it) }
}
ZipFile(file, ZipFile.OPEN_READ).use { block.invoke(it) }
}.getOrThrow()
}
} finally {
file.delete()
}
}
Then, I can use it like this:
suspend fun doStuffWithZip(context: Context, uri: Uri) {
withZipFromUri(context, uri) { // it: ZipFile
for (entry in it.entries()) {
dbg("entry: ${entry.name}") // or whatever
}
}
}
This works, and (in my particular case, where the .zip file in question is never more than a couple MB) is reasonably performant.
But, I tend to regard programming by temporary file as the last refuge of the terminally incompetent, thus I can't escape the feeling that I'm missing a trick here. (Admittedly, I am terminally incompetent in the context of Android + Kotlin, but I'd like to learn to not be...)
Any better ideas? Is there a cleaner way to implement this that doesn't involve making an extra copy of the file?
Copying from external source (and risking downvoting to oblivion) and this isn't quite an answer, but too long for a comment
public class ZipFileUnZipExample {
public static void main(String[] args) {
Path source = Paths.get("/home/mkyong/zip/test.zip");
Path target = Paths.get("/home/mkyong/zip/");
try {
unzipFolder(source, target);
System.out.println("Done");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void unzipFolder(Path source, Path target) throws IOException {
// Put the InputStream obtained from Uri here instead of the FileInputStream perhaps?
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(source.toFile()))) {
// list files in zip
ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null) {
boolean isDirectory = false;
// example 1.1
// some zip stored files and folders separately
// e.g data/
// data/folder/
// data/folder/file.txt
if (zipEntry.getName().endsWith(File.separator)) {
isDirectory = true;
}
Path newPath = zipSlipProtect(zipEntry, target);
if (isDirectory) {
Files.createDirectories(newPath);
} else {
// example 1.2
// some zip stored file path only, need create parent directories
// e.g data/folder/file.txt
if (newPath.getParent() != null) {
if (Files.notExists(newPath.getParent())) {
Files.createDirectories(newPath.getParent());
}
}
// copy files, nio
Files.copy(zis, newPath, StandardCopyOption.REPLACE_EXISTING);
// copy files, classic
/*try (FileOutputStream fos = new FileOutputStream(newPath.toFile())) {
byte[] buffer = new byte[1024];
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
}*/
}
zipEntry = zis.getNextEntry();
}
zis.closeEntry();
}
}
// protect zip slip attack
public static Path zipSlipProtect(ZipEntry zipEntry, Path targetDir)
throws IOException {
// test zip slip vulnerability
// Path targetDirResolved = targetDir.resolve("../../" + zipEntry.getName());
Path targetDirResolved = targetDir.resolve(zipEntry.getName());
// make sure normalized file still has targetDir as its prefix
// else throws exception
Path normalizePath = targetDirResolved.normalize();
if (!normalizePath.startsWith(targetDir)) {
throw new IOException("Bad zip entry: " + zipEntry.getName());
}
return normalizePath;
}
}
This apparently works with pre-existing files; however since you already have an InputStream read from the Uri - you can adapt this and give it a shot.
EDIT:
It seems like it's extracting to Files as well - you could store the individual ByteArrays somewhere then decide what to do with them later on. But I hope you get the general idea - you can do all of this in-memory, without having to use the disk (temp files or files) in between.
Your requirement is a bit vague and unclear however, so I don't know what you're trying to do, merely suggesting a venue/approach to try out
What about a simple ZipInputStream ? –
Shark
Good idea #Shark.
InputSteam is = getContentResolver().openInputStream(uri);
ZipInputStream zis = new ZipInputStream(is);
#Shark has it with ZipInputStream. I'm not sure how I missed that to begin with, but I sure did.
My withZipFromUri() method is much simpler and nicer now:
suspend fun <T> withZipFromUri(
context: Context,
uri: Uri, block: suspend (ZipInputStream) -> T
) : T =
withContext(Dispatchers.IO) {
kotlin.runCatching {
context.contentResolver.openInputStream(uri).use { input ->
if (input == null) throw FileNotFoundException("openInputStream failed")
ZipInputStream(input).use {
block.invoke(it)
}
}
}.getOrThrow()
}
This isn't call-compatible with the old one (since the block function now takes a ZipInputStream as a parameter rather than a ZipFile). In my particular case -- and really, in any case where the consumer doesn't mind dealing with entries in the order they appear -- that's OK.
Okio (3-Alpha) has a ZipFileSystem https://github.com/square/okio/blob/master/okio/src/jvmMain/kotlin/okio/ZipFileSystem.kt
You could probably combine it with a custom FileSystem that reads the content of that file. It will require a fair bit of code but will be efficient.
This is an example of a custom filesystem https://github.com/square/okio/blob/88fa50645946bc42725d2f33e143628e7892be1b/okio/src/jvmMain/kotlin/okio/internal/ResourceFileSystem.kt
But I suspect it's simpler to convert the URI to a file and avoid any copying or additional code.
It's easy to check the .zip and .rar files in the Android-Kotlin FileAdapter(work with file manager), add the bellow function to your code:
private fun isZip(name: String): Boolean {
return name.contains(".zip") || name.contains(".rar")
}

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.

Google Drive REST API : file.getCreatedTime() returns always null

I am working with Android Quickstart for Google Drive Rest APi provided at the below link. Android Quickstart
The sample code works fine as is. However when I try to get other details from files like getCreatedTime() or GetWevViewLink() 'null' is returned. Only getName() and getId() returns values.
Google Drive REST APIs v3 would only return only certain default fields. If you need some field, you have to explicitly request it by setting it with .setFields() method.
Modify your code like this -
private List<String> getDataFromApi() throws IOException {
// Get a list of up to 10 files.
List<String> fileInfo = new ArrayList<String>();
FileList result = mService.files().list()
.setPageSize(10)
// see createdTime added to list of requested fields
.setFields("nextPageToken, files(createdTime,id,name)")
.execute();
List<File> files = result.getFiles();
if (files != null) {
for (File file : files) {
fileInfo.add(String.format("%s (%s)\n",
file.getName(), file.getId()));
}
}
return fileInfo;
}
You can read more about this behavior here https://developers.google.com/drive/v3/web/migration
Updated link https://developers.google.com/drive/api/v2/migration
Quoting from the above link -
Notable changes
Full resources are no longer returned by default. Use the fields query parameter to request specific fields to be returned. If left unspecified only a subset of commonly used fields are returned.
Accept the answer if it works for you so that others facing this issue might also get benefited.
I think you need to use the Metadata class to be able to use the getCreatedDate as indicated in Working with File and Folder Metadata.
Then try something like:
ResultCallback<MetadataResult> metadataRetrievedCallback = new
ResultCallback<MetadataResult>() {
#Override
public void onResult(MetadataResult result) {
if (!result.getStatus().isSuccess()) {
showMessage("Problem while trying to fetch metadata");
return;
}
//show the date when file was created
Metadata metadata = result.getMetadata();
showMessage("File was created on " + metadata.getCreatedDate() );
}
}

Downloaded asset is not being loaded from local storage in android, using unity

I am trying to load an object at run time from downloaded assetbundle in android local storage but no result is found. Though the assetbundle exists in local storage of my android device.
Please somebody help me to do so, following is the code which I have written:
public GameObject obj;
IEnumerable LoadObject()
{
AssetBundle bundle = AssetBundle.CreateFromFile(Application.persistentDataPath + "/androidassetbundle4");
yield return bundle;
AssetBundleRequest request = bundle.LoadAssetAsync("boat", typeof(GameObject));
yield return request;
obj = request.asset as GameObject;
obj.transform.position = new Vector3(0.08f, -2.345f, 297.54f);
obj.transform.Rotate(350.41f,400f,20f);
obj.transform.localScale = new Vector3(1.0518f, 0.998f, 1.1793f);
Instantiate(obj);
bundle.Unload(false);
}//end of Method LoadObject
CreateFromFile is deprecated. LoadFromFile and LoadFromFileAsync are the new functions to use. Not sure if that is the problem but the code below should do it.
IEnumerator LoadObject()
{
AssetBundleCreateRequest bundle = AssetBundle.LoadFromFileAsync(System.IO.Path.Combine(Application.streamingAssetsPath, "assetbundlename"));
yield return bundle;
AssetBundle myLoadedAssetBundle = bundle.assetBundle;
if (myLoadedAssetBundle == null)
{
Debug.Log("Failed to load AssetBundle!");
yield break;
}
AssetBundleRequest request = myLoadedAssetBundle.LoadAssetAsync<GameObject>("boat");
yield return request;
obj = request.asset as GameObject;
obj.transform.position = new Vector3(0.08f, -2.345f, 297.54f);
obj.transform.Rotate(350.41f, 400f, 20f);
obj.transform.localScale = new Vector3(1.0518f, 0.998f, 1.1793f);
Instantiate(obj);
myLoadedAssetBundle.Unload(false);
}
you can't access file in local storage via CreateFromFile or LoadFromFile in android,instead use LoadFromCacheOrDownload and in android this is the address of your StreamingAssets : "jar:file://" + Application.dataPath + "!/assets/"
as mentioned in unity docs : StreamingAssets

Android: How to update Skobbler map style

I am working with Skobbler SDK 2.3 and I am wondering how to update the map styles in SKMaps.zip.
The problem:
I modified the contained "daystyle", but I noticed that this only takes effect after deleting the app data. This is really not what I want. It looks like SKPrepareMapTextureThread(android.content.Context context, java.lang.String mapTexturesPath,java.lang.String zipName, SKPrepareMapTextureListener listener).start()
only copies the files if SKMaps folder doesn't exists. Does anybody know if there is such a check inside the start() method, or (better) how to deliver modified SKMap styles to the users?
SKPrepareMapTextureThread only copies the files if SKMaps folder doesn't exists and this is how it is intended to be, as the unziping of the map resources takes a rather long time and is intended to be executed only once.
To update a style a workaround will be required:
1) delete the file .installed.txt from map resources path and call SKPrepareMapTextureThread so that the resources will be restored from assets. Although this is the easiest way, it is also the most time consuming:
final File txtPreparedFile = new File(mapResourcesDirPath + "/.installed.txt");
if (!txtPreparedFile.exists()) {
txtPreparedFile.delete();
}
new SKPrepareMapTextureThread(context, mapResourcesDirPath, "SKMaps.zip", skPrepareMapTextureListener).start();
2) a more optimal approach would be to write a routine that replace the old style with the new one
copyFile(new File("path/daystyle.json"), new File("mapResourcesDirPath/SKMaps/daystyle/daystyle.json"));
...
public static void copyFile(File sourceFile, File destFile) throws IOException {
if(!destFile.exists()) {
destFile.createNewFile();
}
FileChannel source = null;
FileChannel destination = null;
try {
source = new FileInputStream(sourceFile).getChannel();
destination = new FileOutputStream(destFile).getChannel();
// previous code: destination.transferFrom(source, 0, source.size());
// to avoid infinite loops, should be:
long count = 0;
long size = source.size();
while((count += destination.transferFrom(source, count, size-count))<size);
}
finally {
if(source != null) {
source.close();
}
if(destination != null) {
destination.close();
}
}
}

Categories

Resources