Fix urgent crashes A and E (#1749)

* Create utility class for contribution process

* implement method to save five from given URİ

* Add file utilities for directory checks

* Add ContributionUtils for saving file during upload

* Change method call acordingly with handleImagePicked() method

* Call method to save file temproarily when a photo to upload is chosen from contributions list.

* Call method to save file temproarily when a photo to upload is chosen from nearby list and map

* Arrange method call

* Write a method to save file temporarily during upload process. It will save the file to a internal path and it will be deleted by another method after upload process is done.

* Add a method to save a file to a given path from a content provider Uri

* On openAssetFileDescriptor method, use URi from temporarily saved file, instead of Contributions.getLocalUri which was Uri from content provider

* Edit uploadContribution method so that it will use FileInputStream from temporarily saved file, insdeat of the Uri from content provider.

* Make it work

* Code cleanup

* Add directory cleaner method

* Call temp directory cleaner method at the end of uplpoad process

* Use FileInputStream insted

* Add directory cleaner method

* Add file removal method

* Use external directory instead

* Make destination file name flexible

* Make it work with share action coming from another activity

* Make it work for Multiple hare Activity

* Code cleanup

* Solve camera issue

* Fix camera crash

* Cleanup

* Revert change of commenting out posibly useles code, because I am not sure if it is useless or not. Requires discussion

* Use timestamp in temoorary file names, so that we wont never create same file and access old file reference. It was a weird problem though

* Code cleanup

* Add nullable annotation to handleImagePicked method uri parameter

* Add Nullable anotation to method

* Code cleanup

* Bugfix: use uri.getPath() instead uri.toString

* Remove unecesarry file saving operation, which was added accidentally

* Fix travis fail

* Remove temp file if upload gets failed and file is still there

* Code cleanup:Remove unused parameters from removeTempFile method

* Empty temp directory on app create, in case some of files are still there

* Add null check to array to prevent NPE on first run

* Fix multiple uploads bug

* Remove file if upload is succeed

* Add external storage utility methods

* Check external file permission before saving files temporarily

* finish activity if permission is not granted

* Add log lines

* Remove files even if user decides to go back without sharing

* Add easy null check

* Change storage permission settings in singe upload fragment too

* Finish app if permission is not granted

* Code optimisation

* Remove temp file if upload process never is finalised on activity stop

* Bugfix maybe contribution is never created

* Fix travis build
This commit is contained in:
neslihanturan 2018-08-01 23:24:08 +03:00 committed by Josephine Lim
parent 22d2b1795c
commit d29aa2e2e5
18 changed files with 440 additions and 66 deletions

View file

@ -27,6 +27,7 @@ import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.modifications.ModifierSequenceDao;
import fr.free.nrw.commons.upload.FileUtils; import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.utils.ContributionUtils;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
@ -68,7 +69,6 @@ public class CommonsApplication extends MultiDexApplication {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
ApplicationlessInjection ApplicationlessInjection
.getInstance(this) .getInstance(this)
.getCommonsApplicationComponent() .getCommonsApplicationComponent()
@ -81,6 +81,8 @@ public class CommonsApplication extends MultiDexApplication {
if (setupLeakCanary() == RefWatcher.DISABLED) { if (setupLeakCanary() == RefWatcher.DISABLED) {
return; return;
} }
// Empty temp directory in case some temp files are created and never removed.
ContributionUtils.emptyTemporaryDirectory();
Timber.plant(new Timber.DebugTree()); Timber.plant(new Timber.DebugTree());

View file

@ -51,16 +51,10 @@ public class MediaWikiImageView extends SimpleDraweeView {
return; return;
} }
if(media.getFilename() != null) { if (media.getFilename() != null && thumbnailUrlCache.get(media.getFilename()) != null) {
if (thumbnailUrlCache.get(media.getFilename()) != null) { setImageUrl(thumbnailUrlCache.get(media.getFilename()));
setImageUrl(thumbnailUrlCache.get(media.getFilename())); } else {
} else { setImageUrl(null);
setImageUrl(null);
currentThumbnailTask = new ThumbnailFetchTask(media, mwApi);
currentThumbnailTask.execute(media.getFilename());
}
} else { // local image
setImageUrl(media.getLocalUri().toString());
currentThumbnailTask = new ThumbnailFetchTask(media, mwApi); currentThumbnailTask = new ThumbnailFetchTask(media, mwApi);
currentThumbnailTask.execute(media.getFilename()); currentThumbnailTask.execute(media.getFilename());
} }

View file

@ -46,6 +46,7 @@ public class Contribution extends Media {
private String decimalCoords; private String decimalCoords;
private boolean isMultiple; private boolean isMultiple;
private String wikiDataEntityId; private String wikiDataEntityId;
private Uri contentProviderUri;
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp, public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp,
int state, long dataLength, Date dateUploaded, long transferred, int state, long dataLength, Date dateUploaded, long transferred,
@ -236,4 +237,12 @@ public class Contribution extends Media {
public void setWikiDataEntityId(String wikiDataEntityId) { public void setWikiDataEntityId(String wikiDataEntityId) {
this.wikiDataEntityId = wikiDataEntityId; this.wikiDataEntityId = wikiDataEntityId;
} }
public void setContentProviderUri(Uri contentProviderUri) {
this.contentProviderUri = contentProviderUri;
}
public Uri getContentProviderUri() {
return contentProviderUri;
}
} }

View file

@ -7,9 +7,11 @@ import android.content.pm.ResolveInfo;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentActivity;
import android.support.v4.content.FileProvider; import android.support.v4.content.FileProvider;
import android.util.Log;
import java.io.File; import java.io.File;
import java.util.Date; import java.util.Date;
@ -28,8 +30,8 @@ import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_
public class ContributionController { public class ContributionController {
private static final int SELECT_FROM_GALLERY = 1; public static final int SELECT_FROM_GALLERY = 1;
private static final int SELECT_FROM_CAMERA = 2; public static final int SELECT_FROM_CAMERA = 2;
private Fragment fragment; private Fragment fragment;
@ -91,8 +93,7 @@ public class ContributionController {
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY); fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
} }
public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload, String wikiDataEntityId) { public void handleImagePicked(int requestCode, @Nullable Uri uri, boolean isDirectUpload, String wikiDataEntityId) {
Timber.d("Is direct upload %s and the Wikidata entity ID is %s", isDirectUpload, wikiDataEntityId);
FragmentActivity activity = fragment.getActivity(); FragmentActivity activity = fragment.getActivity();
Timber.d("handleImagePicked() called with onActivityResult()"); Timber.d("handleImagePicked() called with onActivityResult()");
Intent shareIntent = new Intent(activity, ShareActivity.class); Intent shareIntent = new Intent(activity, ShareActivity.class);
@ -100,7 +101,7 @@ public class ContributionController {
switch (requestCode) { switch (requestCode) {
case SELECT_FROM_GALLERY: case SELECT_FROM_GALLERY:
//Handles image picked from gallery //Handles image picked from gallery
Uri imageData = data.getData(); Uri imageData = uri;
shareIntent.setType(activity.getContentResolver().getType(imageData)); shareIntent.setType(activity.getContentResolver().getType(imageData));
shareIntent.putExtra(EXTRA_STREAM, imageData); shareIntent.putExtra(EXTRA_STREAM, imageData);
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY); shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);

View file

@ -38,6 +38,7 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.quiz.QuizChecker; import fr.free.nrw.commons.quiz.QuizChecker;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.UploadService; import fr.free.nrw.commons.upload.UploadService;
import fr.free.nrw.commons.utils.ContributionUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
@ -199,6 +200,9 @@ public class ContributionsActivity
Contribution c = contributionDao.fromCursor(allContributions); Contribution c = contributionDao.fromCursor(allContributions);
if (c.getState() == STATE_FAILED) { if (c.getState() == STATE_FAILED) {
Timber.d("Deleting failed contrib %s", c.toString()); Timber.d("Deleting failed contrib %s", c.toString());
// If upload fails and then user decides to cancel upload at all, which means contribution
// object will be deleted. So we have to delete temp file for that contribution.
ContributionUtils.removeTemporaryFile(c.getLocalUri());
contributionDao.delete(c); contributionDao.delete(c);
} else { } else {
Timber.d("Skipping deletion for non-failed contrib %s", c.toString()); Timber.d("Skipping deletion for non-failed contrib %s", c.toString());

View file

@ -3,11 +3,13 @@ package fr.free.nrw.commons.contributions;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -31,6 +33,7 @@ import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.utils.ContributionUtils;
import timber.log.Timber; import timber.log.Timber;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
@ -117,7 +120,13 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data); requestCode, resultCode, data);
controller.handleImagePicked(requestCode, data, false, null); if (requestCode == ContributionController.SELECT_FROM_CAMERA) {
// If coming from camera, pass null as uri. Because camera photos get saved to a
// fixed directory
controller.handleImagePicked(requestCode, null, false, null);
} else {
controller.handleImagePicked(requestCode, data.getData(), false, null);
}
} else { } else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data); requestCode, resultCode, data);

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.mwapi;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -51,6 +52,7 @@ import fr.free.nrw.commons.category.CategoryImageUtils;
import fr.free.nrw.commons.category.QueryContinue; import fr.free.nrw.commons.category.QueryContinue;
import fr.free.nrw.commons.notification.Notification; import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationUtils; import fr.free.nrw.commons.notification.NotificationUtils;
import fr.free.nrw.commons.utils.ContributionUtils;
import in.yuvi.http.fluent.Http; import in.yuvi.http.fluent.Http;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
@ -856,17 +858,23 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
long dataLength, long dataLength,
String pageContents, String pageContents,
String editSummary, String editSummary,
final ProgressListener progressListener) throws IOException { final ProgressListener progressListener,
Uri fileUri,
Uri contentProviderUri) throws IOException {
ApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, progressListener::onProgress); ApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, progressListener::onProgress);
Log.e("WTF", "Result: " + result.toString()); Log.e("WTF", "Result: " + result.toString());
String resultStatus = result.getString("/api/upload/@result"); String resultStatus = result.getString("/api/upload/@result");
if (!resultStatus.equals("Success")) { if (!resultStatus.equals("Success")) {
String errorCode = result.getString("/api/error/@code"); String errorCode = result.getString("/api/error/@code");
Timber.e(errorCode); Timber.e(errorCode);
return new UploadResult(resultStatus, errorCode); return new UploadResult(resultStatus, errorCode);
} else { } else {
// If success we have to remove file from temp directory
ContributionUtils.removeTemporaryFile(fileUri);
Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename
String imageUrl = result.getString("/api/upload/imageinfo/@url"); String imageUrl = result.getString("/api/upload/imageinfo/@url");
@ -874,7 +882,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
} }
} }
@Override @Override
@NonNull @NonNull
public Single<Integer> getUploadCount(String userName) { public Single<Integer> getUploadCount(String userName) {

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.mwapi; package fr.free.nrw.commons.mwapi;
import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -54,7 +55,7 @@ public interface MediaWikiApi {
List<String> searchCategory(String title, int offset); List<String> searchCategory(String title, int offset);
@NonNull @NonNull
UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException; UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener, Uri fileUri, Uri contentProviderUri) throws IOException;
@Nullable @Nullable
String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException; String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException;

View file

@ -30,6 +30,7 @@ import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.ContributionUtils;
import fr.free.nrw.commons.utils.UriDeserializer; import fr.free.nrw.commons.utils.UriDeserializer;
import timber.log.Timber; import timber.log.Timber;
@ -147,7 +148,13 @@ public class NearbyListFragment extends DaggerFragment {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data); requestCode, resultCode, data);
controller.handleImagePicked(requestCode, data, true, directPrefs.getString(WIKIDATA_ENTITY_ID_PREF, null)); if (requestCode == ContributionController.SELECT_FROM_CAMERA) {
// If coming from camera, pass null as uri. Because camera photos get saved to a
// fixed directory
controller.handleImagePicked(requestCode, null, true, null);
} else {
controller.handleImagePicked(requestCode, data.getData(), true, null);
}
} else { } else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data); requestCode, resultCode, data);

View file

@ -56,6 +56,7 @@ import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.utils.ContributionUtils;
import fr.free.nrw.commons.utils.UriDeserializer; import fr.free.nrw.commons.utils.UriDeserializer;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber; import timber.log.Timber;
@ -765,10 +766,17 @@ public class NearbyMapFragment extends DaggerFragment {
public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data); requestCode, resultCode, data);
controller.handleImagePicked(requestCode, data, true, directPrefs.getString(WIKIDATA_ENTITY_ID_PREF, null)); if (requestCode == ContributionController.SELECT_FROM_CAMERA) {
// If coming from camera, pass null as uri. Because camera photos get saved to a
// fixed directory
controller.handleImagePicked(requestCode, null, true, null);
} else {
controller.handleImagePicked(requestCode, data.getData(), true, null);
}
} else { } else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data); requestCode, resultCode, data);

View file

@ -14,6 +14,7 @@ import android.provider.DocumentsContract;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;

View file

@ -46,6 +46,8 @@ import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.modifications.ModifierSequenceDao;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier; import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ContributionUtils;
import fr.free.nrw.commons.utils.ExternalStorageUtils;
import timber.log.Timber; import timber.log.Timber;
//TODO: We should use this class to see how multiple uploads are handled, and then REMOVE it. //TODO: We should use this class to see how multiple uploads are handled, and then REMOVE it.
@ -55,7 +57,8 @@ public class MultipleShareActivity extends AuthenticatedActivity
AdapterView.OnItemClickListener, AdapterView.OnItemClickListener,
FragmentManager.OnBackStackChangedListener, FragmentManager.OnBackStackChangedListener,
MultipleUploadListFragment.OnMultipleUploadInitiatedHandler, MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
OnCategoriesSaveHandler { OnCategoriesSaveHandler,
ActivityCompat.OnRequestPermissionsResultCallback{
@Inject @Inject
MediaWikiApi mwApi; MediaWikiApi mwApi;
@ -76,6 +79,8 @@ public class MultipleShareActivity extends AuthenticatedActivity
private CategorizationFragment categorizationFragment; private CategorizationFragment categorizationFragment;
private boolean locationPermitted = false; private boolean locationPermitted = false;
private boolean isMultipleUploadsPrepared = false;
private boolean isMultipleUploadsFinalised = false; // Checks is user clicked to upload button or regret before this phase
@Override @Override
public Media getMediaAtPosition(int i) { public Media getMediaAtPosition(int i) {
@ -114,30 +119,25 @@ public class MultipleShareActivity extends AuthenticatedActivity
@Override @Override
public void OnMultipleUploadInitiated() { public void OnMultipleUploadInitiated() {
// No need to request external permission here, because if user can reach this point, then she permission granted
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Timber.d("OnMultipleUploadInitiated");
//Check for Storage permission that is required for upload. Do not allow user to proceed without permission, otherwise will crash multipleUploadBegins();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
} else {
multipleUploadBegins();
}
} else {
multipleUploadBegins();
}
} }
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
multipleUploadBegins(); Timber.d("onRequestPermissionsResult external storage permission granted");
prepareMultipleUpoadList();
} else {
// Permission is not granted, close activity
finish();
} }
} }
private void multipleUploadBegins() { private void multipleUploadBegins() {
Timber.d("Multiple upload begins"); Timber.d("Multiple upload begins");
final ProgressDialog dialog = new ProgressDialog(this); final ProgressDialog dialog = new ProgressDialog(this);
dialog.setIndeterminate(false); dialog.setIndeterminate(false);
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
@ -175,6 +175,7 @@ public class MultipleShareActivity extends AuthenticatedActivity
getSupportFragmentManager().beginTransaction() getSupportFragmentManager().beginTransaction()
.add(R.id.uploadsFragmentContainer, categorizationFragment, "categorization") .add(R.id.uploadsFragmentContainer, categorizationFragment, "categorization")
.commitAllowingStateLoss(); .commitAllowingStateLoss();
isMultipleUploadsFinalised = true;
//See http://stackoverflow.com/questions/7469082/getting-exception-illegalstateexception-can-not-perform-this-action-after-onsa //See http://stackoverflow.com/questions/7469082/getting-exception-illegalstateexception-can-not-perform-this-action-after-onsa
} }
@ -254,13 +255,40 @@ public class MultipleShareActivity extends AuthenticatedActivity
@Override @Override
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); /* This will be true if permission request is granted before we request. Otherwise we will
outState.putParcelableArrayList("uploadsList", photosList); * explicitly call operations under this method again.
*/
if (isMultipleUploadsPrepared) {
super.onSaveInstanceState(outState);
Timber.d("onSaveInstanceState multiple uploads is prepared, permission granted");
outState.putParcelableArrayList("uploadsList", photosList);
} else {
Timber.d("onSaveInstanceState multiple uploads is not prepared, permission not granted");
return;
}
} }
@Override @Override
protected void onAuthCookieAcquired(String authCookie) { protected void onAuthCookieAcquired(String authCookie) {
// Multiple uploads prepared boolean is used to decide when to call multipleUploadsBegin()
isMultipleUploadsFinalised = false;
isMultipleUploadsPrepared = false;
mwApi.setAuthCookie(authCookie); mwApi.setAuthCookie(authCookie);
if (!ExternalStorageUtils.isStoragePermissionGranted(this)) {
ExternalStorageUtils.requestExternalStoragePermission(this);
isMultipleUploadsPrepared = false;
return; // Postpone operation to do after gettion permission
} else {
isMultipleUploadsPrepared = true;
prepareMultipleUpoadList();
}
}
/**
* Prepares a list from files will be uploaded. Saves these files temporarily to external
* storage. Adds them to uploads list
*/
private void prepareMultipleUpoadList() {
Intent intent = getIntent(); Intent intent = getIntent();
if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
@ -270,6 +298,8 @@ public class MultipleShareActivity extends AuthenticatedActivity
for (int i = 0; i < urisList.size(); i++) { for (int i = 0; i < urisList.size(); i++) {
Contribution up = new Contribution(); Contribution up = new Contribution();
Uri uri = urisList.get(i); Uri uri = urisList.get(i);
// Use temporarily saved file Uri instead
uri = ContributionUtils.saveFileBeingUploadedTemporarily(this, uri);
up.setLocalUri(uri); up.setLocalUri(uri);
up.setTag("mimeType", intent.getType()); up.setTag("mimeType", intent.getType());
up.setTag("sequence", i); up.setTag("sequence", i);
@ -351,4 +381,24 @@ public class MultipleShareActivity extends AuthenticatedActivity
return null; return null;
} }
// If on back pressed before sharing
@Override
public void onBackPressed() {
super.onBackPressed();
}
@Override
protected void onStop() {
// Remove saved files if activity is stopped before upload operation, ie user changed mind
if (!isMultipleUploadsFinalised) {
if (photosList != null) {
for (Contribution contribution : photosList) {
Timber.d("User changed mind, didn't click to upload button, deleted file: "+contribution.getLocalUri());
ContributionUtils.removeTemporaryFile(contribution.getLocalUri());
}
}
}
super.onStop();
}
} }

View file

@ -22,7 +22,9 @@ import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi; import android.support.annotation.RequiresApi;
import android.support.design.widget.FloatingActionButton; import android.support.design.widget.FloatingActionButton;
import android.support.graphics.drawable.VectorDrawableCompat; import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@ -34,6 +36,8 @@ import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.drawee.view.SimpleDraweeView;
import com.github.chrisbanes.photoview.PhotoView; import com.github.chrisbanes.photoview.PhotoView;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -62,6 +66,8 @@ import fr.free.nrw.commons.modifications.ModifierSequenceDao;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier; import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.CategoryApi; import fr.free.nrw.commons.mwapi.CategoryApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ContributionUtils;
import fr.free.nrw.commons.utils.ExternalStorageUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber; import timber.log.Timber;
@ -77,7 +83,9 @@ import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_
public class ShareActivity public class ShareActivity
extends AuthenticatedActivity extends AuthenticatedActivity
implements SingleUploadFragment.OnUploadActionInitiated, implements SingleUploadFragment.OnUploadActionInitiated,
OnCategoriesSaveHandler { OnCategoriesSaveHandler,
ActivityCompat.OnRequestPermissionsResultCallback {
private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4; private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4;
//Had to make them class variables, to extract out the click listeners, also I see no harm in this //Had to make them class variables, to extract out the click listeners, also I see no harm in this
final Rect startBounds = new Rect(); final Rect startBounds = new Rect();
@ -120,6 +128,7 @@ public class ShareActivity
private String mimeType; private String mimeType;
private CategorizationFragment categorizationFragment; private CategorizationFragment categorizationFragment;
private Uri mediaUri; private Uri mediaUri;
private Uri contentProviderUri;
private Contribution contribution; private Contribution contribution;
private GPSExtractor gpsObj; private GPSExtractor gpsObj;
private String decimalCoords; private String decimalCoords;
@ -136,9 +145,12 @@ public class ShareActivity
private long ShortAnimationDuration; private long ShortAnimationDuration;
private boolean isFABOpen = false; private boolean isFABOpen = false;
private float startScaleFinal; private float startScaleFinal;
private Bundle savedInstanceState;
private boolean isUploadFinalised = false; // Checks is user clicked to upload button or regret before this phase
private boolean isZoom = false; private boolean isZoom = false;
/** /**
* Called when user taps the submit button. * Called when user taps the submit button.
* Requests Storage permission, if needed. * Requests Storage permission, if needed.
@ -184,6 +196,7 @@ public class ShareActivity
return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri) return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri)
&& (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) && (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED); != PackageManager.PERMISSION_GRANTED);
//return false;
} }
@ -204,13 +217,12 @@ public class ShareActivity
Timber.d("Cache the categories found"); Timber.d("Cache the categories found");
} }
uploadController.startUpload(title,mediaUri,description,mimeType,source,decimalCoords,wikiDataEntityId,c -> uploadController.startUpload(title, contentProviderUri, mediaUri, description, mimeType, source, decimalCoords, wikiDataEntityId, c -> {
ShareActivity.this.contribution = c;
{ showPostUpload();
ShareActivity.this.contribution = c; });
showPostUpload(); isUploadFinalised = true;
}); }
}
/** /**
* Starts CategorizationFragment after uploadBegins. * Starts CategorizationFragment after uploadBegins.
@ -271,7 +283,7 @@ public class ShareActivity
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
isUploadFinalised = false;
setContentView(R.layout.activity_share); setContentView(R.layout.activity_share);
ButterKnife.bind(this); ButterKnife.bind(this);
initBack(); initBack();
@ -282,9 +294,29 @@ public class ShareActivity
.setFailureImage(VectorDrawableCompat.create(getResources(), .setFailureImage(VectorDrawableCompat.create(getResources(),
R.drawable.ic_error_outline_black_24dp, getTheme())) R.drawable.ic_error_outline_black_24dp, getTheme()))
.build()); .build());
if (!ExternalStorageUtils.isStoragePermissionGranted(this)) {
this.savedInstanceState = savedInstanceState;
ExternalStorageUtils.requestExternalStoragePermission(this);
return; // Postpone operation to do after getting permission
} else {
receiveImageIntent();
createContributionWithReceivedIntent(savedInstanceState);
}
}
receiveImageIntent(); @Override
protected void onStop() {
// If upload is not finalised with failure or success, but contribution is created,
// we have to remove temp file, to prevent using unnecessary memory
if (!isUploadFinalised) {
if (mediaUri != null) {
ContributionUtils.removeTemporaryFile(mediaUri);
}
}
super.onStop();
}
private void createContributionWithReceivedIntent(Bundle savedInstanceState) {
if (savedInstanceState != null) { if (savedInstanceState != null) {
contribution = savedInstanceState.getParcelable("contribution"); contribution = savedInstanceState.getParcelable("contribution");
} }
@ -319,6 +351,11 @@ public class ShareActivity
if (Intent.ACTION_SEND.equals(intent.getAction())) { if (Intent.ACTION_SEND.equals(intent.getAction())) {
mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
contentProviderUri = mediaUri;
mediaUri = ContributionUtils.saveFileBeingUploadedTemporarily(this, mediaUri);
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 {
@ -401,17 +438,20 @@ public class ShareActivity
*/ */
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) { if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Storage (from submit button) - this needs to be separate from (1) because only the Timber.d("onRequestPermissionsResult external storage permission granted");
// submit button should bring user to next screen // You can receive image intent and save image to a temp file only if ext storage permission is granted
case REQUEST_PERM_ON_SUBMIT_STORAGE: { receiveImageIntent();
if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { createContributionWithReceivedIntent(savedInstanceState);
checkIfFileExists();
//Uploading only begins if storage permission granted from arrow icon if (requestCode == REQUEST_PERM_ON_SUBMIT_STORAGE) {
uploadBegins(); checkIfFileExists();
} //Uploading only begins if storage permission granted from arrow icon
uploadBegins();
} }
} else {
finish();
} }
} }

View file

@ -15,9 +15,10 @@ import android.os.AsyncTask;
import android.os.IBinder; import android.os.IBinder;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.TextUtils; import android.text.TextUtils;
import android.widget.Toast; import android.util.Log;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Date; import java.util.Date;
@ -100,7 +101,7 @@ public class UploadController {
* @param wikiDataEntityId * @param wikiDataEntityId
* @param onComplete the progress tracker * @param onComplete the progress tracker
*/ */
public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) { public void startUpload(String title, Uri contentProviderUri, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) {
Contribution contribution; Contribution contribution;
@ -133,6 +134,7 @@ public class UploadController {
contribution.setTag("mimeType", mimeType); contribution.setTag("mimeType", mimeType);
contribution.setSource(source); contribution.setSource(source);
contribution.setWikiDataEntityId(wikiDataEntityId); contribution.setWikiDataEntityId(wikiDataEntityId);
contribution.setContentProviderUri(contentProviderUri);
} }
@ -168,9 +170,12 @@ public class UploadController {
long length; long length;
ContentResolver contentResolver = context.getContentResolver(); ContentResolver contentResolver = context.getContentResolver();
try { try {
//TODO: understand do we really need this code
if (contribution.getDataLength() <= 0) { if (contribution.getDataLength() <= 0) {
Log.d("deneme","UploadController/doInBackground, contribution.getLocalUri():"+contribution.getLocalUri());
AssetFileDescriptor assetFileDescriptor = contentResolver AssetFileDescriptor assetFileDescriptor = contentResolver
.openAssetFileDescriptor(contribution.getLocalUri(), "r"); .openAssetFileDescriptor(Uri.fromFile(new File(contribution.getLocalUri().getPath())), "r");
if (assetFileDescriptor != null) { if (assetFileDescriptor != null) {
length = assetFileDescriptor.getLength(); length = assetFileDescriptor.getLength();
if (length == -1) { if (length == -1) {
@ -220,7 +225,7 @@ public class UploadController {
contribution.setDateCreated(new Date()); contribution.setDateCreated(new Date());
} }
} }
return contribution; return contribution;
} }
@Override @Override

View file

@ -10,9 +10,12 @@ import android.content.Intent;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import android.widget.Toast; import android.widget.Toast;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -180,13 +183,15 @@ public class UploadService extends HandlerService<Contribution> {
@SuppressLint("StringFormatInvalid") @SuppressLint("StringFormatInvalid")
private void uploadContribution(Contribution contribution) { private void uploadContribution(Contribution contribution) {
InputStream file; InputStream fileInputStream;
String notificationTag = contribution.getLocalUri().toString(); String notificationTag = contribution.getLocalUri().toString();
try { try {
//FIXME: Google Photos bug //FIXME: Google Photos bug
file = this.getContentResolver().openInputStream(contribution.getLocalUri()); File file1 = new File(contribution.getLocalUri().getPath());
fileInputStream = new FileInputStream(file1);
//fileInputStream = this.getContentResolver().openInputStream(contribution.getLocalUri());
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Timber.d("File not found"); Timber.d("File not found");
Toast fileNotFound = Toast.makeText(this, R.string.upload_failed, Toast.LENGTH_LONG); Toast fileNotFound = Toast.makeText(this, R.string.upload_failed, Toast.LENGTH_LONG);
@ -194,9 +199,9 @@ public class UploadService extends HandlerService<Contribution> {
return; return;
} }
//As the file is null there's no point in continuing the upload process //As the fileInputStream is null there's no point in continuing the upload process
//mwapi.upload accepts a NonNull input stream //mwapi.upload accepts a NonNull input stream
if(file == null) { if(fileInputStream == null) {
Timber.d("File not found"); Timber.d("File not found");
return; return;
} }
@ -244,7 +249,7 @@ public class UploadService extends HandlerService<Contribution> {
getString(R.string.upload_progress_notification_title_finishing, contribution.getDisplayTitle()), getString(R.string.upload_progress_notification_title_finishing, contribution.getDisplayTitle()),
contribution contribution
); );
UploadResult uploadResult = mwApi.uploadFile(filename, file, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater); UploadResult uploadResult = mwApi.uploadFile(filename, fileInputStream, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater, contribution.getLocalUri(), contribution.getContentProviderUri());
Timber.d("Response is %s", uploadResult.toString()); Timber.d("Response is %s", uploadResult.toString());

View file

@ -0,0 +1,95 @@
package fr.free.nrw.commons.utils;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import java.io.File;
import java.util.Random;
import timber.log.Timber;
/**
* This class includes utility methods for uploading process of images.
*/
public class ContributionUtils {
private static String TEMP_EXTERNAL_DIRECTORY =
android.os.Environment.getExternalStorageDirectory().getPath()+
File.separatorChar+"UploadingByCommonsApp";
/**
* Saves images temporarily to a fixed folder and use Uri of that file during upload process.
* Otherwise, temporary Uri provided by content provider sometimes points to a null space and
* consequently upload fails. See: issue #1400A and E.
* Not: Saved image will be deleted, our directory will be empty after upload process.
* @return URI of saved image
*/
public static Uri saveFileBeingUploadedTemporarily(Context context, Uri URIfromContentProvider) {
// TODO add exceptions for Google Drive URİ is needed
Uri result = null;
if (FileUtils.checkIfDirectoryExists(TEMP_EXTERNAL_DIRECTORY)) {
String destinationFilename = decideTempDestinationFileName();
result = FileUtils.saveFileFromURI(context, URIfromContentProvider, destinationFilename);
} else { // If directory doesn't exist, create it and recursive call current method to check again
File file = new File(TEMP_EXTERNAL_DIRECTORY);
if (file.mkdirs()) {
Timber.d("saveFileBeingUploadedTemporarily() parameters: URI from Content Provider %s", URIfromContentProvider);
result = saveFileBeingUploadedTemporarily(context, URIfromContentProvider); // If directory is created
} else { //An error occurred to create directory
Timber.e("saveFileBeingUploadedTemporarily() parameters: URI from Content Provider %s", URIfromContentProvider);
}
}
return result;
}
/**
* Removes temp file created during upload
* @param tempFileUri
*/
public static void removeTemporaryFile(Uri tempFileUri) {
//TODO: do I have to notify file system about deletion?
File tempFile = new File(tempFileUri.getPath());
if (tempFile.exists()) {
boolean isDeleted= tempFile.delete();
Timber.e("removeTemporaryFile() parameters: URI tempFileUri %s, deleted status %b", tempFileUri, isDeleted);
}
}
private static String decideTempDestinationFileName() {
int i = 0;
while (true) {
if (new File(TEMP_EXTERNAL_DIRECTORY +File.separatorChar+i+"_tmp").exists()) {
// This file is in use, try enother file
i++;
} else {
// Use time stamp for file name, so that two temporary file never has same file name
// to prevent previous file reference bug
Long tsLong = System.currentTimeMillis()/1000;
String ts = tsLong.toString();
// For multiple uploads, time randomisation should be combined with another random
// parameter, since they created at same time
int multipleUploadRandomParameter = new Random().nextInt(100);
return TEMP_EXTERNAL_DIRECTORY +File.separatorChar+ts+multipleUploadRandomParameter+"_tmp";
}
}
}
public static void emptyTemporaryDirectory() {
File dir = new File(TEMP_EXTERNAL_DIRECTORY);
if (dir.isDirectory())
{
String[] children = dir.list();
if (children != null && children.length >0) {
for (int i = 0; i < children.length; i++)
{
new File(dir, children[i]).delete();
}
}
}
}
}

View file

@ -0,0 +1,47 @@
package fr.free.nrw.commons.utils;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.v4.app.ActivityCompat;
import android.util.Log;
import timber.log.Timber;
/**
* Created by root on 23.07.2018.
*/
public class ExternalStorageUtils {
/**
* Checks if external storage permission is granted
* @param context activity we are on
* @return true if permission is granted, false if not
*/
public static boolean isStoragePermissionGranted(Context context) {
if (Build.VERSION.SDK_INT >= 23) {
if (context.checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
Timber.d("External storage permission granted, API >= 23");
return true;
} else {
Timber.d("External storage permission not granted, API >= 23");
return false;
}
} else { //permission is automatically granted on sdk<23 upon installation
Timber.d("External storage permission granted before, API < 23");
return true;
}
}
/**
* Requests external storage permission
* @param context activity we are on
*/
public static void requestExternalStoragePermission(Context context) {
Timber.d("External storage permission requested");
ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}
}

View file

@ -0,0 +1,89 @@
package fr.free.nrw.commons.utils;
import android.content.Context;
import android.net.Uri;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
/**
* Created for file operations
*/
public class FileUtils {
/**
* Saves file from source URI to destination.
* @param sourceUri Uri which points to file to be saved
* @param destinationFilename where file will be located at
* @return Uri points to file saved
*/
public static Uri saveFileFromURI(Context context, Uri sourceUri, String destinationFilename) {
File file = new File(destinationFilename);
if (file.exists()) {
file.delete();
}
InputStream in = null;
OutputStream out = null;
try {
in = context.getContentResolver().openInputStream(sourceUri);
out = new FileOutputStream(new File(destinationFilename));
byte[] buf = new byte[1024];
int len;
while((len=in.read(buf))>0){
out.write(buf,0,len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
out.close();
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return Uri.parse("file://" + destinationFilename);
}
/**
* Checks if directory exists
* @param pathToCheck path of directory to check
* @return true if directory exists, false otherwise
*/
public static boolean checkIfDirectoryExists(String pathToCheck) {
File director = new File(pathToCheck);
if(director.exists() && director.isDirectory()) {
return true;
} else {
return false;
}
}
/**
* Creates new directory.
* @param pathToCreateAt where directory will be created at
* @return true if directory is created, false if an error occured, or already exists.
*/
public static boolean createDirectory(String pathToCreateAt) {
File directory = new File(pathToCreateAt);
if (!directory.exists()) {
return directory.mkdirs(); //true if directory is created
} else {
return false; //false if file already exists
}
}
}