diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..0e56f734c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: android +android: + components: + - platform-tools + - tools + - build-tools-23.0.3 + - extra-google-m2repository + - extra-android-m2repository + - android-23 + - sys-img-x86-android-18 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ca3949a6..0c2f3c7e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Wikimedia Commons for Android +##v1.21 +- Fixed Google Photos multiple share crash + +##v1.20 +- Hotfix for data=null crash + +##v1.19 +- Fixed adapter crash +- Attempt at fixing Google Photos crash + +## v1.18 +- Fixed various crashes +- Fixed camera and gallery for API 23 + ## v1.17 - Fixed various crashes - Fixed 'Desc/license/categories empty' bug diff --git a/README.md b/README.md index 774918b7f..e374bc9a7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Wikimedia Commons Android app # +# Upload to Commons [![Build status](https://api.travis-ci.org/nicolas-raoul/apps-android-commons.svg)](https://travis-ci.org/nicolas-raoul/apps-android-commons) Upload pictures from your Android phone/tablet to Wikimedia Commons. diff --git a/app/build.gradle b/app/build.gradle index 063c0c441..89cfc74d7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,6 +13,7 @@ dependencies { compile 'com.android.support:appcompat-v7:23.4.0' compile 'com.android.support:design:23.4.0' + //noinspection GradleDependency - old version has required feature compile 'com.google.code.gson:gson:1.4' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6476610a6..9240c241c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ + android:versionCode="39" + android:versionName="1.21" > @@ -44,9 +44,7 @@ > - - @@ -58,9 +56,7 @@ > - - diff --git a/app/src/main/java/fr/free/nrw/commons/HandlerService.java b/app/src/main/java/fr/free/nrw/commons/HandlerService.java index f2e30ffec..e29d64fdd 100644 --- a/app/src/main/java/fr/free/nrw/commons/HandlerService.java +++ b/app/src/main/java/fr/free/nrw/commons/HandlerService.java @@ -16,6 +16,7 @@ public abstract class HandlerService extends Service { @Override public void handleMessage(Message msg) { + //FIXME: Google Photos bug handle(msg.what, (T)msg.obj); stopSelf(msg.arg1); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index b37e1b018..9cee652d3 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -61,18 +61,20 @@ public class ContributionController { } public void startGalleryPick() { + //FIXME: Starts gallery (opens Google Photos) Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT); pickImageIntent.setType("image/*"); fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY); } - public void handleImagePicked(int requestCode, Intent data) { + public void handleImagePicked(int requestCode, Uri imageData) { Intent shareIntent = new Intent(activity, ShareActivity.class); shareIntent.setAction(Intent.ACTION_SEND); switch(requestCode) { case SELECT_FROM_GALLERY: - shareIntent.setType(activity.getContentResolver().getType(data.getData())); - shareIntent.putExtra(Intent.EXTRA_STREAM, data.getData()); + //FIXME: Handles image picked from gallery (from Google Photos) + shareIntent.setType(activity.getContentResolver().getType(imageData)); + shareIntent.putExtra(Intent.EXTRA_STREAM, imageData); shareIntent.putExtra(UploadService.EXTRA_SOURCE, fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY); break; case SELECT_FROM_CAMERA: @@ -82,7 +84,11 @@ public class ContributionController { break; } Log.i("Image", "Image selected"); - activity.startActivity(shareIntent); + try { + activity.startActivity(shareIntent); + } catch (SecurityException e) { + Log.e("ContributionController", "Security Exception", e); + } } public void saveState(Bundle outState) { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java index 7884af248..a4abe07c6 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java @@ -227,6 +227,7 @@ public class ContributionsActivity } + //FIXME: Potential cause of wrong image display bug public Media getMediaAtPosition(int i) { if (contributionsList.getAdapter() == null) { // not yet ready to return data diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java index 669ed0099..8ea7d9696 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java @@ -36,6 +36,7 @@ class ContributionsListAdapter extends CursorAdapter { return parent; } + //FIXME: Potential cause of wrong image display bug @Override public void bindView(View view, Context context, Cursor cursor) { final ContributionViewHolder views = (ContributionViewHolder)view.getTag(); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index 7ebbc0201..df2257896 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -1,11 +1,17 @@ package fr.free.nrw.commons.contributions; +import android.Manifest; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -24,6 +30,7 @@ import fr.free.nrw.commons.AboutActivity; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.SettingsActivity; +import fr.free.nrw.commons.upload.UploadService; public class ContributionsListFragment extends Fragment { @@ -83,9 +90,15 @@ public class ContributionsListFragment extends Fragment { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { + //FIXME: must get the file data for Google Photos when receive the intent answer, in the onActivityResult method super.onActivityResult(requestCode, resultCode, data); - if(resultCode == Activity.RESULT_OK) { - controller.handleImagePicked(requestCode, data); + + if (data != null) { + Log.d("Contributions", "OnActivityResult() parameters: Result code: " + resultCode + " Data: " + data.toString()); + Uri imageData = data.getData(); + controller.handleImagePicked(requestCode, imageData); + } else { + Log.e("Contributions", "OnActivityResult() parameters: Result code: " + resultCode + " Data: null"); } } @@ -94,8 +107,20 @@ public class ContributionsListFragment extends Fragment { public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case R.id.menu_from_gallery: - controller.startGalleryPick(); - return true; + //Gallery crashes before reach ShareActivity screen so must implement permissions check here + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission(this.getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this.getActivity(), new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1); + return true; + } else { + controller.startGalleryPick(); + return true; + } + } + else { + controller.startGalleryPick(); + return true; + } case R.id.menu_from_camera: controller.startCameraCapture(); return true; @@ -129,6 +154,23 @@ public class ContributionsListFragment extends Fragment { } } + @Override + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + switch (requestCode) { + // 1 = Storage allowed when gallery selected + case 1: { + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + controller.startGalleryPick(); + } else { + return; + } + return; + } + } + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { menu.clear(); // See http://stackoverflow.com/a/8495697/17865 diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index a9a2b2066..62ecf6c37 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -84,19 +84,23 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa View view = inflater.inflate(R.layout.fragment_media_detail_pager, container, false); pager = (ViewPager) view.findViewById(R.id.mediaDetailsPager); pager.setOnPageChangeListener(this); + + final MediaDetailAdapter adapter = new MediaDetailAdapter(getChildFragmentManager()); + if(savedInstanceState != null) { final int pageNumber = savedInstanceState.getInt("current-page"); // Adapter doesn't seem to be loading immediately. // Dear God, please forgive us for our sins view.postDelayed(new Runnable() { public void run() { - pager.setAdapter(new MediaDetailAdapter(getChildFragmentManager())); + pager.setAdapter(adapter); pager.setCurrentItem(pageNumber, false); getActivity().supportInvalidateOptionsMenu(); + adapter.notifyDataSetChanged(); } }, 100); } else { - pager.setAdapter(new MediaDetailAdapter(getChildFragmentManager())); + pager.setAdapter(adapter); } return view; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java index d030eb201..2f76e3149 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java @@ -57,15 +57,25 @@ public class GPSExtractor { provider = locationManager.getBestProvider(criteria, true); myLocationListener = new MyLocationListener(); - locationManager.requestLocationUpdates(provider, 400, 1, myLocationListener); - Location location = locationManager.getLastKnownLocation(provider); - if (location != null) { - myLocationListener.onLocationChanged(location); + try { + locationManager.requestLocationUpdates(provider, 400, 1, myLocationListener); + Location location = locationManager.getLastKnownLocation(provider); + if (location != null) { + myLocationListener.onLocationChanged(location); + } + } catch (IllegalArgumentException e) { + Log.e(TAG, "Illegal argument exception", e); + } catch (SecurityException e) { + Log.e(TAG, "Security exception", e); } } protected void unregisterLocationManager() { - locationManager.removeUpdates(myLocationListener); + try { + locationManager.removeUpdates(myLocationListener); + } catch (SecurityException e) { + Log.e(TAG, "Security exception", e); + } } /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java index 0aa135428..5fa881a60 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java @@ -2,13 +2,18 @@ package fr.free.nrw.commons.upload; import java.util.*; +import android.Manifest; import android.app.*; import android.content.*; +import android.content.pm.PackageManager; import android.database.DataSetObserver; import android.net.*; import android.os.*; +import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentManager; +import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; +import android.util.Log; import android.view.*; import android.view.inputmethod.InputMethodManager; import android.widget.*; @@ -77,6 +82,32 @@ public class MultipleShareActivity } public void OnMultipleUploadInitiated() { + + 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 + 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 + public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { + if (requestCode == 1) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + multipleUploadBegins(); + } + } + } + + private void multipleUploadBegins() { + + Log.d("MultipleShareActivity", "Multiple upload begins"); + final ProgressDialog dialog = new ProgressDialog(MultipleShareActivity.this); dialog.setIndeterminate(false); dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); @@ -114,7 +145,8 @@ public class MultipleShareActivity } getSupportFragmentManager().beginTransaction() .add(R.id.uploadsFragmentContainer, categorizationFragment, "categorization") - .commit(); + .commitAllowingStateLoss(); + //See http://stackoverflow.com/questions/7469082/getting-exception-illegalstateexception-can-not-perform-this-action-after-onsa } public void onCategoriesSave(ArrayList categories) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java index bdb58fa1d..6079a4479 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java @@ -12,6 +12,7 @@ import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityCompat; import android.support.v4.app.NavUtils; import android.support.v4.content.ContextCompat; +import android.support.v7.app.AlertDialog; import android.util.Log; import android.view.MenuItem; import android.view.View; @@ -72,11 +73,36 @@ public class ShareActivity private boolean storagePermission = false; private boolean locationPermission = false; + private String title; + private String description; + private Snackbar snackbar; + public ShareActivity() { super(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE); } + /** + * Called when user taps the submit button + */ public void uploadActionInitiated(String title, String description) { + + this.title = title; + this.description = description; + + 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 + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 4); + } else { + uploadBegins(); + } + } else { + uploadBegins(); + } + } + + private void uploadBegins() { + Toast startingToast = Toast.makeText(getApplicationContext(), R.string.uploading_started, Toast.LENGTH_LONG); startingToast.show(); @@ -160,7 +186,6 @@ public class ShareActivity protected void onAuthCookieAcquired(String authCookie) { app.getApi().setAuthCookie(authCookie); - shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView"); categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization"); if(shareView == null && categorizationFragment == null) { @@ -180,11 +205,6 @@ public class ShareActivity finish(); } - /** - * Initiates retrieval of image coordinates or user coordinates, and caching of coordinates. - * Then initiates the calls to MediaWiki API through an instance of MwVolleyApi. - */ - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -234,7 +254,7 @@ public class ShareActivity if (useNewPermissions && (!storagePermission || !locationPermission)) { if (!storagePermission && !locationPermission) { String permissionRationales = getResources().getString(R.string.storage_permission_rationale) + "\n" + getResources().getString(R.string.location_permission_rationale); - Snackbar snackbar = Snackbar.make(findViewById(android.R.id.content), permissionRationales, + snackbar = Snackbar.make(findViewById(android.R.id.content), permissionRationales, Snackbar.LENGTH_INDEFINITE) .setAction(R.string.ok, new View.OnClickListener() { @Override @@ -280,7 +300,7 @@ public class ShareActivity public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { - // 1 = Storage + // 1 = Storage (from snackbar) case 1: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { @@ -306,6 +326,22 @@ public class ShareActivity && grantResults[1] == PackageManager.PERMISSION_GRANTED) { getLocationData(); } + return; + } + // 4 = Storage (from submit button) - this needs to be separate from (1) because only the + // submit button should bring user to next screen + case 4: { + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + //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 + getFileMetadata(); + + //Uploading only begins if storage permission granted from arrow icon + uploadBegins(); + snackbar.dismiss(); + } + return; } } } @@ -332,6 +368,10 @@ public class ShareActivity useImageCoords(); } + /** + * Initiates retrieval of image coordinates or user coordinates, and caching of coordinates. + * Then initiates the calls to MediaWiki API through an instance of MwVolleyApi. + */ public void useImageCoords() { if(decimalCoords != null) { Log.d(TAG, "Decimal coords of image: " + decimalCoords); @@ -388,5 +428,4 @@ public class ShareActivity } return super.onOptionsItemSelected(item); } - } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index ba896da37..09acdd6f5 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.upload; +import android.Manifest; import android.app.Activity; import android.content.ComponentName; import android.content.Context; @@ -13,7 +14,9 @@ import android.os.IBinder; import android.preference.PreferenceManager; import android.provider.MediaStore; import android.text.TextUtils; +import android.util.Log; import android.webkit.MimeTypeMap; +import android.widget.Toast; import java.io.IOException; import java.util.Date; @@ -21,6 +24,7 @@ import java.util.Date; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.Prefs; +import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.contributions.Contribution; @@ -88,6 +92,8 @@ public class UploadController { } public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) { + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); if(TextUtils.isEmpty(contribution.getCreator())) { @@ -101,6 +107,8 @@ public class UploadController { String license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA); contribution.setLicense(license); + + //FIXME: Add permission request here. Only executeAsyncTask if permission has been granted Utils.executeAsyncTask(new AsyncTask() { // Fills up missing information about Contributions @@ -119,7 +127,11 @@ public class UploadController { contribution.setDataLength(length); } } catch(IOException e) { - throw new RuntimeException(e); + Log.e("UploadController", "IO Exception: ", e); + } catch(NullPointerException e) { + Log.e("UploadController", "Null Pointer Exception: ", e); + } catch(SecurityException e) { + Log.e("UploadController", "Security Exception: ", e); } String mimeType = (String)contribution.getTag("mimeType"); @@ -142,10 +154,10 @@ public class UploadController { dateCreated = new Date(); } contribution.setDateCreated(dateCreated); + cursor.close(); } else { contribution.setDateCreated(new Date()); } - cursor.close(); } return contribution; @@ -159,5 +171,4 @@ public class UploadController { } }); } - } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 32482095b..2adb71776 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -111,6 +111,7 @@ public class UploadService extends HandlerService { protected void handle(int what, Contribution contribution) { switch(what) { case ACTION_UPLOAD_FILE: + //FIXME: Google Photos bug uploadContribution(contribution); break; default: @@ -173,6 +174,7 @@ public class UploadService extends HandlerService { String notificationTag = contribution.getLocalUri().toString(); try { + //FIXME: Google Photos bug file = this.getContentResolver().openInputStream(contribution.getLocalUri()); } catch(FileNotFoundException e) { Log.d("Exception", "File not found"); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e9e5af8e2..bf52e5853 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -150,8 +150,8 @@ Campaigns Refresh - Recommended: Storage for photo metadata - Optional: Current location for category suggestions + Required permission: Read external storage. App cannot function without this. + Optional permission: Get current location for category suggestions OK Back