Share file with camera using cache and FileProvider

* Use cache instead of external storage to share file with camera
* Execute ExistingFileAsync after permission is granted
This commit is contained in:
Yusuke Matsubara 2017-06-10 16:47:18 +09:00
parent 9e0792f1e2
commit 9c69539276
9 changed files with 333 additions and 177 deletions

View file

@ -125,6 +125,16 @@
android:resource="@xml/modifications_sync_adapter" /> android:resource="@xml/modifications_sync_adapter" />
</service> </service>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
<provider <provider
android:name=".contributions.ContributionsContentProvider" android:name=".contributions.ContributionsContentProvider"
android:label="@string/provider_contributions" android:label="@string/provider_contributions"

View file

@ -3,10 +3,12 @@ package fr.free.nrw.commons.contributions;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.content.FileProvider;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -29,34 +31,24 @@ public class ContributionController {
} }
// See http://stackoverflow.com/a/5054673/17865 for why this is done // See http://stackoverflow.com/a/5054673/17865 for why this is done
private Uri lastGeneratedCaptureURI; private Uri lastGeneratedCaptureUri;
private Uri reGenerateImageCaptureURI() { private Uri reGenerateImageCaptureUriInCache() {
String storageState = Environment.getExternalStorageState(); File photoFile = new File(fragment.getContext().getCacheDir() + "/images",
if(storageState.equals(Environment.MEDIA_MOUNTED)) { new Date().getTime() + ".jpg");
photoFile.getParentFile().mkdirs();
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Commons/images/" + new Date().getTime() + ".jpg"; return FileProvider.getUriForFile(
File _photoFile = new File(path); fragment.getContext(),
try { fragment.getActivity().getApplicationContext().getPackageName() + ".provider",
if(!_photoFile.exists()) { photoFile);
_photoFile.getParentFile().mkdirs();
_photoFile.createNewFile();
}
} catch (IOException e) {
Timber.e(e, "Could not create file: %s", path);
}
return Uri.fromFile(_photoFile);
} else {
throw new RuntimeException("No external storage found!");
}
} }
public void startCameraCapture() { public void startCameraCapture() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
lastGeneratedCaptureURI = reGenerateImageCaptureURI(); lastGeneratedCaptureUri = reGenerateImageCaptureUriInCache();
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, lastGeneratedCaptureURI); takePictureIntent.setFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, lastGeneratedCaptureUri);
fragment.startActivityForResult(takePictureIntent, SELECT_FROM_CAMERA); fragment.startActivityForResult(takePictureIntent, SELECT_FROM_CAMERA);
} }
@ -80,7 +72,7 @@ public class ContributionController {
break; break;
case SELECT_FROM_CAMERA: case SELECT_FROM_CAMERA:
shareIntent.setType("image/jpeg"); //FIXME: Find out appropriate mime type shareIntent.setType("image/jpeg"); //FIXME: Find out appropriate mime type
shareIntent.putExtra(Intent.EXTRA_STREAM, lastGeneratedCaptureURI); shareIntent.putExtra(Intent.EXTRA_STREAM, lastGeneratedCaptureUri);
shareIntent.putExtra(UploadService.EXTRA_SOURCE, Contribution.SOURCE_CAMERA); shareIntent.putExtra(UploadService.EXTRA_SOURCE, Contribution.SOURCE_CAMERA);
break; break;
} }
@ -93,12 +85,12 @@ public class ContributionController {
} }
public void saveState(Bundle outState) { public void saveState(Bundle outState) {
outState.putParcelable("lastGeneratedCaptureURI", lastGeneratedCaptureURI); outState.putParcelable("lastGeneratedCaptureURI", lastGeneratedCaptureUri);
} }
public void loadState(Bundle savedInstanceState) { public void loadState(Bundle savedInstanceState) {
if(savedInstanceState != null) { if(savedInstanceState != null) {
lastGeneratedCaptureURI = savedInstanceState.getParcelable("lastGeneratedCaptureURI"); lastGeneratedCaptureUri = savedInstanceState.getParcelable("lastGeneratedCaptureURI");
} }
} }

View file

@ -197,7 +197,7 @@ public class MediaDetailFragment extends Fragment {
extractor.fetch(); extractor.fetch();
return Boolean.TRUE; return Boolean.TRUE;
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); Timber.d(e);
} }
return Boolean.FALSE; return Boolean.FALSE;
} }

View file

@ -6,13 +6,13 @@ import android.content.Intent;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import fr.free.nrw.commons.MWApi;
import org.mediawiki.api.ApiResult; import org.mediawiki.api.ApiResult;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MWApi;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import timber.log.Timber; import timber.log.Timber;
@ -22,13 +22,22 @@ import timber.log.Timber;
* Displays a warning to the user if the file already exists on Commons * Displays a warning to the user if the file already exists on Commons
*/ */
public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> { public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
interface Callback {
void onResult(Result result);
}
public enum Result {
NO_DUPLICATE,
DUPLICATE_PROCEED,
DUPLICATE_CANCELLED
}
private final String fileSHA1;
private final Context context;
private final Callback callback;
private String fileSHA1; public ExistingFileAsync(String fileSHA1, Context context, Callback callback) {
private Context context;
public ExistingFileAsync(String fileSHA1, Context context) {
this.fileSHA1 = fileSHA1; this.fileSHA1 = fileSHA1;
this.context = context; this.context = context;
this.callback = callback;
} }
@Override @Override
@ -79,17 +88,20 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
//Go back to ContributionsActivity //Go back to ContributionsActivity
Intent intent = new Intent(context, ContributionsActivity.class); Intent intent = new Intent(context, ContributionsActivity.class);
context.startActivity(intent); context.startActivity(intent);
callback.onResult(Result.DUPLICATE_CANCELLED);
} }
}); });
builder.setNegativeButton(R.string.yes, new DialogInterface.OnClickListener() { builder.setNegativeButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int id) { public void onClick(DialogInterface dialog, int id) {
//No need to do anything, user remains on upload screen callback.onResult(Result.DUPLICATE_PROCEED);
} }
}); });
AlertDialog dialog = builder.create(); AlertDialog dialog = builder.create();
dialog.show(); dialog.show();
} else {
callback.onResult(Result.NO_DUPLICATE);
} }
} }
} }

View file

@ -9,6 +9,16 @@ import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.provider.DocumentsContract; import android.provider.DocumentsContract;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import timber.log.Timber;
public class FileUtils { public class FileUtils {
@ -23,6 +33,7 @@ public class FileUtils {
*/ */
// Can be safely suppressed, checks for isKitKat before running isDocumentUri // Can be safely suppressed, checks for isKitKat before running isDocumentUri
@SuppressLint("NewApi") @SuppressLint("NewApi")
@Nullable
public static String getPath(Context context, Uri uri) { public static String getPath(Context context, Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
@ -93,6 +104,7 @@ public class FileUtils {
* @param selectionArgs (Optional) Selection arguments used in the query. * @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path. * @return The value of the _data column, which is typically a file path.
*/ */
@Nullable
public static String getDataColumn(Context context, Uri uri, String selection, public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) { String[] selectionArgs) {
@ -108,6 +120,8 @@ public class FileUtils {
final int column_index = cursor.getColumnIndexOrThrow(column); final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index); return cursor.getString(column_index);
} }
} catch (IllegalArgumentException e) {
Timber.d(e);
} finally { } finally {
if (cursor != null) if (cursor != null)
cursor.close(); cursor.close();
@ -119,7 +133,7 @@ public class FileUtils {
* @param uri The Uri to check. * @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider. * @return Whether the Uri authority is ExternalStorageProvider.
*/ */
public static boolean isExternalStorageDocument(Uri uri) { private static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority()); return "com.android.externalstorage.documents".equals(uri.getAuthority());
} }
@ -127,7 +141,7 @@ public class FileUtils {
* @param uri The Uri to check. * @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider. * @return Whether the Uri authority is DownloadsProvider.
*/ */
public static boolean isDownloadsDocument(Uri uri) { private static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority()); return "com.android.providers.downloads.documents".equals(uri.getAuthority());
} }
@ -135,7 +149,37 @@ public class FileUtils {
* @param uri The Uri to check. * @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider. * @return Whether the Uri authority is MediaProvider.
*/ */
public static boolean isMediaDocument(Uri uri) { private static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority()); return "com.android.providers.media.documents".equals(uri.getAuthority());
} }
/**
* Check if the URI is owned by the current app.
*/
public static boolean isSelfOwned(Context context, Uri uri) {
return uri.getAuthority().equals(context.getPackageName() + ".provider");
}
/**
* Copy content from source file to destination file.
* @param source stream copied from
* @param destination stream copied to
* @throws IOException thrown when failing to read source or opening destination file
*/
public static void copy(@NonNull FileInputStream source, @NonNull FileOutputStream destination) throws IOException {
FileChannel source_ = source.getChannel();
FileChannel dest_ = destination.getChannel();
source_.transferTo(0, source_.size(), dest_);
}
/**
* Copy content from source file to destination file.
* @param source file descriptor copied from
* @param destination file path copied to
* @throws IOException thrown when failing to read source or opening destination file
*/
public static void copy(@NonNull FileDescriptor source, @NonNull String destination) throws IOException {
copy(new FileInputStream(source), new FileOutputStream(destination));
}
} }

View file

@ -7,10 +7,14 @@ import android.location.Location;
import android.location.LocationListener; import android.location.LocationListener;
import android.location.LocationManager; import android.location.LocationManager;
import android.media.ExifInterface; import android.media.ExifInterface;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
@ -23,17 +27,38 @@ import timber.log.Timber;
*/ */
public class GPSExtractor { public class GPSExtractor {
private String filePath; private ExifInterface exif;
private double decLatitude, decLongitude; private double decLatitude;
private double decLongitude;
private Double currentLatitude = null; private Double currentLatitude = null;
private Double currentLongitude = null; private Double currentLongitude = null;
public boolean imageCoordsExists; public boolean imageCoordsExists;
private MyLocationListener myLocationListener; private MyLocationListener myLocationListener;
private LocationManager locationManager; private LocationManager locationManager;
/**
* Construct from the file descriptor of the image (only for API 24 or newer).
* @param fileDescriptor the file descriptor of the image
*/
@RequiresApi(24)
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
try {
exif = new ExifInterface(fileDescriptor);
} catch (IOException | IllegalArgumentException e) {
Timber.w(e);
}
}
public GPSExtractor(String filePath) { /**
this.filePath = filePath; * Construct from the file path of the image.
* @param path file path of the image
*/
public GPSExtractor(@NonNull String path) {
try {
exif = new ExifInterface(path);
} catch (IOException | IllegalArgumentException e) {
Timber.w(e);
}
} }
/** /**
@ -86,45 +111,37 @@ public class GPSExtractor {
*/ */
@Nullable @Nullable
public String getCoords(boolean useGPS) { public String getCoords(boolean useGPS) {
ExifInterface exif;
String latitude = ""; String latitude = "";
String longitude = ""; String longitude = "";
String latitude_ref = ""; String latitude_ref = "";
String longitude_ref = ""; String longitude_ref = "";
String decimalCoords = ""; String decimalCoords = "";
try {
exif = new ExifInterface(filePath);
} catch (IOException e) {
Timber.w(e);
return null;
} catch (IllegalArgumentException e) {
Timber.w(e);
return null;
}
//If image has no EXIF data and user has enabled GPS setting, get user's location //If image has no EXIF data and user has enabled GPS setting, get user's location
if (exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null && useGPS) { if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) {
registerLocationManager(); if (useGPS) {
registerLocationManager();
imageCoordsExists = false; imageCoordsExists = false;
Timber.d("EXIF data has no location info"); Timber.d("EXIF data has no location info");
//Check what user's preference is for automatic location detection //Check what user's preference is for automatic location detection
boolean gpsPrefEnabled = gpsPreferenceEnabled(); boolean gpsPrefEnabled = gpsPreferenceEnabled();
//Check that currentLatitude and currentLongitude have been explicitly set by MyLocationListener and do not default to (0.0,0.0) //Check that currentLatitude and currentLongitude have been
if (gpsPrefEnabled && currentLatitude != null && currentLongitude != null) { // explicitly set by MyLocationListener
Timber.d("Current location values: Lat = %f Long = %f", // and do not default to (0.0,0.0)
currentLatitude, currentLongitude); if (gpsPrefEnabled && currentLatitude != null && currentLongitude != null) {
return String.valueOf(currentLatitude) + "|" + String.valueOf(currentLongitude); Timber.d("Current location values: Lat = %f Long = %f",
currentLatitude, currentLongitude);
return String.valueOf(currentLatitude) + "|" + String.valueOf(currentLongitude);
} else {
// No coords found
return null;
}
} else { } else {
// No coords found
return null; return null;
} }
} else if (exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) {
return null;
} else { } else {
//If image has EXIF data, extract image coords //If image has EXIF data, extract image coords
imageCoordsExists = true; imageCoordsExists = true;

View file

@ -8,6 +8,9 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.support.graphics.drawable.VectorDrawableCompat; import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityCompat;
@ -20,9 +23,11 @@ import android.widget.Toast;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.drawee.view.SimpleDraweeView;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import butterknife.ButterKnife; import butterknife.ButterKnife;
@ -48,13 +53,16 @@ public class ShareActivity
implements SingleUploadFragment.OnUploadActionInitiated, implements SingleUploadFragment.OnUploadActionInitiated,
CategorizationFragment.OnCategoriesSaveHandler { CategorizationFragment.OnCategoriesSaveHandler {
private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1;
private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2;
private static final int REQUEST_PERM_ON_CREATE_STORAGE_AND_LOCATION = 3;
private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4;
private CategorizationFragment categorizationFragment; private CategorizationFragment categorizationFragment;
private CommonsApplication app; private CommonsApplication app;
private String source; private String source;
private String mimeType; private String mimeType;
private String mediaUriString;
private Uri mediaUri; private Uri mediaUri;
private Contribution contribution; private Contribution contribution;
@ -68,15 +76,16 @@ public class ShareActivity
private String decimalCoords; private String decimalCoords;
private boolean useNewPermissions = false; private boolean useNewPermissions = false;
private boolean storagePermission = false; private boolean storagePermitted = false;
private boolean locationPermission = false; private boolean locationPermitted = false;
private String title; private String title;
private String description; private String description;
private Snackbar snackbar; private Snackbar snackbar;
private boolean duplicateCheckPassed = false;
/** /**
* Called when user taps the submit button * Called when user taps the submit button.
*/ */
@Override @Override
public void uploadActionInitiated(String title, String description) { public void uploadActionInitiated(String title, String description) {
@ -85,10 +94,11 @@ public class ShareActivity
this.description = description; this.description = description;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//Check for Storage permission that is required for upload. Do not allow user to proceed without permission, otherwise will crash // Check for Storage permission that is required for upload.
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // Do not allow user to proceed without permission, otherwise will crash
//See http://stackoverflow.com/questions/33169455/onrequestpermissionsresult-not-being-called-in-dialog-fragment if (needsToRequestStoragePermission()) {
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 4); requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_PERM_ON_SUBMIT_STORAGE);
} else { } else {
uploadBegins(); uploadBegins();
} }
@ -97,12 +107,18 @@ public class ShareActivity
} }
} }
@RequiresApi(16)
private boolean needsToRequestStoragePermission() {
// We need to ask storage permission when
// the file is not owned by this app, (e.g. shared from the Gallery)
// and permission is not obtained.
return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri)
&& (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED);
}
private void uploadBegins() { private void uploadBegins() {
if (locationPermission) { getFileMetadata(locationPermitted);
getFileMetadata(true);
} else {
getFileMetadata(false);
}
Toast startingToast = Toast.makeText( Toast startingToast = Toast.makeText(
CommonsApplication.getInstance(), CommonsApplication.getInstance(),
@ -232,9 +248,9 @@ public class ShareActivity
//Receive intent from ContributionController.java when user selects picture to upload //Receive intent from ContributionController.java when user selects picture to upload
Intent intent = getIntent(); Intent intent = getIntent();
if(intent.getAction().equals(Intent.ACTION_SEND)) { if (intent.getAction().equals(Intent.ACTION_SEND)) {
mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if(intent.hasExtra(UploadService.EXTRA_SOURCE)) { if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
source = intent.getStringExtra(UploadService.EXTRA_SOURCE); source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
} else { } else {
source = Contribution.SOURCE_EXTERNAL; source = Contribution.SOURCE_EXTERNAL;
@ -243,129 +259,101 @@ public class ShareActivity
} }
if (mediaUri != null) { if (mediaUri != null) {
mediaUriString = mediaUri.toString(); backgroundImageView.setImageURI(mediaUri);
backgroundImageView.setImageURI(mediaUriString);
//Test SHA1 of image to see if it matches SHA1 of a file on Commons
try {
InputStream inputStream = getContentResolver().openInputStream(mediaUri);
Timber.d("Input stream created from %s", mediaUriString);
String fileSHA1 = Utils.getSHA1(inputStream);
Timber.d("File SHA1 is: %s", fileSHA1);
ExistingFileAsync fileAsyncTask = new ExistingFileAsync(fileSHA1, this);
fileAsyncTask.execute();
} catch (IOException e) {
Timber.d(e, "IO Exception: ");
}
} }
if(savedInstanceState != null) { if (savedInstanceState != null) {
contribution = savedInstanceState.getParcelable("contribution"); contribution = savedInstanceState.getParcelable("contribution");
} }
requestAuthToken(); requestAuthToken();
Timber.d("Uri: %s", mediaUriString); Timber.d("Uri: %s", mediaUri.toString());
Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory()); Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory());
useNewPermissions = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
useNewPermissions = true; useNewPermissions = true;
if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
storagePermission = true; if (!needsToRequestStoragePermission()) {
storagePermitted = true;
} }
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
locationPermission = true; locationPermitted = true;
} }
} }
// Check storage permissions if marshmallow or newer // Check storage permissions if marshmallow or newer
if (useNewPermissions && (!storagePermission || !locationPermission)) { if (useNewPermissions && (!storagePermitted || !locationPermitted)) {
if (!storagePermission && !locationPermission) { if (!storagePermitted && !locationPermitted) {
String permissionRationales = getResources().getString(R.string.storage_permission_rationale) + "\n" + getResources().getString(R.string.location_permission_rationale); String permissionRationales =
snackbar = Snackbar.make(findViewById(android.R.id.content), permissionRationales, getResources().getString(R.string.storage_permission_rationale) + "\n"
Snackbar.LENGTH_INDEFINITE) + getResources().getString(R.string.location_permission_rationale);
.setAction(R.string.ok, new View.OnClickListener() { snackbar = requestPermissionUsingSnackBar(
@Override permissionRationales,
public void onClick(View view) { new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.ACCESS_FINE_LOCATION},
ActivityCompat.requestPermissions(ShareActivity.this, REQUEST_PERM_ON_CREATE_STORAGE_AND_LOCATION);
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.ACCESS_FINE_LOCATION}, 3);
}
});
snackbar.show();
View snackbarView = snackbar.getView(); View snackbarView = snackbar.getView();
TextView textView = (TextView) snackbarView.findViewById(android.support.design.R.id.snackbar_text); TextView textView = (TextView) snackbarView.findViewById(android.support.design.R.id.snackbar_text);
textView.setMaxLines(3); textView.setMaxLines(3);
} else if (!storagePermission) { } else if (!storagePermitted) {
Snackbar.make(findViewById(android.R.id.content), R.string.storage_permission_rationale, requestPermissionUsingSnackBar(
Snackbar.LENGTH_INDEFINITE) getString(R.string.storage_permission_rationale),
.setAction(R.string.ok, new View.OnClickListener() { new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
@Override REQUEST_PERM_ON_CREATE_STORAGE);
public void onClick(View view) { } else if (!locationPermitted) {
ActivityCompat.requestPermissions(ShareActivity.this, requestPermissionUsingSnackBar(
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1); getString(R.string.location_permission_rationale),
} new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
}).show(); REQUEST_PERM_ON_CREATE_LOCATION);
} else if (!locationPermission) {
Snackbar.make(findViewById(android.R.id.content), R.string.location_permission_rationale,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
ActivityCompat.requestPermissions(ShareActivity.this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 2);
}
}).show();
} }
} else if (useNewPermissions && storagePermission && !locationPermission) {
getFileMetadata(true);
} else if(!useNewPermissions || (storagePermission && locationPermission)) {
getFileMetadata(true);
} }
preuploadProcessingOfFile();
} }
@Override @Override
public void onRequestPermissionsResult(int requestCode, public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) { String[] permissions, int[] grantResults) {
switch (requestCode) { switch (requestCode) {
// 1 = Storage (from snackbar) case REQUEST_PERM_ON_CREATE_STORAGE: {
case 1: { if (grantResults.length >= 1
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getFileMetadata(true); backgroundImageView.setImageURI(mediaUri);
storagePermitted = true;
preuploadProcessingOfFile();
} }
return; return;
} }
// 2 = Location case REQUEST_PERM_ON_CREATE_LOCATION: {
case 2: { if (grantResults.length >= 1
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getFileMetadata(false); locationPermitted = true;
preuploadProcessingOfFile();
} }
return; return;
} }
// 3 = Storage + Location case REQUEST_PERM_ON_CREATE_STORAGE_AND_LOCATION: {
case 3: { if (grantResults.length >= 2
if (grantResults.length > 1
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getFileMetadata(true); backgroundImageView.setImageURI(mediaUri);
storagePermitted = true;
preuploadProcessingOfFile();
} }
if (grantResults.length > 1 if (grantResults.length >= 2
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) { && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
getFileMetadata(false); locationPermitted = true;
preuploadProcessingOfFile();
} }
return; return;
} }
// 4 = Storage (from submit button) - this needs to be separate from (1) because only the // Storage (from submit button) - this needs to be separate from (1) because only the
// submit button should bring user to next screen // submit button should bring user to next screen
case 4: { case REQUEST_PERM_ON_SUBMIT_STORAGE: {
if (grantResults.length > 0 if (grantResults.length >= 1
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//It is OK to call this at both (1) and (4) because if perm had been granted at //It is OK to call this at both (1) and (4) because if perm had been granted at
//snackbar, user should not be prompted at submit button //snackbar, user should not be prompted at submit button
getFileMetadata(true); preuploadProcessingOfFile();
//Uploading only begins if storage permission granted from arrow icon //Uploading only begins if storage permission granted from arrow icon
uploadBegins(); uploadBegins();
@ -376,22 +364,110 @@ public class ShareActivity
} }
} }
private void preuploadProcessingOfFile() {
if(!useNewPermissions || storagePermitted) {
if (!duplicateCheckPassed) {
//Test SHA1 of image to see if it matches SHA1 of a file on Commons
try {
InputStream inputStream = getContentResolver().openInputStream(mediaUri);
Timber.d("Input stream created from %s", mediaUri.toString());
String fileSHA1 = Utils.getSHA1(inputStream);
Timber.d("File SHA1 is: %s", fileSHA1);
ExistingFileAsync fileAsyncTask = new ExistingFileAsync(fileSHA1, this, new ExistingFileAsync.Callback() {
@Override
public void onResult(ExistingFileAsync.Result result) {
Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
duplicateCheckPassed =
result == ExistingFileAsync.Result.DUPLICATE_PROCEED
|| result == ExistingFileAsync.Result.NO_DUPLICATE;
}
});
fileAsyncTask.execute();
} catch (IOException e) {
Timber.d(e, "IO Exception: ");
}
}
getFileMetadata(locationPermitted);
} else {
Timber.w("not ready for preprocess: useNewPermissions=%s storage=%s location=%s",
useNewPermissions, storagePermitted, locationPermitted);
}
}
private Snackbar requestPermissionUsingSnackBar(String rationale, final String[] perms, final int code) {
Snackbar snackbar = Snackbar.make(findViewById(android.R.id.content), rationale,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
ActivityCompat.requestPermissions(ShareActivity.this,
perms, code);
}
});
snackbar.show();
return snackbar;
}
@Nullable
private String getPathOfMediaOrCopy() {
String filePath = FileUtils.getPath(getApplicationContext(), mediaUri);
Timber.d("Filepath: " + filePath);
if (filePath == null) {
// in older devices getPath() may fail depending on the source URI
// creating and using a copy of the file seems to work instead.
// TODO: there might be a more proper solution than this
String copyPath = getApplicationContext().getCacheDir().getAbsolutePath()
+ "/" + new Date().getTime() + ".jpg";
try {
ParcelFileDescriptor descriptor
= getContentResolver().openFileDescriptor(mediaUri, "r");
if (descriptor != null) {
FileUtils.copy(
descriptor.getFileDescriptor(),
copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
} catch (IOException e) {
Timber.w(e, "Error in file " + copyPath);
return null;
}
}
return filePath;
}
/** /**
* Gets coordinates for category suggestions, either from EXIF data or user location * Gets coordinates for category suggestions, either from EXIF data or user location
* @param gpsEnabled * @param gpsEnabled if true use GPS
*/ */
public void getFileMetadata(boolean gpsEnabled) { private void getFileMetadata(boolean gpsEnabled) {
String filePath = FileUtils.getPath(getApplicationContext(), mediaUri);
Timber.d("Filepath: %s", filePath);
Timber.d("Calling GPSExtractor"); Timber.d("Calling GPSExtractor");
if(imageObj == null) { try {
imageObj = new GPSExtractor(filePath); if (imageObj == null) {
} ParcelFileDescriptor descriptor
= getContentResolver().openFileDescriptor(mediaUri, "r");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (descriptor != null) {
imageObj = new GPSExtractor(descriptor.getFileDescriptor());
}
} else {
String filePath = getPathOfMediaOrCopy();
if (filePath != null) {
imageObj = new GPSExtractor(filePath);
}
}
}
if (filePath != null && !filePath.equals("")) { if (imageObj != null) {
// Gets image coords from exif data or user location // Gets image coords from exif data or user location
decimalCoords = imageObj.getCoords(gpsEnabled); decimalCoords = imageObj.getCoords(gpsEnabled);
useImageCoords(); useImageCoords();
}
} catch (FileNotFoundException e) {
Timber.w("File not found: " + mediaUri, e);
} }
} }

View file

@ -113,35 +113,36 @@ public class UploadController {
} }
contribution.setDataLength(length); contribution.setDataLength(length);
} }
} catch(IOException e) { } catch (IOException e) {
Timber.e(e, "IO Exception: "); Timber.e(e, "IO Exception: ");
} catch(NullPointerException e) { } catch (NullPointerException e) {
Timber.e(e, "Null Pointer Exception: "); Timber.e(e, "Null Pointer Exception: ");
} catch(SecurityException e) { } catch (SecurityException e) {
Timber.e(e, "Security Exception: "); Timber.e(e, "Security Exception: ");
} }
String mimeType = (String)contribution.getTag("mimeType"); String mimeType = (String)contribution.getTag("mimeType");
Boolean imagePrefix = false; Boolean imagePrefix = false;
if(mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) { if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
mimeType = app.getContentResolver().getType(contribution.getLocalUri()); mimeType = app.getContentResolver().getType(contribution.getLocalUri());
} }
if(mimeType != null) { if (mimeType != null) {
contribution.setTag("mimeType", mimeType); contribution.setTag("mimeType", mimeType);
imagePrefix = mimeType.startsWith("image/"); imagePrefix = mimeType.startsWith("image/");
Timber.d("MimeType is: %s", mimeType); Timber.d("MimeType is: %s", mimeType);
} }
if(imagePrefix && contribution.getDateCreated() == null) { if (imagePrefix && contribution.getDateCreated() == null) {
Timber.d("local uri " + contribution.getLocalUri());
Cursor cursor = app.getContentResolver().query(contribution.getLocalUri(), Cursor cursor = app.getContentResolver().query(contribution.getLocalUri(),
new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null); new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
if(cursor != null && cursor.getCount() != 0) { if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) {
cursor.moveToFirst(); cursor.moveToFirst();
Date dateCreated = new Date(cursor.getLong(0)); Date dateCreated = new Date(cursor.getLong(0));
Date epochStart = new Date(0); Date epochStart = new Date(0);
if(dateCreated.equals(epochStart) || dateCreated.before(epochStart)) { if (dateCreated.equals(epochStart) || dateCreated.before(epochStart)) {
// If date is incorrect (1st second of unix time) then set it to the current date // If date is incorrect (1st second of unix time) then set it to the current date
dateCreated = new Date(); dateCreated = new Date();
} }

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="images" path="images/" />
</paths>