Just got a Project Tango Development Kit tablet and have worked through some of the demos and examples.
Some older blog posts use the log files from a "Tango Mapper" application that should be preloaded on the device.
Interactive Visualization of Google Project Tango Data with ParaView
Ologic Announces integration between ROS and Project Tango
Google Tango and ROS integration at Bosch
Mapping Hints and Tips
Unfortunately, the "Tango Mapper" application did not come preloaded on my device and I can't seem to find it on the Play Store.
Is there some other method to simply export or retrieve the PointCloud data for downstream rendering?
[Model number: yellowstone, Tango Core Version: 1.1:2014.11.14-bernoulli-release]
Not sure if you ever got to solve this, but I was able to find the APK along with a method to export using Tango updated tablet version. I successfully exported the point cloud data using the method described in this blog.
http://www.kitware.com/blog/home/post/838
Edit
Procedure download the APK or use the source code found found in the GITHUB project folder.
Once that is done boot up the app as you normally would. There will a slider record, and auto. If you slide record it will only wait until you hit the snap shot button to record the point cloud data you are currently viewing.
If you slide the auto it will continuously record the point cloud data and create files as it tracks where you are moving. Keep in mind the larger the file the larger it takes to save as a zip.
Once done slide the record and it will prompt you to save and send.
I find it easier to save to the Google Drive as other the other methods sometimes fail to send.
Once done download the free Paraview App found http://www.paraview.org/download/ load up your Point cloud data.
It should be two files one your pose data and the other point cloud. (you could individually load each data using the collapse arrow you see before importing it in.)
That will be it you will be able to see your data and actually play back the animation of you recording it because of your pose data collected.
( only wrote this out because you were looking for an easier way to export data) This is probably the easiest. You could take said data and begin to reconstructed the room based on the pose data collected.)
all credit for source code and tutorial goes to the The Kitware blog
If links are broken DM me and I will send the file to you.
APK is found here
APK DOWNLOAD
they also have listed their source code at the bottom of the blog. It is based on the tango Explorer found in the app store.
Tango Mapper is an internal tool, and it's currently not public to developers. I think the best way to log the point cloud data is using the c or java example code provided, and maybe do some small modification to log the data to a file.
c example: https://github.com/googlesamples/tango-examples-c
java example: https://github.com/googlesamples/tango-examples-java
Sparse mapping: https://www.youtube.com/watch?v=x5C_HNnW_3Q
More indoor mapping: https://www.youtube.com/watch?v=3BNOsxMZD14
It appears that more than a few of the contributors to the Tango project were hired or bought by google. As an example most of the links to code and/or articles by Hidof are MIA, only a facebook page with few clues remains. The internet archive's wayback machine has a few snapshots of their website for the curious.
Go take a look at the Java Point Cloud sample on GitHub - The function you want to look at is onXyzIsAvailable in PointCloudActivity. Extracting a few relevant lines....
public void onXyzIjAvailable(final TangoXyzIjData xyzIj) {
....
byte[] buffer = new byte[xyzIj.xyzCount * 3 * 4];
FileInputStream fileStream = new FileInputStream(
xyzIj.xyzParcelFileDescriptor.getFileDescriptor());
try {
fileStream.read(buffer,
xyzIj.xyzParcelFileDescriptorOffset, buffer.length);
fileStream.close();
} catch (IOException e) {
e.printStackTrace();
}
At this point buffer contains the point cloud data - I would strongly recommend you ship this off the device via a binary service call, as I think making the poor thing try and convert it to JSON or XML would make things slower than you would like
Thank you Mark for your advice. I am a novice programmer and it is my first time working with java...
I am interested in exporting the Tango acquired PointCloud data to a file and I would like to ask for your feedback on my approach (I created a Save button, and onClick the data would be saved to a file on an external drive). Please find the code bellow for the part that should save the xyzIj data:
#Override
public void onClick(View v) {
switch (v.getId()) {
...
case R.id.save_button:
savePointCloud();
break;
default:
Log.w(TAG, "Unrecognized button click.");
}
}
private static void savePointCloud(final TangoXyzIjData xyzIj, String file) {
File directoryName = getAlbumStorageDir(file);
FileOutputStream out = new FileOutputStream(directoryName,"text.txt");
byte[] buffer = new byte[xyzIj.xyzCount * 3 * 4];
FileInputStream fileStream = new FileInputStream(
xyzIj.xyzParcelFileDescriptor.getFileDescriptor());
int read;
while ((read=fileStream.read(buffer))!=1){
try{
out.write(buffer, 0, read);
out.close();
System.out.println("Printed to file");
}catch(IOException e){e.printStackTrace();}
}
}
public File getAlbumStorageDir(String dirName) {
if (!isExternalStorageWritable()) {
return null;
} else {
// Get the directory for the user's public downloads directory.
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS), dirName);
if (!file.mkdirs() || !file.exists()) {
Log.e(TAG, "Directory not created");
return null;
}
return file;
}
}
public boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
if ((Environment.MEDIA_MOUNTED.equals(state)
&& Environment.MEDIA_MOUNTED_READ_ONLY.equals(state))) {
return true;
} else {
Log.e(TAG, "External storage is not mounted READ/WRITE.");
return false;
}
}
Related
So in my project I have an image object that loads various different sprites throughout the game. In order to implement this, I have a folder with a bunch of .pngs that the game accesses. In order for this to work in a standalone build I put these images in the following path:
StreamingAssets/Question Images
However, in Android I am getting an error because, as the manual says:
"On Android, the files are contained within a compressed .jar file (which is essentially the same format as standard zip-compressed files). This means that if you do not use Unity’s WWW class to retrieve the file, you need to use additional software to see inside the .jar archive and obtain the file.
The thing is I have no idea how to go about implementing this, any ideas?
From Application.streamingAssetsPath
It is not possible to access the StreamingAssets folder on WebGL and Android platforms. No file access is available on WebGL. Android uses a compressed .apk file. These platforms return a URL. Use the UnityWebRequest class to access the Assets.
Example using UnityWebRequestTexture
private void Start()
{
StartCoroutine(GetTexture());
}
private IEnumerator GetTexture()
{
// in general I would always avoid to have spaces in file-paths
var path = Path.Combine(Application.streamingAssetsPath, "QuestionImages", "exampleImage.png");
using(var www = UnityWebRequestTexture.GetTexture(path))
{
yield return www.SendWebRequest();
if(www.isNetworkError || www.isHttpError)
{
Debug.LogErrorFormat(this, "Unable to load texture due to {0} - {1}", www.responseCode, www.error);
}
else
{
Texture myTexture = ((DownloadHandlerTexture)www.downloadHandler).texture;
}
}
}
Note: Typed on smartphone so no warrenty but I hope the idea gets clear.
I implemented the uploading function the file to Cloudinary.
Cloudinary cloudinary = new Cloudinary(Constants.CLOUDINARY_URL);
try {
FileInputStream is = new FileInputStream(new File(filePath));
Uploader uploader = cloudinary.uploader();
Map map = uploader.upload(is, new HashMap());
return map;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
Now I would like to show the uploading percent in progress bar while upload.
But I can't find any callback function to get uploading percent.
Please help me.
Thanks.
cprakashagr answer actually works wonders. Just to elaborate on his answer, you will need to download the latest cloudinary library off github by doing this:
git clone https://github.com/cloudinary/cloudinary_java.git
Then you will have to make the changes according to his link:
https://github.com/cloudinary/cloudinary_java/pull/41
You cannot use Android Studio to make these changes as this is a maven project so you will need to use another IDE, like IntelliJ for example.
This is how I made the changes:
You will need to add on the class: UploadCallback
You will need to replace the class Uploader with his class: https://github.com/cprakashagr/cloudinary_java/blob/master/cloudinary-core/src/main/java/com/cloudinary/Uploader.java
Once you have done this, go into the main folder of your cloudinary_java, and then enter into Terminal. Go and type in mvn install and press enter. You will see that mvn will build the jars for the different folders in your directory. After the build is complete, if you navigate into the cloudinary-core folder for example, you will see the cloudinary-core jar.
You must copy the cloudinary-core jar from the folder and add it into your android project "libs" folder in the root of your android project. Once you have done that and it appears there, right click on the jar in Android Studios and click "Add as Library" so that android studios will add it as a dependency.
The cloudinary-android jar does not need to be added as a jar as you can grab a copy off gradle. Your final gradle file should look something like this:
compile('com.cloudinary:cloudinary-android:1.2.2') {
exclude module: 'cloudinary-core'
}
compile files('/Users/XXX/Documents/myApp/libs/cloudinary-core-1.4.2-SNAPSHOT.jar')
Rebuild your android studio project and you will see that the UploadCallback is now an object you can use in your android project. This is the way you know your jar build was successfully modified.
Now inside a service class, add your code for cloudinary direct upload. You will need to put the code inside a service class because you cannot do network operations on a UI thread and you will get an error if you tried:
Map config = new HashMap();
config.put("cloud_name", "XXX");
Cloudinary mobileCloudinary = new Cloudinary(config);
Map map = null;
try {
map = mobileCloudinary.uploader()
.uploadLarge(this,
intent.getSerializableExtra(getString(R.string.file)),
ObjectUtils.asMap("public_id", 123),
"tags", "myphoto",
"folder", "mylibrary",
"unsigned", true,
"upload_preset", "XXX"),
51200);
} catch (IOException e) {
e.printStackTrace();
}
String imageUrl = map.get("url").toString();
Timber.e("imageUrl " + imageUrl);
You must set the buffer to be large enough (but not too large) to make sure your callback is actually called. When I first tried this code out, I set the buffer to something really large, like 200000000, and the callback was not called because the upload would have happened all in one go. If you set it to a small value, like 2048, the callback will be called regularly however, the upload will become really slow. It is up to you to determine an adequate size which will work well for your app. For me it was 51200 which means for every 50kb of the file which is uploaded, a callback will occur.
Once it is working, you will need to transmit the progress information from the service class back to your activity class so you can display the progress on screen. I use a messageHandler to do so. This is the message method in my service class:
public void sendMessage(float progress) {
Message message = Message.obtain();
message.arg1 = Math.round(progress);
try {
messageHandler.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}
This is my messageHandler on my activity class:
public class MessageHandler extends Handler {
#Override
public void handleMessage(final Message message) {
Timber.e("transfer: " + message.arg1);
}
}
The callback is not implemented from the cloudinary team yet. Though there have been few works around.
Check this link.
You need to import or build new JARs from the cloudinary source. For that, either you use this repository or make required changes to their original repository.
Let me know if you have some questions.
EDIT: On how to use it
Import these two cloudinary sources in your Android project (src/java dir):
Cloudinary Core Lib Source
Cloudinary Android Lib Source
Do changes as per my pull request.
And call the method uploadLarge as per the changed signature. Like:
cloudinary.uploader().uploadLarge(UploadCallback : uploadCallback, File : file, Map : options, int : bufferSize);
For buffer size, please assure you use at least 5242881. This is the limitation from cloudinary server. The minimum packet needs to be 5.001 MB at least.
Lots of Intent actions, like ACTION_VIEW, take a Uri pointing to the content the action should be performed upon. If the content is backed by a file -- whether the Uri points directly to the file, or to a ContentProvider serving the file (see FileProvider) -- this generally works.
There are scenarios in which developers do not want to have the content reside in a file for sharing with other apps. One common scenario is for encryption: the decrypted data should reside in RAM, not on disk, to minimize the risk of somebody getting at that decrypted data.
My classic solution to sharing from RAM is to use ParcelFileDescriptor and createPipe(). However, when the activity responding to ACTION_VIEW (or whatever) gets an InputStream on that pipe, the resulting stream is limited compared to the streams you get when the ContentProvider is serving up content from a file. For example, this sample app works fine with Adobe Reader and crashes QuickOffice.
Based on past related questions, my assumption is that createPipe() is truly creating a pipe, and that pipes are non-seekable. Clients that attempt to "rewind" or "fast forward" run into problems as a result.
I am seeking a reliable solution for sharing in-memory content with a third-party app that gets around this limitation. Specifically:
It has to use a Uri syntax that is likely to be honored by client apps (i.e., ACTION_VIEW implementers); solutions that involve something obtuse that client apps are unlikely to recognize (e.g., pass such-and-so via an Intent extra) do not qualify
The data to be shared cannot be written to a file as part of the sharing (of course, the client app could wind up saving the received bytes to disk, but let's ignore that risk for the moment)
Ideally it does not involve the app looking to share the data opening up a ServerSocket or otherwise exacerbating security risks
Possible suggested ideas include:
Some way to reconfigure createPipe() that results in a seekable pipe
Some way to use a socket-based FileDescriptor that results in a seekable pipe
Some kind of RAM disk or something else that feels like a file to the rest of Android but is not persistent
A key critierion, if you will, of a working solution is if I can get a PDF served from RAM that QuickOffice can read.
Any suggestions?
Thanks!
You've posed a really difficult combination of requirements.
Lets look at your ideas for solutions:
Possible suggested ideas include:
Some way to reconfigure createPipe() that results in a seekable pipe
Some way to use a socket-based FileDescriptor that results in a seekable pipe
Some kind of RAM disk or something else that feels like a file to the rest of Android but is not persistent
The first one won't work. This issue is that the pipe primitive implemented by the OS is fundamentally non-seekable. The reason is supporting seek that would require the OS to buffer the entire pipe "contents" ... until the reading end closes. That is unimplementable ... unless you place a limit on the amount of data that can be sent through the pipe.
The second one won't work either, for pretty much the same reason. OS-level sockets are not seekable.
At one level, the final idea (a RAM file system) works, modulo that such a capability is supported by the Android OS. (A Ramfs file is seekable, after all.) However, a file stream is not a pipe. In particular the behaviour with respect to the end-of-file is different for a file stream and a pipe. And getting a file stream to look like a pipe stream from the perspective of the reader would entail some special code on that side. (The problem is similar to the problem of running tail -f on a log file ...)
Unfortunately, I don't think there's any other way to get a file descriptor that behaves like a pipe with respect to end-of-file and is also seekable ... short of radically modifying the operating system.
If you could change the application that is reading from the stream, you could work around this. This is precluded by the fact that the fd needs to be read and seeked by QuickOffice which (I assume) you can't modify. (But if you could change the application, there are ways to make this work ...)
By the way, I think you'd have the some problems with these requirements on Linux or Windows. And they are not Java specific.
UPDATE
There have been various interesting comments on this, and I want to address some here:
The OP has explained the use-case that is motivating his question. Basically, he wants a scheme where the data passing through the "channel" between the applications is not going to be vulnerable in the event that the users device is stolen (or confiscated) while the applications are actually running.
Is that achievable?
In theory, no. If one postulates a high degree of technical sophistication (and techniques that the public may not know about ...) then the "bad guys" could break into the OS and read the data from shared memory while the "channel" remained active.
I doubt that such attacks are (currently) possible in practice.
However, even if we assume that the "channel" writes nothing to "disc" there could still be traces of the channel in memory: e.g.
a still mounted RAMfs or still active shared memory segments, or
remnants of previous RAMfs / shared memory.
In theory, this data could in theory be retrieved, provided that the "bad guy" doesn't turn of or reboot the device.
It has been suggested that ashmem could be used in this context:
The issue of there being no public Java APIs could be addressed (by writing 3rd-party APIs, for example)
The real stumbling block is the need for a stream API. According the "ashmem" docs, they have a file-like API. But I think that just means that they conform to the "file descriptor" model. These FDs can be passed from one application to another (across fork / exec), and you use "ioctl" to operate on them. But there is no indication that they implement "read" and "write" ... let alone "seek".
Now, you could probably implement a read/write/seekable stream on top of ashmem, using native and Java libraries on both ends of the channel. But both applications would need to be "aware" of this process, probably to the level of providing command line options to set up the channel.
These issues also apply to old-style shmem ... except that the channel setup is probably more difficult.
The other potential option is to use a RAM fs.
This is easier to implement. The files in the RAMfs will behave like "normal" files; when opened by an application you get a file descriptor that can be read, written and seeked ... depending on how it was opened. And (I think) you should be able to pass a seekable FD for a RAMfs file across a fork/exec.
The problem is that the RAMfs needs to be "mounted" by the operating system in order to use it. While it is mounted, another (privileged) application can also open and read files. And the OS won't let you unmount the RAMfs while some application has open fds for RAMfs files.
There is a (hypothetical) scheme that partly mitigates the above.
The source application creates and mounts a "private" RAMfs.
The source application creates/opens the file for read/write and then unlinks it.
The source application writes the file using the fd from the open.
The source application forks / execs the sink application, passing the fd.
The sink application reads from the (I think) still seekable fd, seeking as required.
When the source application notices that the (child) sink application process has exited, it unmounts and destroys the RAMfs.
This would not require modifying the reading (sink) application.
However, a third (privileged) application could still potentially get into the RAMfs, locate the unlinked file in memory, and read it.
However, having re-reviewed all of the above, the most practical solution is still to modify the reading (sink) application to read the entire input stream into a byte[], then open a ByteArrayInputStream on the buffered data. The core application can seek and reset it at will.
It's not a general solution to your problem, but opening a PDF in QuickOffice works for me with the following code (based on your sample):
#Override
public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
try {
byte[] data = getData(uri);
long size = data.length;
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
new TransferThread(new ByteArrayInputStream(data), new AutoCloseOutputStream(pipe[1])).start();
return new AssetFileDescriptor(pipe[0], 0, size);
} catch (IOException e) {
e.printStackTrace();
}
return null;
};
private byte[] getData(Uri uri) throws IOException {
AssetManager assets = getContext().getResources().getAssets();
InputStream is = assets.open(uri.getLastPathSegment());
ByteArrayOutputStream os = new ByteArrayOutputStream();
copy(is, os);
return os.toByteArray();
}
private void copy(InputStream in, OutputStream out) throws IOException {
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
out.flush();
out.close();
}
#Override
public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sort) {
if (projection == null) {
projection = new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
}
String[] cols = new String[projection.length];
Object[] values = new Object[projection.length];
int i = 0;
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = url.getLastPathSegment();
}
else if (OpenableColumns.SIZE.equals(col)) {
cols[i] = OpenableColumns.SIZE;
values[i++] = AssetFileDescriptor.UNKNOWN_LENGTH;
}
}
cols = copyOf(cols, i);
values = copyOf(values, i);
final MatrixCursor cursor = new MatrixCursor(cols, 1);
cursor.addRow(values);
return cursor;
}
private String[] copyOf(String[] original, int newLength) {
final String[] result = new String[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
private Object[] copyOf(Object[] original, int newLength) {
final Object[] result = new Object[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
I believe you're looking for StorageManager.openProxyFileDescriptor, function added in API 26. This will give you ParcelFileDescriptor, needed for your ContentProvider.openAssetFile to work. But you can also grab its file descriptor and use it in file I/O: new FileInputStream(fd.getFileDescriptor())
In function description is :
This can be useful when you want to provide quick access to a large file that isn't backed by a real file on disk, such as a file on a
network share, cloud storage service, etc. As an example, you could
respond to a ContentResolver#openFileDescriptor(android.net.Uri,
String) request by returning a ParcelFileDescriptor created with this
method, and then stream the content on-demand as requested. Another
useful example might be where you have an encrypted file that you're
willing to decrypt on-demand, but where you want to avoid persisting
the cleartext version.
It works with ProxyFileDescriptorCallback, which is your function to provide I/O, mainly read pieces of your file from various offsets (or decrypt it, read from network, generate, etc).
As I tested, it's well suited also for video playback over content:// scheme, because seeking is efficient, no seek-by-read as is the option for pipe-based approach, but Android really asks relevant fragments of your file.
Internally Android uses some fuse driver to transfer the data between processes.
I've been experimenting with #josias code. I found some of the query(...) calls came with a projection of _data. Including the data for that column and setting the actual length means more file types can be opened in more apps. Always including _data even when not in the passed in projection allows opening even more file types.
Here is what I ended up with:
private static final String[] PROJECTION = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE, "_data"};
#Override
public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sort) {
byte[] data = getData(mSourcePath, url);
final MatrixCursor cursor = new MatrixCursor(PROJECTION, 1);
cursor.newRow()
.add(url.getLastPathSegment())
.add(data.length)
.add(data);
return cursor;
}
I have yet another hurdle to climb with my GOOGLE DRIVE SDK Android App. I am uploading scanned images with tightly controlled index fields - user defined 'tags' from local dictionary. For instance XXX.JPG has index words "car" + "insurance". Here is a simplified code snippet:
...
body.setTitle("XXX.JPG");
body.setDescription("car, insurance");
body.setIndexableText(new IndexableText().setText("car insurance"));
body.setMimeType("image/jpeg");
body.setParents(Arrays.asList(new ParentReference().setId(...)));
FileContent cont = new FileContent("image/jpeg", new java.io.File(fullPath("xxx.jpg")));
File gooFl = _svc.files().insert(body, cont).execute();
...
Again, everything works great, except when I start a search, I get results that apparently come from some OCR post process, thus rendering my system's DICTIONARY unusable. I assume I can use a custom MIME type, but then the JPEG images become invisible for users who use standard GOOGLE DRIVE application (local, browser-based ... ). So the question is: Can I upload MIME "image/jpeg" files with custom indexes (either Indexable, or Description fields) but stop GOOGLE from OCR-ing my files and adding indexes I did not intend to have?
Just to be more specific, I search for "car insurance" and instead of my 3 files I indexed this way, I get unmanageable pile of other results (JPEG scanned documents) that had "car" and "insurance" somewhere in them. Not what my app wants.
Thank you in advance, sean
...
Based on Burcu's advise below, I modified my code to something that looks like this (stripped to bare bones):
// define meta-data
File body = new File();
body.setTitle("xxx.jpg");
body.setDescription(tags);
body.setIndexableText(new IndexableText().setText(tags));
body.setMimeType("image/jpeg");
body.setParents(Arrays.asList(new ParentReference().setId(_ymID)));
body.setModifiedDate(DateTime.parseRfc3339(ymdGOO));
FileContent cont =
new FileContent("image/jpeg",new java.io.File(fullPath("xxx.jpg")));
String sID = findOnGOO(driveSvc, body.getTitle());
// file not found on gooDrive, upload and fix the date
if (sID == null) {
driveSvc.files().insert(body, cont).setOcr(false).execute();
driveSvc.files().patch(gooFl.getId(), body).setOcr(false).setSetModifiedDate(true).execute();
// file found on gooDrive - modify metadata and/or body
} else {
// modify content + metadata
if (contentModified) {
driveSvc.files().update(sID, body, cont).setOcr(false).setSetModifiedDate(true).execute();
// only metadata (tags,...)
} else {
driveSvc.files().patch(sID, body).setOcr(false).setSetModifiedDate(true).execute();
}
}
...
It is a block that uploads or modifies a Google Drive file. The two non-standard operations are:
1/ resetting the file's 'modified' date in order to force the date of file creation - tested, works OK
2/ stopping the OCR process that interferes with my apps indexing scheme - will test shortly and update here
For the sake of simplicity, I did not include the implementation of "findInGOO()" method. It is quite simple 2-liner and I can supply it upon request
sean
On insertion, set the ocr parameter to false:
service.files().update(body, content).setOcr(false).execute();
I've spent the last six hours pouring over documents from Google and I still don't know how to get started with this. All I want to do is make it so my existing Android app can read files from Google Drive, upload new files to Google Drive, and edit existing files on Google Drive.
I've read that Drive SDK v2 was focused solely on making it easy for Android (and mobile in general) developers to use it, and yet there seems to be virtually nothing in their documentation about it.
Ideally, I'd like someone to point at some decent documentation, example, or tutorial covering how to do this (keep in mind I'm using Android. They have plenty of stuff on how to use Drive with the Google App Engine; I have already looked at it and I have no idea how to go from that to an Android app.)
I need to know which libraries I need to download and add to my project, what I need to add to my manifest, and how I can ultimately get a list of files from Google Drive, download one, and then upload a modified version.
Ideally, I'd like it to handle accounts automatically, the way that the officially Google Drive app does.
Edit: Claudio Cherubino says that Google Play Services is now available and will make this process a lot easier. However, there's no sample code available (yet, he says it's coming soon... they said Google Play Services was "coming soon" 4 months ago, so there's a good chance this answer will continue to be the only completely working example of accessing Google Drive from your Android application into 2013.)
Edit 2X: Looks like I was off by about a month when I said Google wouldn't have a working example until next year. The official guide from Google is over here:
https://developers.google.com/drive/quickstart-android
I haven't tested their methods yet, so it's possible that my solutions from September 2012 (below) are still the best:
Google Play Services is NOT REQUIRED for this. It's a pain in the butt, and I spent well over 50 hours (edit: 100+ hours) figuring it all out, but here's a lot of things that it'll help to know:
THE LIBRARIES
For Google's online services in general you'll need these libraries in your project: (Instructions and Download Link)
google-api-client-1.11.0-beta.jar
google-api-client-android-1.11.0-beta.jar
google-http-client-1.11.0-beta.jar
google-http-client-android-1.11.0-beta.jar
google-http-client-jackson-1.11.0-beta.jar
google-oauth-client-1.11.0-beta.jar
guava-11.0.1.jar
jackson-core-asl-1.9.9.jar
jsr305-1.3.9.jar
For Google Drive in particular you'll also need this:
google-api-services-drive-v2-rev9-1.8.0-beta.jar (Download Link)
SETTING UP THE CONSOLE
Next, go to Google Console. Make a new project. Under Services, you'll need to turn on two things: DRIVE API and DRIVE SDK! They are separate, one does not automatically turn the other on, and YOU MUST TURN BOTH ON! (Figuring this out wasted at least 20 hours of my time alone.)
Still on the console, go to API Access. Create a client, make it an Android app. Give it your bundle ID. I don't think the fingerprints thing is actually important, as I'm pretty sure I used the wrong one, but try to get that right anyways (Google provides instructions for it.)
It'll generate a Client ID. You're going to need that. Hold onto it.
Edit: I've been told that I'm mistaken and that you only need to turn on Drive API, Drive SDK doesn't need to be turned on at all, and that you just need to use the Simple API Key, not set up something for Android. I'm looking into that right now and will probably edit this answer in a few minutes if i figure it out...
THE ANDROID CODE - Set Up and Uploading
First, get an auth token:
AccountManager am = AccountManager.get(activity);
am.getAuthToken(am.getAccounts())[0],
"oauth2:" + DriveScopes.DRIVE,
new Bundle(),
true,
new OnTokenAcquired(),
null);
Next, OnTokenAcquired() needs to be set up something like this:
private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
#Override
public void run(AccountManagerFuture<Bundle> result) {
try {
final String token = result.getResult().getString(AccountManager.KEY_AUTHTOKEN);
HttpTransport httpTransport = new NetHttpTransport();
JacksonFactory jsonFactory = new JacksonFactory();
Drive.Builder b = new Drive.Builder(httpTransport, jsonFactory, null);
b.setJsonHttpRequestInitializer(new JsonHttpRequestInitializer() {
#Override
public void initialize(JSonHttpRequest request) throws IOException {
DriveRequest driveRequest = (DriveRequest) request;
driveRequest.setPrettyPrint(true);
driveRequest.setKey(CLIENT ID YOU GOT WHEN SETTING UP THE CONSOLE BEFORE YOU STARTED CODING)
driveRequest.setOauthToken(token);
}
});
final Drive drive = b.build();
final com.google.api.services.drive.model.File body = new com.google.api.services.drive.model.File();
body.setTitle("My Test File");
body.setDescription("A Test File");
body.setMimeType("text/plain");
final FileContent mediaContent = new FileContent("text/plain", an ordinary java.io.File you'd like to upload. Make it using a FileWriter or something, that's really outside the scope of this answer.)
new Thread(new Runnable() {
public void run() {
try {
com.google.api.services.drive.model.File file = drive.files().insert(body, mediaContent).execute();
alreadyTriedAgain = false; // Global boolean to make sure you don't repeatedly try too many times when the server is down or your code is faulty... they'll block requests until the next day if you make 10 bad requests, I found.
} catch (IOException e) {
if (!alreadyTriedAgain) {
alreadyTriedAgain = true;
AccountManager am = AccountManager.get(activity);
am.invalidateAuthToken(am.getAccounts()[0].type, null); // Requires the permissions MANAGE_ACCOUNTS & USE_CREDENTIALS in the Manifest
am.getAuthToken (same as before...)
} else {
// Give up. Crash or log an error or whatever you want.
}
}
}
}).start();
Intent launch = (Intent)result.getResult().get(AccountManager.KEY_INTENT);
if (launch != null) {
startActivityForResult(launch, 3025);
return; // Not sure why... I wrote it here for some reason. Might not actually be necessary.
}
} catch (OperationCanceledException e) {
// Handle it...
} catch (AuthenticatorException e) {
// Handle it...
} catch (IOException e) {
// Handle it...
}
}
}
THE ANDROID CODE - Downloading
private java.io.File downloadGFileToJFolder(Drive drive, String token, File gFile, java.io.File jFolder) throws IOException {
if (gFile.getDownloadUrl() != null && gFile.getDownloadUrl().length() > 0 ) {
if (jFolder == null) {
jFolder = Environment.getExternalStorageDirectory();
jFolder.mkdirs();
}
try {
HttpClient client = new DefaultHttpClient();
HttpGet get = new HttpGet(gFile.getDownloadUrl());
get.setHeader("Authorization", "Bearer " + token);
HttpResponse response = client.execute(get);
InputStream inputStream = response.getEntity().getContent();
jFolder.mkdirs();
java.io.File jFile = new java.io.File(jFolder.getAbsolutePath() + "/" + getGFileName(gFile)); // getGFileName() is my own method... it just grabs originalFilename if it exists or title if it doesn't.
FileOutputStream fileStream = new FileOutputStream(jFile);
byte buffer[] = new byte[1024];
int length;
while ((length=inputStream.read(buffer))>0) {
fileStream.write(buffer, 0, length);
}
fileStream.close();
inputStream.close();
return jFile;
} catch (IOException e) {
// Handle IOExceptions here...
return null;
}
} else {
// Handle the case where the file on Google Drive has no length here.
return null;
}
}
One last thing... if that intent gets sent off, you'll need to handle when it returns with a result.
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 3025) {
switch (resultCode) {
case RESULT_OK:
AccountManager am = AccountManager.get(activity);
am.getAuthToken(Same as the other two times... it should work this time though, because now the user is actually logged in.)
break;
case RESULT_CANCELED:
// This probably means the user refused to log in. Explain to them why they need to log in.
break;
default:
// This isn't expected... maybe just log whatever code was returned.
break;
}
} else {
// Your application has other intents that it fires off besides the one for Drive's log in if it ever reaches this spot. Handle it here however you'd like.
}
}
THE ANDROID CODE - Updating
Two quick notes on updating the last modified date of a file on Google Drive:
You must provide a fully initialized DateTime. If you do not, you'll get a response of "Bad Request" from Google Drive.
You must use both setModifiedDate() on the File from Google Drive and setSetModifiedDate(true) on the update request itself. (Fun name, huh? "setSet[...]", there's no way people could mistype that one...)
Here's some brief sample code showing how to do an update, including updating the file time:
public void updateGFileFromJFile(Drive drive, File gFile, java.io.File jFile) throws IOException {
FileContent gContent = new FileContent("text/csv", jFile);
gFile.setModifiedDate(new DateTime(false, jFile.lastModified(), 0));
gFile = drive.files().update(gFile.getId(), gFile, gContent).setSetModifiedDate(true).execute();
}
THE MANIFEST
You'll need the following permissions: GET_ACCOUNTS, USE_CREDENTIALS, MANAGE_ACCOUNTS, INTERNET, and there's a good chance you'll need WRITE_EXTERNAL_STORAGE as well, depending on where you'd like to store the local copies of your files.
YOUR BUILD TARGET
Right click your project, go into it's properties, and under Android change the build target to Google APIs if you must. If they aren't there, download them from the android download manager.
If you're testing on an emulator, make sure its target is Google APIs, not generic Android.
You'll need a Google Account set up on your test device. The code as written will automatically use the first Google Account it finds (that's what the [0] is.) IDK if you need to have downloaded the Google Drive app for this to have worked. I was using API Level 15, I don't know how far back this code will work.
THE REST
The above should get you started and hopefully you can figure your way out from there... honestly, this is just about as far as I've gotten so far. I hope this helps A LOT of people and saves them A LOT of time. I'm fairly certain I've just written the most comprehensive set up guide to setting up an Android app to use Google Drive. Shame on Google for spreading the necessary material across at least 6 different pages that don't link to each other at all.
It's 2015, things have changed!
Get the 'Drive API for Android' with gradle:
compile 'com.google.android.gms:play-services-drive:7.8.0'
There's some new doco (although still lackluster IMO):
https://developers.google.com/drive/web/quickstart/android
And for those about to go caving...the biggest problem I encountered thus far is that there is absolutely no way of distinguishing folders that have been permanently deleted from folders that are normal...you can find them, you can create folders and files within them, only writing to the file DriveContents will always fail.
Check this video from Google I/O to learn how to integrate your Android app with Drive:
http://www.youtube.com/watch?v=xRGyzqD-vRg
Please be aware that what you see in the video is based on Google Play Services:
https://developers.google.com/android/google-play-services/
Take a look at Google's DrEdit Example, which has a folder called android/. Copy it, follow the readme, and it should work (works for me on an Android emulator with KitKat).
.
P.S.
Sorry for reviving this, but the new Google Drive Android API doesn't support full Drive access, only drive.file and drive.appdata authorization scopes, so if you need full access you have to go back to the good 'ol Google API's Client for Java (which the DrEdit example uses).