I'm having the issue that Android 10 can't read GPS information from my jpeg files. The exact same files are working on Android 9.
I tried the ExifInterface (androidx) which returns null, as well as apache commons imaging which is saying "GPSLatitude: Invalid rational (0/0), Invalid rational (0/0)".
Other exif information like the rating or XPKeywords are read correctly.
How to fix that?
Test code:
import androidx.exifinterface.media.ExifInterface;
ExifInterface exif2 = new ExifInterface(is);
double[] latLngArray = exif2.getLatLong();
getLatLong Implementation:
public double[] getLatLong() {
String latValue = getAttribute(TAG_GPS_LATITUDE);
String latRef = getAttribute(TAG_GPS_LATITUDE_REF);
String lngValue = getAttribute(TAG_GPS_LONGITUDE);
String lngRef = getAttribute(TAG_GPS_LONGITUDE_REF);
if (latValue != null && latRef != null && lngValue != null && lngRef != null) {
try {
double latitude = convertRationalLatLonToDouble(latValue, latRef);
double longitude = convertRationalLatLonToDouble(lngValue, lngRef);
return new double[] {latitude, longitude};
} catch (IllegalArgumentException e) {
Log.w(TAG, "Latitude/longitude values are not parseable. " +
String.format("latValue=%s, latRef=%s, lngValue=%s, lngRef=%s",
latValue, latRef, lngValue, lngRef));
}
}
return null;
}
All getAttribute calls are returning null on Android Q/10. On Android 9 it is working. My test photo does contain GPS information.
Even Android itself was unable to index the GPS location into their media store. The field is null as well in their database. I transferred the test photo via mail.
As mentioned in developer android, now, on Android 10, the location embed on image files is not directly available to the apps:
Because this location information is sensitive, however, Android 10 by default hides this information from your app if it uses scoped storage.
On that same link, you can check how to fix:
Request the ACCESS_MEDIA_LOCATION permission in your app's manifest.
From your MediaStore object, call setRequireOriginal(), passing in the URI of the photograph, as shown in the following code snippet:
Code snippet:
Uri photoUri = Uri.withAppendedPath(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
cursor.getString(idColumnIndex));
// Get location data from the ExifInterface class.
photoUri = MediaStore.setRequireOriginal(photoUri);
InputStream stream = getContentResolver().openInputStream(photoUri);
if (stream != null) {
ExifInterface exifInterface = new ExifInterface(stream);
// Don't reuse the stream associated with the instance of "ExifInterface".
stream.close();
} else {
// Failed to load the stream, so return the coordinates (0, 0).
latLong = new double[2];
}
Note how they are now using Uri, InputStream (and also FileDescriptor) to access the file since now, you can't access a file using its file path.
Related
UPDATE:
My initial question may be misleading so I want to rephrase it:
I want to traverse through the hierarchy tree from an MTP connected device through Android's Storage Access Framework. I can't seem to achieve this because I get a SecurityException stating that a subnode is not a descendant of its parent node. Is there a way to workaround this issue? Or is this a known issue? Thanks.
I'm writing an Android application that attempts to traverse and access documents through the hierarchy tree using Android's Storage Access Framework (SAF) via the MtpDocumentsProvider. I am more or less following the code example described in https://github.com/googlesamples/android-DirectorySelection on how to launch the SAF Picker from my app, select the MTP data source, and then, in onActivityResult, use the returned Uri to traverse through the hierarchy. Unfortunately, this doesn't seem to work because as soon as I access a sub-folder and try to traverse that, I always get a SecurityException stating that document xx is not a descendant of yy
So my question is, using the MtpDocumentProvider, how can I successfully traverse through the hierarchy tree from my app and avoid this exception?
To be specific, in my app, first, I call the following method to launch the SAF Picker:
private void launchStoragePicker() {
Intent browseIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
browseIntent.addFlags(
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
| Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
);
startActivityForResult(browseIntent, REQUEST_CODE_OPEN_DIRECTORY);
}
The Android SAF picker then launches, and I see my connected device recognized as the MTP data source. I select said data source and I get the Uri from my onActivityResult:
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_OPEN_DIRECTORY && resultCode == Activity.RESULT_OK) {
traverseDirectoryEntries(data.getData()); // getData() returns the root uri node
}
}
Then, using the returned Uri, I call DocumentsContract.buildChildDocumentsUriUsingTree to get a Uri which I can then use to query and access the tree hierarchy:
void traverseDirectoryEntries(Uri rootUri) {
ContentResolver contentResolver = getActivity().getContentResolver();
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, DocumentsContract.getTreeDocumentId(rootUri));
// Keep track of our directory hierarchy
List<Uri> dirNodes = new LinkedList<>();
dirNodes.add(childrenUri);
while(!dirNodes.isEmpty()) {
childrenUri = dirNodes.remove(0); // get the item from top
Log.d(TAG, "node uri: ", childrenUri);
Cursor c = contentResolver.query(childrenUri, new String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE}, null, null, null);
try {
while (c.moveToNext()) {
final String docId = c.getString(0);
final String name = c.getString(1);
final String mime = c.getString(2);
Log.d(TAG, "docId: " + id + ", name: " + name + ", mime: " + mime);
if(isDirectory(mime)) {
final Uri newNode = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, docId);
dirNodes.add(newNode);
}
}
} finally {
closeQuietly(c);
}
}
}
// Util method to check if the mime type is a directory
private static boolean isDirectory(String mimeType) {
return DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType);
}
// Util method to close a closeable
private static void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (RuntimeException re) {
throw re;
} catch (Exception ignore) {
// ignore exception
}
}
}
The first iteration on the outer while loop succeeds: the call to query returned a valid Cursor for me to traverse. The problem is the second iteration: when I try to query for the Uri, which happens to be a subnode of rootUri, I get a SecurityException stating the document xx is not a descendent of yy.
D/MyApp(19241): node uri: content://com.android.mtp.documents/tree/2/document/2/children
D/MyApp(19241): docId: 4, name: DCIM, mime: vnd.android.document/directory
D/MyApp(19241): node uri: content://com.android.mtp.documents/tree/2/document/4/children
E/DatabaseUtils(20944): Writing exception to parcel
E/DatabaseUtils(20944): java.lang.SecurityException: Document 4 is not a descendant of 2
Can anyone provide some insight as to what I'm doing wrong? If I use a different data source provider, for example, one that is from external storage (i.e. an SD Card attached via a standard USB OTG reader), everything works fine.
Additional information:
I'm running this on a Nexus 6P, Android 7.1.1, and my app minSdkVersion is 19.
I am using your code to go in sub folders with adding lines:
Uri childrenUri;
try {
//for childs and sub child dirs
childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri));
} catch (Exception e) {
// for parent dir
childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));
}
The flags added before start activity for result do nothing.
I do not understand that you use DocumentsContract. The user picks a directory. From the uri you get you can construct a DocumentFile for that directory.
After that use DocumentFile::listFiles() on that instance to get a list of subdirectories and files.
After different attempts, I'm not sure there is a way around this SecurityException for an MTP Data Source (unless someone can refute me on this). Looking at the DocumentProvider.java source code and the stack trace, it appears that the call to DocumentProvider#isChildDocument inside DocumentProvider#enforceTree may not have been properly overriden in the MtpDocumentProvider implementation in the Android framework. Default implementation always returns false. #sadface
Using Android 6.0.1, API 23,
I successfully implemented reading a file from a storage-location that the user picks in the document-picker of SAF (Storage Access Framework).
Now, I would like to use the Persist permissions that SAF allows in order to allow my App to always pick the same file (but without any document-picker window [and sub-windows] popping in all the time).
I somehow succeeded in implementing the Persist-permissions (as shown in the SAF-doc) (...for that, see my code example below...) - But questions came up:
In spite of persistence-permission working - why does the document-picker window keep popping up? Once the file is picked by the user - from this moment on - I would like to simply get the file content without any picker window coming to foreground. Is this possible? And if yes, how?
(I can tell that persistence permission is somehow working since the file gets read immediately without the user having to pick again. But still this nasty picker window should not have to come up - or does it?)
Is it possible to keep SAF persistence-permission even if my App gets closed or stopped? If yes, how?
Here is the code sample that does get the file content, including my trial with the persistence permission:
Inside a StorageClientFragment Class:
public String locationFileContent;
public void performFileSearch() {
Intent openDocumentIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
openDocumentIntent.addCategory(Intent.CATEGORY_OPENABLE);
openDocumentIntent.setType("text/plain");
openDocumentIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
startActivityForResult(openDocumentIntent, READ_REQUEST_CODE);
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
Uri uri = null;
if (resultData != null) {
uri = resultData.getData();
// Here is my Persist-permission trial !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
getActivity().getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
this.locationFileContent = readTextFromUri(uri);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private String readTextFromUri(Uri uri) throws IOException {
InputStream inputStream = getActivity().getContentResolver().openInputStream(uri);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
inputStream.close();
return stringBuilder.toString();
}
Creating the Fragment gets done in MainActivity:
StorageClientFragment storageClientFragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (getSupportFragmentManager().findFragmentByTag(FRAGTAG) == null ) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
storageClientFragment = new StorageClientFragment();
transaction.add(storageClientFragment, FRAGTAG);
transaction.commit();
}
}
Calling the FileSearch is done when a Test-Button is clicked:
public void TestingButtonClicked(View view) {
this.storageClientFragment.performFileSearch();
String stringFileContent = this.storageClientFragment.locationFileContent;
Toast.makeText(this, "Back in Main, Content = " + stringFileContent, Toast.LENGTH_SHORT).show();
}
The above code works nicely. But again, how can I stop the SAF document-picker window from showing up all the time (even though persist permission is set)?
why does the document-picker window keep croping up ?
Presumably because you keep starting an ACTION_OPEN_DOCUMENT activity. If you do not want to ask the user to pick a document, do not start this activity.
I would like to simply get the file content without any picker-window coming to foreground. Is this possible ? And if yes, how ??
The same way that you got the content originally: pass the Uri to openInputStream() on a ContentResolver.
If you are using takePersistableUriPermissions() to aim for long-lived access to the content, it is still your job to hold onto the Uri. This is not significantly different than how this sort of thing works in other environments (e.g., remembering the user's history of local file paths for a desktop OS program).
Since a Uri can be trivially converted to and from a String, saving the Uri somewhere (database, SharedPreferences, or other file) is not especially difficult.
Is it possible to keep SAF persistance-permission even if my App gets closed or stopped ? If yes, how ??
takePersistableUriPermission(), as you have already implemented.
I've written an Android Wear application which receives an image wrapped in an Asset from a phone app using the Data API. The app used to work fine and has not been changed in ages but recently I started to find the image passed from the phone app was failing to be rendered on the screen of the wearable. On investigation I found that one of the methods, getFdForAsset was failing with a wearable status code of 4005 which means Asset Unavailable. See https://developers.google.com/android/reference/com/google/android/gms/wearable/WearableStatusCodes
I process data events in a call to my wearable app's onDataChanged method like this:
public void onDataChanged(DataEventBuffer dataEvents) {
LOGD(TAG, "XXXX MainActivity.onDataChanged()");
final List<DataEvent> events = FreezableUtils.freezeIterable(dataEvents);
dataEvents.close();
LOGD(TAG, "onDataChanged data event count=" + events.size());
for (DataEvent event : events) {
if (event.getType() == DataEvent.TYPE_CHANGED) {
String path = event.getDataItem().getUri().getPath();
if (IMAGE_PATH.equals(path)) {
DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());
LOGD(TAG, "onDataChanged getting image asset");
Asset photo = dataMapItem.getDataMap()
.getAsset(IMAGE_KEY);
LOGD(TAG, "onDataChanged photo asset="+photo);
final String toi_name = dataMapItem.getDataMap().getString(GYBO_NAME);
final String toi_info = dataMapItem.getDataMap().getString(GYBO_INFO);
current_toi_name = toi_name;
current_toi_info = toi_info;
LOGD(TAG, "onDataChanged TOI name="+toi_name);
LOGD(TAG, "onDataChanged TOI info="+toi_info);
Bitmap bitmap = loadBitmapFromAsset(google_api_client, photo);
And then attempt to create a Bitmap from the Asset in this method:
private Bitmap loadBitmapFromAsset(GoogleApiClient apiClient, Asset asset) {
if (asset == null) {
throw new IllegalArgumentException("XXXX Asset must be non-null");
}
DataApi.GetFdForAssetResult result = Wearable.DataApi.getFdForAsset(
apiClient, asset).await();
if (result == null) {
Log.w(TAG, "XXXX getFdForAsset returned null");
return null;
}
if (result.getStatus().isSuccess()) {
Log.d(TAG, "XXXX loadBitmapFromAsset getFdForAsset was successful");
} else {
Log.d(TAG, "XXXX loadBitmapFromAsset getFdForAsset was not successful. Error="+result.getStatus().getStatusCode()+":"+result.getStatus().getStatusMessage());
// Seeing status code 4005 here which means Asset Unavailable
}
InputStream assetInputStream = result.getInputStream();
if (assetInputStream == null) {
Log.w(TAG, "XXXX Requested an unknown Asset");
result.release();
return null;
}
result.release();
return BitmapFactory.decodeStream(assetInputStream);
}
The Asset object itself is not null, so it's coming across from the mobile app OK. And the path of the data event is being correctly recognised as being one which contains an image.
Does anyone have any idea as to why I'm getting this result and how to resolve it?
Thanks
one important thing... wearable as well as mobile module have to have the same signing certificate; just make sure if you define it via your build.gradle it's the same. this affects transferring assets... other data were synced w/o issues even with different certificates;
I was recently fighting with this issue and found this was the cause of ASSET_UNAVAILABLE, while adding wear module to existing app which had custom debug signing certificate defined in build.gradle - I had to have this certificate even for wearable for asset sync to work.
How are you sending the image? I found that if I used Asset.createFromUri(), it didn't work and gave me the ASSET UNAVAILABLE error. But when I switched to Asset.createFromFd(), it worked.
Here's the code that worked for me:
private static Asset createAssetFromBitmap(String imagePath) throws FileNotFoundException {
// creating from Uri doesn't work: gives a ASSET_UNAVAILABLE error
//return Asset.createFromUri(Uri.parse(imagePath));
final File file = new File(imagePath);
final ParcelFileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return Asset.createFromFd(fd);
}
I'm trying to develop an android app that can read a xml file stored in my google drive folder, the idea at first is trying to open the file and handle the content.
I've read the Google Drive API docs for android and i reached a point that I'm lost, it's working with file contents.
According to this guide the way to open a file from drive is this:
DriveFile file = ...
file.open(mGoogleApiClient, DriveFile.MODE_READ_ONLY, null).setResultCallback(contentsOpenedCallback);`
Searching I realized that the complete code (that they not include there is):
DriveFile file = Drive.DriveApi.getFile(mGoogleApiClient,DriveId.bg(id));
file.open(mGoogleApiClient, DriveFile.MODE_READ_ONLY, null).setResultCallback(contentsOpenedCallback);`
Well the problem there is that I don't know the file "id". I've tried the id from the web link of google drive, something like this (https://drive.google.com/open?id=1EafJ-T6H4xI9VaUuUO5FMVb9Y30xyr7OHuISQ53avso&authuser=0) but didnĀ“t work.
You could use the DriveAPI Query method, to retrieve any information about an specific file. you will need to define a query object as the following:
Query query = new Query.Builder()
.addFilter(Filters.eq(SearchableField.TITLE, "HelloWorld.java"))
.build();
And set a callback function to iterate on the results:
Drive.DriveApi.query(googleApiClient, query)
.setResultCallback(new OnChildrenRetrievedCallback() {
#Override
public void onChildrenRetrieved(MetadataBufferResult result) {
// Iterate over the matching Metadata instances in mdResultSet
}
});
You can find more information on the topic here: https://developers.google.com/drive/android/queries
The solution i found for this problem was creating the file from the app. Using the class ("CreateFileActivity.java") from google drive api demo app.
With this class i save the returning Driveid from the new file in a global DriveId variable.
final private ResultCallback<DriveFolder.DriveFileResult> fileCallback = new
ResultCallback<DriveFolder.DriveFileResult>() {
#Override
public void onResult(DriveFolder.DriveFileResult result) {
if (!result.getStatus().isSuccess()) {
Log.e("","Error while trying to create the file");
return;
}
Id=result.getDriveFile().getDriveId();
Log.e("","Created a file with content: " + Id);
}
};
Then with this id in another method i call the file and read it (If i want i can edit this file information from Google Drive Web App):
public void leer(){
DriveFile file = Drive.DriveApi.getFile(getGoogleApiClient(),Id);
file.open(mGoogleApiClient, DriveFile.MODE_READ_ONLY, null)
.setResultCallback(contentsOpenedCallback);
}
ResultCallback<DriveApi.DriveContentsResult> contentsOpenedCallback =
new ResultCallback<DriveApi.DriveContentsResult>() {
#Override
public void onResult(DriveApi.DriveContentsResult result) {
if (!result.getStatus().isSuccess()) {
Log.e("Error:","No se puede abrir el archivo o no se encuentra");
return;
}
// DriveContents object contains pointers
// to the actual byte stream
DriveContents contents = result.getDriveContents();
BufferedReader reader = new BufferedReader(new InputStreamReader(contents.getInputStream()));
StringBuilder builder = new StringBuilder();
String line;
try {
while ((line = reader.readLine()) != null) {
builder.append(line);
}
} catch (IOException e) {
e.printStackTrace();
}
String contentsAsString = builder.toString();
Log.e("RESULT:",contentsAsString);
}
};
I've been playing with this stuff a few months back, and still have some code on GitHub. It may be VERY outdated (libver 15 or so), but it may serve as a reference point, and it is simple. Look here. Pull it down, plug in, step through. Fix what's not working anymore :-). I've abandoned it some time ago.
Be aware of the fact that there are 2 different IDs for Google Drive Android API objects, see SO 22841237.
In general, you usually start with knowing the file/folder name, query GDAA to get a list of objects. Each of them will yield DriveID and ResourceID. DriveID is used in your app to manipulate the objects (does not mean anything outside your Android App and/or device). ResourceID is the string that appears in different forms in URLs and can be used outside your app (web browser for instance...). Look at this wrapper to get some feeling how it works. But again, it's been a few versions back, so there are no guaranties.
The Google Drive API is deprecated, now its Google Drive V3 and for Query we use
String pageToken = null;
do {
FileList result = driveService.files().list()
.setQ("mimeType='image/jpeg'")
.setSpaces("drive")
.setFields("nextPageToken, files(id, name)")
.setPageToken(pageToken)
.execute();
for (File file : result.getFiles()) {
System.out.printf("Found file: %s (%s)\n",
file.getName(), file.getId());
}
pageToken = result.getNextPageToken();
}
while (pageToken != null);
You can Learn more here Officals Docs
I use Phonegap (2.5) to get Contact image from android (4.0) to make a list contacts with avatar and name.
Contact avatar get fine as url data (example data: content://com.android.contacts/contacts/189/photo) but I don't know how to handle if that url is point to a null image ( as it will show up a blue square with question mark inside in that contact item did not have avarta setup ). If image is null or not setup then it should be my default image url.
...
navigator.contacts.find(
['id', 'name', 'phoneNumbers', 'photos', 'displayName'],
function(deviceContacts) {
for (var i = 0; i < deviceContacts.length; i++) {
var deviceContact = deviceContacts[ i ];
if (deviceContact.photos !== null){
img = deviceContact.photos[0].value; //url to image
my_img_item.setSrc(img);
//how to check if image is null or not setup then show default image?
}
...
}
Please help, thanks all.
I did some further research on that topic. I inspected the Phonegap code (unfortunately I only had version 2.8 right here.
And I found this:
private JSONObject photoQuery(Cursor cursor, String contactId) {
JSONObject photo = new JSONObject();
try {
photo.put("id", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Photo._ID)));
photo.put("pref", false);
photo.put("type", "url");
Uri person = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, (new Long(contactId)));
Uri photoUri = Uri.withAppendedPath(person, ContactsContract.Contacts.Photo.CONTENT_DIRECTORY);
photo.put("value", photoUri.toString());
} catch (JSONException e) {
Log.e(LOG_TAG, e.getMessage(), e);
}
return photo; }
If I interpret this correctly then the URI is just put together generically without even checking whether there is an avatar available. So what you can do is to write your own Phonegap-plugin to build your own native query. Otherwise I belive, there is no way for you to achieve what you are planning to do.