Using the new GDAA, as I understand it, in order to access a folder and its contents you have to have the folder's DriveId - because this insures that an app can only access the content it has created itself.
Now, my app uploads files to the user's Google Drive account (pictures) in a custom folder. When this folder is first created I save the DriveId of the folder to Shared Preferences so I can access the folder later for more uploads. My problem is that I would like the user to be able to access the pictures from multiple devices (so he/she e.g. can look at pictures uploaded from his/her phone while being on a tablet and vice versa), but this I cannot do without having the folder's DriveId on both devices. The only solution I can think of is sharing the DriveId between user's devices via some cloud service, but this seems awfully inconvenient for the purpose.
Any thoughts?
Shamelessly promoting myself to Cheryl's sidekick, I can give you some specific points, since I've run through this gauntlet before. This is what I did:
Create a unique 'root' for my app in the system root - "MyStupidAppRoot'. Here you'll hit the main challenge since you are creating it by name and if you base the creation on it's non-existence, you may not reliably be able to check it. But it is getting better, see SO 22382099 and SO 22515028.
Once you have a reliable anchor 'MyStupidAppRoot', you can create AppFolder mentioned by Cheryl (not available yet) or create you own visible file (again, the unique creation challenge) that can keep anything you want. Like for instance all your PREFERENCE strings. I even got so brave as to store full SQLite DB file there. It is possible since you write a byte[] buffer to a file.
Than, any other device with you app can find 'MyStupidAppRoot', get the resource file from there and read it.
It should be noted that the main difference between you own folder/file and the AppFolder is, that user's can't read the contents of an AppFolder, but can still delete it.
Here's how you can write byte[] buffer to a file. It is the 'await' version to make it simple, but there is an async version 'createFileAsync()' here.
public DriveFile createFileWait(DriveFolder fldr, String name, String mime, byte[] buff) {
DriveFile drvFile = null;
if (isConnected()) try {
ContentsResult rslt = Drive.DriveApi.newContents(_gac).await();
if (rslt.getStatus().isSuccess()) {
Contents cont = rslt.getContents();
cont.getOutputStream().write(buff);
MetadataChangeSet meta = (mime == null) ?
new MetadataChangeSet.Builder().setTitle(name).build() :
new MetadataChangeSet.Builder().setTitle(name).setMimeType(mime).build();
drvFile = fldr.createFile(_gac, meta, cont).await().getDriveFile();
}
} catch (Exception e) {}
return drvFile;
}
About the IDs:
The DriveId you mention above is an object, that can be turned into 2 different strings as discussed in SO 21800257. It is up to you which one you choose. The long one from 'encodeToString()' is easier to turn back into DriveId by 'decodeFromString()', the shorter one can be easily recognized in the http address, but takes async / await method to get back the DriveId - fetchDriveId().
You don't necessarily have to have the DriveId, its just the most sure-fire way to do it since it uniquely identifies the folder. You can also query based on the title to try to find the same folder. Assuming that the web and Android app share an app id, both should be able to access the same files.
One easy option for sharing state between apps is to make use of the newly launched App Folders (called App Data Folders on the web.) This is a hidden folder where you can store files specific to your app. Its not yet in the Android docs, but it should show up there as soon as the rollout of Google Play Services 4.3 is released. See http://android-developers.blogspot.com/2014/03/google-play-services-43.html
Related
As Android is very inconsistent between different major Versions regarding File access, I feel a bit lost.
I try to describe the problem as easy as possible:
My Company uses a commercial native DRM to protect other native library's we provide. We have a Licensing App, which invoked some Voodoo to end up with Licensing files in say /sdcard/companyname/LicenseContainer/. Other protected Apps looked at this directory in native code, checking if the user has a valid License.
The Android 10 update however, invalidated this workflow completely as it only provides scoped storage access. We could do a workaround using Storage Manager to grant access, which is unfortunately also deprecated now.
So my Question is now:
How can one App save files to a location on /sdcard/FOLDER which are
not deleted on App deletion
Accessible in native code by other apps
I'm a bit overwhelmed with all the possible solutions (SAF, FileProvider, etc), which invoke often that one app grants permissions to the other. But the files should be accessible without an installed first app who put it there.
I know there must be a solution, as recent FileManagers (i.e. Files by Google) get access to the whole /sdcard/ directory.
Whats the easiest, future-proof route to go here without invoking "hacks" like android:requestLegacyExternalStorage="true"
You may ask the user to give you access to any file or directory, including the root of internal storage or external SD card. You can make this access permanent for your app, be able to read/write files anywhere with the Scoped Storage API afterwards, until the app is uninstalled or reset.
Then, if you need to read or write a file in native C/C++ code, you may get Linux file descriptor (int number) of the file and pass it to native code to use with fdopen() call for example.
Here is a Java code snippet to get a file descriptor form a single file Uri (which in string form is like content://...)
ParcelFileDescriptor parcelFileDescriptor =
getContentResolver().openFileDescriptor(uri, "r"); // gets FileNotFoundException here, if file we used to have was deleted
int fd = parcelFileDescriptor.getFd(); // detachFd() if we want to close in native code
If you have source code for your native libraries, or can call them with C FILE* - it will work fine. The only problem is when you don't have the source code and they expect a file path/name. * UPDATE *: it is still possible to use the path/file name strings to pass to C/C++ functions that expect a file name. Simply instead of the "real path/file name", create a name to symbolic link like this:
// fd is file descriptor obtained in Java code above
char fileName[32];
sprintf(fileName, "/proc/self/fd/%d", fd);
// The above fileName can be passed to C/C++ functions that expect a file name.
// They can read or write to it, depending on permissions to fd given in Java,
// but I guess C/C++ code can not create a new file. Someone correct me,
// if I'm mistaken here.
However, at this time I'm not sure that when you create a file in a directory beyond the app "sandbox" in that way, if the system will delete this file too after uninstall... Would need to write a quick test on Android 10 to find out, and then we still won't know if Google won't change this behavior in future.
If you want to save files in shared storage (where it can be accessed by users & other apps) you need to use
for Media Files (Images, Videos, Audio, Downloads) use MediaStore
for Documents and Other Files use Storage Access Framework (this is simply a system file picker)
For instance you can use the following snippet to save a pdf file using Storage Access Framework
const val CREATE_FILE = 1
private fun createFile(pickerInitialUri: Uri) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf"
putExtra(Intent.EXTRA_TITLE, "invoice.pdf")
// Optionally, specify a URI for the directory that should be opened in
// the system file picker before your app creates the document.
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
startActivityForResult(intent, CREATE_FILE)
}
After the user has picked a directory we still need to handle the result Uri in on onActivityResult method.
override fun onActivityResult(
requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == CREATE_FILE && resultCode == Activity.RESULT_OK) {
// The result data contains a URI for directory that
// the user selected.
resultData?.data?.also { uri ->
// save your data using the `uri` path
}
}
}
You can read about this in more detail on the following blogpost
https://androidexplained.github.io/android/android11/scoped-storage/2020/09/29/file-saving-android-11.html
Background
Up until Android Q, if we wanted to get information about an APK file, we could use WRITE_EXTERNAL_STORAGE and READ_EXTERNAL_STORAGE to get access to the storage, and then use PackageManager.getPackageArchiveInfo function on the file-path.
Similar cases exist, such as using ZipFile class on a compressed file, and probably countless framework APIs and third party libraries.
The problem
Google announced a huge amount of restrictions recently on Android Q.
One of them is called Scoped Storage, which ruins storage permission when it comes to accessing all files the device has. It lets you either handle media files, or use the very restricted Storage-Access-Framework (SAF) which can't allow apps to reach and use files using File API and file-paths.
When Android Q Beta 2 was published, it broke a lot of apps because of it, including of Google. The reason was that it was turned on by default, affecting all apps, whether they target Android Q or not.
The reason is that many apps, SDKs and Android framework itself - all use File API quite often. On many cases, they also don't support InputStream or SAF-related solutions. An example for this is exactly the APK parsing example I wrote about (PackageManager.getPackageArchiveInfo).
On Q beta 3, however, things changed a bit, so that app that target Q will have the scoped storage, and there is a flag to disable it and still use the normal storage permissions and File API as usual. Sadly the flag is only temporary (read here), so it's delaying the inevitable .
What I've tried
I've tried and found the next things:
Using the storage permission indeed didn't let me read any file that's not media file (I wanted to find APK files). It's as if the files don't exist.
Using SAF, I could find the APK file, and with some workaround to find its real path (link here), I've noticed that File API can tell me that indeed the file exist, but it couldn't get its size, and the framework failed to use its path using getPackageArchiveInfo . Wrote about this here
I tried to make a symlink to the file (link here), and then read from the symlink. It didn't help.
For the case of parsing APK files, I tried to search for alternative solutions. I've found 2 github repositories that handle the APK using a File class (here and here), and one that uses InputStream instead ( here). Sadly the one that uses InputStream is very old, missing various features (such as getting the app's name and icon) and isn't going to be updated anytime soon. Besides, having a library requires maintenance to keep up with future versions of Android, otherwise it might have issues in the future, or even crash.
The questions
Generally, is there a way to still use File API when using SAF ? I'm not talking about root solutions or just copying the file to somewhere else. I'm talking about a more solid solution.
For the case of APK parsing, is there a way to overcome this issue that the framework only provides file-path as a parameter? Any workaround or a way to use InputStream perhaps?
How to handle SAF when I can only handle File or file-path? It is possible, even if you can send only a Java File object, or path string to a library function which you cannot modify:
First, obtain a Uri to a file you need to handle (in String form it would be like "content://..."), then:
try {
ParcelFileDescriptor parcelFileDescriptor =
getContentResolver().openFileDescriptor(uri, "r"); // may get FileNotFoundException here
// Obtain file descriptor:
int fd = parcelFileDescriptor.getFd(); // or detachFd() if we want to close file in native code
String linkFileName = "/proc/self/fd/" + fd;
// Call library function with path/file string:
someFunc(/*file name*/ linkFileName);
// or with File parameter
otherFunc(new File(linkFileName));
// Finally, if you did not call detachFd() to obtain the file descriptor, call:
parcelFileDescriptor.close();
// Otherwise your library function should close file/stream...
} catch (FileNotFoundException fnf) {
fnf.printStackTrace(); // or whatever
}
Posting another answer just to have more room and let me insert code snipes. Given the file descriptor as explained in my previous answer, I tried using net.dongliu:apk-parser package mentioned by #androiddeveloper in the original question, as follows (Lt.d is my shorthand to using Log.d(SOME_TAG, string...)):
String linkFileName = "/proc/self/fd/" + fd;
try (ApkFile apkFile = new ApkFile(new File(linkFileName))) {
ApkMeta apkMeta = apkFile.getApkMeta();
Lt.d("ApkFile Label: ", apkMeta.getLabel());
Lt.d("ApkFile pkg name: ", apkMeta.getPackageName());
Lt.d("ApkFile version code: ", apkMeta.getVersionCode());
String iconStr = apkMeta.getIcon();
Lt.d("ApkFile icon str: ", iconStr);
for (UseFeature feature : apkMeta.getUsesFeatures()) {
Lt.d(feature.getName());
}
}
catch (Exception ex) {
Lt.e("Exception in ApkFile code: ", ex);
ex.printStackTrace();
}
}
It gives me the correct app label, for the icon it gives me only a string to the resource directory (like "res/drawable-mdpi-v4/fex.png"), so again raw ZIP reading functions would have to be applied to read the actual icon bits. Specifically I was testing ES File Explorer Pro APK (bought this product and saved APK for my own backup, got the following output:
I/StorageTest: ApkFile Label: ES File Explorer Pro
I/StorageTest: ApkFile pkg name: com.estrongs.android.pop.pro
I/StorageTest: ApkFile version code: 1010
I/StorageTest: ApkFile icon str: res/drawable-mdpi-v4/fex.png
I/StorageTest: android.hardware.bluetooth
I/StorageTest: android.hardware.touchscreen
I/StorageTest: android.hardware.wifi
I/StorageTest: android.software.leanback
I/StorageTest: android.hardware.screen.portrait
My application will be used offline and I plan daily pull/push synchronizations via USB cable. My users are in a very primitive situation: no wifi, and no cell phone towers. My question is not about synchronization, but rather just getting access to the data so that I can synchronize.
I connect the cable, select USB for file transfer, and I can see Internal Shared Storage. But I cannot find my SqlLite database anywhere. I have tried using these paths for the database:
Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData);
Environment.GetFolderPath(Environment.SpecialFolder.Personal);
Android.App.Application.Context.FilesDir.AbsolutePath;
The app works fine with any of those paths, the data is stored and retrieved, but I cannot see the database from my PC.
I have also tried this but it blows up:
Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads).AbsolutePath;
I have tried debugging with Xamarin Live but then I get this error:
"You need to call SQLitePCL.raw.SetProvider();"
I have tried adding console.writeline and Log.Error to add in some diagnostics but I can't find any log files in Internal Shared Storage.
I have WRITE_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions in the manifest; even tho I think that is not necessary.
If I could store files in Internal Shared Storage then I could put the database there and access it to synchronize. And I could create a simple text logging facility to write a text log to the same place.
I have rarely asked for help in 40 years but I've been at this for days. Thanks!
To get files onto Internal Shared Storage accessible via USB took 3 steps:
1: Get runtime permissions
ActivityCompat.RequestPermissions(activity, new String[] { Manifest.Permission.WriteExternalStorage, Manifest.Permission.ReadExternalStorage }, 1);
2) use this path:
string extPath = Android.OS.Environment.ExternalStorageDirectory.AbsolutePath;
extPath = Path.Combine(extPath, "RtTrace.txt");
File.AppendAllText(extPath, "new content" + System.Environment.NewLine);
3) Media scan the resulting file to make it visible:
MediaScannerConnection.ScanFile(Android.App.Application.Context, new String[] { extPath }, null, null);
The path on Environment.SpecialFolder.Personal refers to a private area where only the app (and the OS itself when you clear data from the app, for example) have access. I don't know about ApplicationData.
You can easily copy your app file (the protected one) to a public folder, like Downloads or create a new folder MyAppDirectory at the public storage space, that will allow access from other devices. Then, you can clear local data that you don't need anymore (after the sync process).
To create a new public folder on Android:
var folder = System.IO.Path.Combine(Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, "MyAppDirectory");
System.IO.Directory.CreateDirectory(folder);
You'll get this:
Then, copy the file:
var newFile = Path.Combine(folder, "MySharedFile"); // The database, xml, json, text or any file you want to share
var sourceFullName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal),"MyOriginalFile");
System.IO.File.Copy(sourceFullName, newFile, true /* overwrite */);
I hope it helps.
SQLite db is stored in application private memory and can't be accessed over USB, try exporting contents of your DB to an external file in your internal memory or External memory to be able to access it via a USB chord.
I am making a notepad app using Google Drive. Whenever user creates a file in the app, it also creates a file in the Google Drive. And user can enter the text and save the file, the unsaved text will get committed whenever the internet is available. I am managing the update and create processes within the app using the DriveId.
If the user wants to use the files with the alternative device using my app, for that I also have the option called DriveId import. By clicking the option DriveId import user will be prompted with the input box for entering the existing DriveId. Using the DriveId I thought of opening the files, But it was giving an error.
Then I saw an answer given in this SO which clearly says DriveId can be used only inside the app and device which created the file.
I also found a similar question like mine in here SO But I can’t get my problem solved. I have taken ResourceId using result.getDriveFolder().getDriveId().getResourceId()
How to read the data’s programmatically using the ResourceID? As said in the above answer here I don’t want to change the track and go into Drive REST API. Is there a way that I can read the data using Google Drive Android API ? I have done all the development process, but in the ending when I try to access from other device it is giving the error. Totally struck.
If I can only read the data using REST API any simple code will be appreciated. Thanks in advance.
Finally Solved the DriveId Issue without REST API.
To get DriveId on the alternative device. You will need resourceId. You can use following code:-
String resourseId = "xxxxxxxxxxxxx"
Drive.DriveApi.fetchDriveId(mGoogleApiClient,resourseId).setResultCallback(idCallBack);
private ResultCallBack<DriveApi.DriveResult> idCallBack = new ResultCallback<DriveApi.DriveIdResult>() {
#Override
public void onResult(DriveApi.DriveIdResult driveIdResult) {
msg.Log("onResult");
DriveId id = driveIdResult.getDriveId(); //Here you go :)
}
}
I have been on to this topic since 4 days. I want to download the files listed in Google Cloud Storage into my App. For this :
1) I have created my application in GAE and set my Google Cloud Storage project and created my bucket.
2) Now I want to write a servlet which List the files inside "mybucket" and serve it with blob which I will read inside my android app (via Http response )and save to the SD card or memory. To do this I have collected piece of code , but I have no clue how to put it together.
I am not even sure if I am doing it right. ANy help or link or suggestions are highly appreciated. Please let me know if you need any further information.
Code which will go in servlet to implement the step 2):
Storage.Objects.List listObjects = storage.objects().list("mybucket");
Objects objects;
do {
objects = listObjects.execute();
for (StorageObject object : objects.getItems()) {
// Do things!
}
listObjects.setPageToken(objects.getNextPageToken());
} while (null != objects.getNextPageToken());
BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService();
BlobKey blobKey = blobstoreService.createGsBlobKey(
"/gs/"+"androidbucket"+"/"+filename");
blobstoreService.serve(blobKey, resp);
Now my question regarding this code snippet is how "Storage.Objects.List" is going to know under which project it has look for "mybucket"?? Because I read this line under my application settings on GAE "When creating new buckets, or listing the buckets owned by a project, you must specify the x-goog-project-id HTTP header in order to identify which project you are using."
EDIT: Also I am not able to compile my servlet on "Storage.Objects.List". I have downloaded Cloud Storage API from this link :https://code.google.com/p/google-api-java-client/wiki/APIs#Cloud_Storage_API .. Thanks!!
Your quote:
When creating new buckets, or listing the buckets owned by a project,
you must specify the x-goog-project-id HTTP header in order to
identify which project you are using.
This refers to listing buckets. You are listing objects in a bucket.
If you were retrieving a list of buckets, you'd have to pass in a project ID. The reference for buckets.list shows how to do this:
Storage.Buckets.List listBuckets = storage.buckets().list("myproject");