Merge pull request #727 from whym/camera

Share file with camera using cache and FileProvider (for API 24)
This commit is contained in:
Josephine Lim 2017-06-14 18:51:08 +10:00 committed by GitHub
commit 473c0e7e47
10 changed files with 381 additions and 178 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,24 @@ 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);
}
private String fileSHA1; public enum Result {
private Context context; NO_DUPLICATE,
DUPLICATE_PROCEED,
DUPLICATE_CANCELLED
}
public ExistingFileAsync(String fileSHA1, Context context) { private final String fileSha1;
this.fileSHA1 = fileSHA1; private final Context context;
private final Callback callback;
public ExistingFileAsync(String fileSha1, Context context, Callback callback) {
this.fileSha1 = fileSha1;
this.context = context; this.context = context;
this.callback = callback;
} }
@Override @Override
@ -46,7 +57,7 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
result = api.action("query") result = api.action("query")
.param("format", "xml") .param("format", "xml")
.param("list", "allimages") .param("list", "allimages")
.param("aisha1", fileSHA1) .param("aisha1", fileSha1)
.get(); .get();
Timber.d("Searching Commons API for existing file: %s", result); Timber.d("Searching Commons API for existing file: %s", result);
} catch (IOException e) { } catch (IOException e) {
@ -79,17 +90,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,39 @@ 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 sourceChannel = source.getChannel();
FileChannel destinationChannel = destination.getChannel();
sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel);
}
/**
* 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,26 +111,15 @@ 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) {
if (useGPS) {
registerLocationManager(); registerLocationManager();
imageCoordsExists = false; imageCoordsExists = false;
@ -114,7 +128,9 @@ public class GPSExtractor {
//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
// explicitly set by MyLocationListener
// and do not default to (0.0,0.0)
if (gpsPrefEnabled && currentLatitude != null && currentLongitude != null) { if (gpsPrefEnabled && currentLatitude != null && currentLongitude != null) {
Timber.d("Current location values: Lat = %f Long = %f", Timber.d("Current location values: Lat = %f Long = %f",
currentLatitude, currentLongitude); currentLatitude, currentLongitude);
@ -123,8 +139,9 @@ public class GPSExtractor {
// No coords found // No coords found
return null; return null;
} }
} else if (exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) { } else {
return 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,13 +107,19 @@ public class ShareActivity
} }
} }
private void uploadBegins() { @RequiresApi(16)
if (locationPermission) { private boolean needsToRequestStoragePermission() {
getFileMetadata(true); // We need to ask storage permission when
} else { // the file is not owned by this app, (e.g. shared from the Gallery)
getFileMetadata(false); // and permission is not obtained.
return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri)
&& (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED);
} }
private void uploadBegins() {
getFileMetadata(locationPermitted);
Toast startingToast = Toast.makeText( Toast startingToast = Toast.makeText(
CommonsApplication.getInstance(), CommonsApplication.getInstance(),
R.string.uploading_started, R.string.uploading_started,
@ -243,22 +259,7 @@ 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) {
@ -267,105 +268,94 @@ public class ShareActivity
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[]{
ActivityCompat.requestPermissions(ShareActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.ACCESS_FINE_LOCATION}, 3); Manifest.permission.ACCESS_FINE_LOCATION},
} REQUEST_PERM_ON_CREATE_STORAGE_AND_LOCATION);
});
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},
REQUEST_PERM_ON_CREATE_LOCATION);
} }
}).show();
} 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(); performPreuploadProcessingOfFile();
}
} else if (useNewPermissions && storagePermission && !locationPermission) {
getFileMetadata(true);
} else if(!useNewPermissions || (storagePermission && locationPermission)) {
getFileMetadata(true);
}
} }
@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;
performPreuploadProcessingOfFile();
} }
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;
performPreuploadProcessingOfFile();
} }
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;
performPreuploadProcessingOfFile();
} }
if (grantResults.length > 1 if (grantResults.length >= 2
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) { && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
getFileMetadata(false); locationPermitted = true;
performPreuploadProcessingOfFile();
} }
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); performPreuploadProcessingOfFile();
//Uploading only begins if storage permission granted from arrow icon //Uploading only begins if storage permission granted from arrow icon
uploadBegins(); uploadBegins();
@ -376,23 +366,112 @@ public class ShareActivity
} }
} }
/** private void performPreuploadProcessingOfFile() {
* Gets coordinates for category suggestions, either from EXIF data or user location if (!useNewPermissions || storagePermitted) {
* @param gpsEnabled if (!duplicateCheckPassed) {
*/ //Test SHA1 of image to see if it matches SHA1 of a file on Commons
public void getFileMetadata(boolean gpsEnabled) { try {
String filePath = FileUtils.getPath(getApplicationContext(), mediaUri); InputStream inputStream = getContentResolver().openInputStream(mediaUri);
Timber.d("Filepath: %s", filePath); Timber.d("Input stream created from %s", mediaUri.toString());
Timber.d("Calling GPSExtractor"); String fileSHA1 = Utils.getSHA1(inputStream);
if(imageObj == null) { Timber.d("File SHA1 is: %s", fileSHA1);
imageObj = new GPSExtractor(filePath);
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: ");
}
} }
if (filePath != null && !filePath.equals("")) { getFileMetadata(locationPermitted);
} else {
Timber.w("not ready for preprocessing: 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
* @param gpsEnabled if true use GPS
*/
private void getFileMetadata(boolean gpsEnabled) {
Timber.d("Calling GPSExtractor");
try {
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 (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

@ -135,9 +135,10 @@ public class UploadController {
} }
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);

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>

View file

@ -0,0 +1,40 @@
package fr.free.nrw.commons;
import org.junit.Assert;
import org.junit.Test;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import fr.free.nrw.commons.upload.FileUtils;
import static org.hamcrest.CoreMatchers.is;
public class FileUtilsTest {
@Test public void copiedFileIsIdenticalToSource() throws IOException {
File source = File.createTempFile("temp", "");
File dest = File.createTempFile("temp", "");
writeToFile(source, "Hello, World");
FileUtils.copy(new FileInputStream(source), new FileOutputStream(dest));
Assert.assertThat(getString(dest), is(getString(source)));
}
private static void writeToFile(File file, String s) throws IOException {
BufferedOutputStream buf = new BufferedOutputStream(new FileOutputStream(file));
buf.write(s.getBytes());
buf.close();
}
private static String getString(File file) throws IOException {
int size = (int) file.length();
byte[] bytes = new byte[size];
BufferedInputStream buf = new BufferedInputStream(new FileInputStream(file));
buf.read(bytes, 0, bytes.length);
buf.close();
return new String(bytes);
}
}