diff --git a/CHANGELOG.md b/CHANGELOG.md index 035835839..7f3dfc30a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Wikimedia Commons for Android +## v2.7.0 +- New Nearby Places UI with direct uploads (and associated category suggestions) +- Added two-factor authentication login +- Added Notifications activity to display user talk messages +- Added real-time location tracking in Nearby +- Added "rate us", "translate", and FB link in About +- Improvements to UI of navigation drawer, tutorial, media details view, login activity and Settings +- Added option to nominate picture for deletion in media details view +- Too many bug and crash fixes to mention! + ## v2.6.7 - Added null checks to prevent frequent crashes in ModificationsSyncAdapter diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee7f42e06..0f1feeac7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1,19 @@ -Please see our guidelines in the wiki: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21 +Thanks for considering to contribute to this project! A few guidelines for +people who want to contribute their code to this software are documented in +[this project's Wiki](https://github.com/commons-app/apps-android-commons/wiki/Contributing-Guidelines). +If you're not sure where to start head on to [this wiki page](https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome!). + +Here's a gist of the guidelines, + +# Make separate commits for logically separate changes + +# Describe your changes well in the commit message + +The first line of the commit message should be a short description of what has +changed. It is also good to prefix the first line with "area: " where the "area" +is a filename or identifier for the general area of the code being modified. +The body should provide a meaningful commit message. + +# Write tests for your code (if possible) + +# Make sure the Wiki pages don't become stale by updating them (if needed) diff --git a/CREDITS b/CREDITS index a4a4ae0f5..6847ac9b6 100644 --- a/CREDITS +++ b/CREDITS @@ -30,6 +30,16 @@ their contribution to the product. * Bruke Mekuria Mulugeta * Paul Hawke * Vishan Seru +* Abhishek Poonia +* Ayushi Negi +* Harisanker Pradeep +* Hassan Ismaeel +* Jatin Rao +* Meghna Gupta +* S Balakrishnan +* Suchit Kar +* Tanvi Dadu +* Ujjwal Agrawal 3rd party open source libraries used: * Butterknife diff --git a/app/build.gradle b/app/build.gradle index ac181a36c..46008d1c2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,18 +11,19 @@ dependencies { implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' implementation 'in.yuvi:http.fluent:1.3' implementation 'com.android.volley:volley:1.0.0' - implementation 'ch.acra:acra:4.7.0' + implementation 'ch.acra:acra:4.9.2' implementation 'org.mediawiki:api:1.3' implementation 'commons-codec:commons-codec:1.10' implementation 'com.github.pedrovgs:renderers:3.3.3' implementation 'com.google.code.gson:gson:2.8.1' implementation 'com.jakewharton.timber:timber:4.5.1' implementation 'info.debatty:java-string-similarity:0.24' + implementation 'com.borjabravo:readmoretextview:2.1.0' + implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.4.1@aar'){ transitive=true } - implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION" implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION" implementation "com.android.support:design:$SUPPORT_LIB_VERSION" @@ -41,8 +42,6 @@ dependencies { // explicitly depend on RxJava's latest version for bug fixes and new features. implementation 'com.android.support:multidex:1.0.3' - testImplementation "org.robolectric:multidex:3.4.2" - implementation 'io.reactivex.rxjava2:rxjava:2.1.2' implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0' @@ -54,33 +53,25 @@ dependencies { implementation "com.google.dagger:dagger:$DAGGER_VERSION" implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" - kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" + testImplementation "org.robolectric:multidex:3.4.2" testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" - androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" - + testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testImplementation 'junit:junit:4.12' testImplementation 'org.robolectric:robolectric:3.7.1' - testImplementation 'org.mockito:mockito-all:1.10.19' - + testImplementation 'com.nhaarman:mockito-kotlin:1.5.0' testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' + + androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION" - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2-alpha1' debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" - - implementation "com.google.dagger:dagger:$DAGGER_VERSION" - implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" - kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" - kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" - - implementation 'com.borjabravo:readmoretextview:2.1.0' - implementation 'com.android.support.constraint:constraint-layout:1.0.2' } android { @@ -91,8 +82,8 @@ android { defaultConfig { applicationId 'fr.free.nrw.commons' - versionCode 82 - versionName '2.6.7' + versionCode 83 + versionName '2.7.0' setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) minSdkVersion project.minSdkVersion @@ -137,6 +128,7 @@ android { buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"" + buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\"" buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\"" buildConfigField "String", "EVENTLOG_URL", "\"https://www.wikimedia.org/beacon/event\"" buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\"" @@ -152,6 +144,7 @@ android { buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"" + buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\"" buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\"" buildConfigField "String", "EVENTLOG_URL", "\"https://commons.wikimedia.beta.wmflabs.org/beacon/event\"" buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\"" diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java index ecf4c21f0..e6bf34736 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java @@ -6,11 +6,15 @@ import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.text.Html; import android.text.SpannableString; import android.text.style.UnderlineSpan; import android.util.Log; import android.support.customtabs.CustomTabsIntent; import android.support.v4.content.ContextCompat; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.widget.ArrayAdapter; import android.widget.LinearLayout; @@ -62,6 +66,18 @@ public class AboutActivity extends NavigationBaseActivity { content.setSpan(new UnderlineSpan(), 0, content.length(), 0); faqText.setText(content); versionText.setText(BuildConfig.VERSION_NAME); + TextView rate_us = findViewById(R.id.about_rate_us); + TextView privacy_policy = findViewById(R.id.about_privacy_policy); + TextView translate = findViewById(R.id.about_translate); + TextView credits = findViewById(R.id.about_credits); + TextView faq = findViewById(R.id.about_faq); + + rate_us.setText(Html.fromHtml(getString(R.string.about_rate_us))); + privacy_policy.setText(Html.fromHtml(getString(R.string.about_privacy_policy))); + translate.setText(Html.fromHtml(getString(R.string.about_translate))); + credits.setText(Html.fromHtml(getString(R.string.about_credits))); + faq.setText(Html.fromHtml(getString(R.string.about_faq))); + initDrawer(); } @@ -108,6 +124,28 @@ public class AboutActivity extends NavigationBaseActivity { Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Frequently-Asked-Questions\\")); } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_about, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.share_app_icon: + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, "http://play.google.com/store/apps/details?id=fr.free.nrw.commons"); + sendIntent.setType("text/plain"); + startActivity(Intent.createChooser(sendIntent, "Share app via...")); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + @OnClick(R.id.about_translate) public void launchTranslate(View view) { final ArrayAdapter languageAdapter = new ArrayAdapter(AboutActivity.this, diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index 5638db97e..57cb5fad1 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -54,9 +54,11 @@ public class CommonsApplication extends MultiDexApplication { public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com"; + public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback"; + public static final String LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com"; - public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback"; + public static final String LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Android App (%s) Logs"; private RefWatcher refWatcher; diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 726d787f3..5f6a498ea 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -43,6 +43,7 @@ public class Media implements Parcelable { protected String license; protected String creator; protected ArrayList categories; // as loaded at runtime? + protected boolean requestedDeletion; private Map descriptions; // multilingual descriptions as loaded private HashMap tags = new HashMap<>(); private @Nullable LatLng coordinates; @@ -416,4 +417,12 @@ public class Media implements Parcelable { parcel.writeStringList(categories); parcel.writeMap(descriptions); } + + public void setRequestedDeletion(){ + requestedDeletion = true; + } + + public boolean getRequestedDeletion(){ + return requestedDeletion; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index 25e778b74..2d79a6c4f 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -35,6 +35,7 @@ import timber.log.Timber; public class MediaDataExtractor { private final MediaWikiApi mediaWikiApi; private boolean fetched; + private boolean deletionStatus; private ArrayList categories; private Map descriptions; private String license; @@ -59,6 +60,14 @@ public class MediaDataExtractor { throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again."); } + try{ + Timber.d("Nominated for deletion: " + mediaWikiApi.pageExists("Commons:Deletion_requests/"+filename)); + deletionStatus = mediaWikiApi.pageExists("Commons:Deletion_requests/"+filename); + } + catch (Exception e){ + Timber.d(e.getMessage()); + } + MediaResult result = mediaWikiApi.fetchMediaByFilename(filename); // In-page category links are extracted from source, as XML doesn't cover [[links]] @@ -296,6 +305,9 @@ public class MediaDataExtractor { if (license != null) { media.setLicense(license); } + if (deletionStatus){ + media.setRequestedDeletion(); + } // add author, date, etc fields } diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java index 05873782c..91c23ce26 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -177,7 +177,7 @@ public class Utils { } } - public static void handleWebUrl(Context context,Uri url){ + public static void handleWebUrl(Context context, Uri url) { Intent browserIntent = new Intent(Intent.ACTION_VIEW, url); if (browserIntent.resolveActivity(context.getPackageManager()) == null) { Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT); diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java index a873136fe..a41a52139 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java @@ -280,8 +280,11 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { private Observable directCategories() { String directCategory = directPrefs.getString("Category", ""); + // Strip newlines to prevent blank categories, and to tidy existing categories + directCategory = directCategory.replace("\n", ""); + List categoryList = new ArrayList<>(); - Timber.d("Direct category found: " + directCategory); + Timber.d("Direct category found: " + "'" + directCategory + "'"); if (!directCategory.equals("")) { hasDirectCategories = true; diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java index a5202046b..010e97095 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java @@ -105,6 +105,7 @@ public class CategoryDao { return items; } + @NonNull Category fromCursor(Cursor cursor) { // Hardcoding column positions! return new Category( diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java index 07ae8cb81..0cce496f0 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java @@ -1,10 +1,13 @@ package fr.free.nrw.commons.delete; -import android.app.AlertDialog; +import android.app.NotificationManager; import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; import android.os.AsyncTask; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationCompat.Builder; +import android.view.Gravity; +import android.widget.Toast; + import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Locale; @@ -18,17 +21,18 @@ import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; -import static android.support.v4.content.ContextCompat.startActivity; +import static android.support.v4.app.NotificationCompat.DEFAULT_ALL; +import static android.support.v4.app.NotificationCompat.PRIORITY_HIGH; -public class DeleteTask extends AsyncTask { - - private static final int SUCCESS = 0; - private static final int FAILED = -1; - private static final int ALREADY_DELETED = -2; +public class DeleteTask extends AsyncTask { @Inject MediaWikiApi mwApi; @Inject SessionManager sessionManager; + public static final int NOTIFICATION_DELETE = 1; + + private NotificationManager notificationManager; + private Builder notificationBuilder; private Context context; private Media media; private String reason; @@ -45,10 +49,19 @@ public class DeleteTask extends AsyncTask { .getInstance(context.getApplicationContext()) .getCommonsApplicationComponent() .inject(this); + + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(context); + Toast toast = new Toast(context); + toast.setGravity(Gravity.CENTER,0,0); + toast = Toast.makeText(context,"Trying to nominate "+media.getDisplayTitle()+ " for deletion",Toast.LENGTH_SHORT); + toast.show(); } @Override - protected Integer doInBackground(Void ...voids) { + protected Boolean doInBackground(Void ...voids) { + publishProgress(0); + String editToken; String authCookie; String summary = "Nominating " + media.getFilename() +" for deletion."; @@ -56,27 +69,6 @@ public class DeleteTask extends AsyncTask { authCookie = sessionManager.getAuthCookie(); mwApi.setAuthCookie(authCookie); - try{ - if (mwApi.pageExists("Commons:Deletion_requests/"+media.getFilename())){ - return ALREADY_DELETED; - } - } - catch (Exception e) { - Timber.d(e.getMessage()); - return FAILED; - } - - try { - editToken = mwApi.getEditToken(); - } - catch (Exception e){ - Timber.d(e.getMessage()); - return FAILED; - } - if (editToken.equals("+\\")) { - return FAILED; - } - Calendar calendar = Calendar.getInstance(); String fileDeleteString = "{{delete|reason=" + reason + "|subpage=" +media.getFilename() + @@ -84,91 +76,106 @@ public class DeleteTask extends AsyncTask { "|month=" + calendar.getDisplayName(Calendar.MONTH,Calendar.LONG, Locale.getDefault()) + "|year=" + calendar.get(Calendar.YEAR) + "}}"; - try{ - mwApi.prependEdit(editToken,fileDeleteString+"\n", - media.getFilename(),summary); - } - catch (Exception e) { - Timber.d(e.getMessage()); - return FAILED; - } String subpageString = "=== [[:" + media.getFilename() + "]] ===\n" + reason + " ~~~~"; - try{ - mwApi.edit(editToken,subpageString+"\n", - "Commons:Deletion_requests/"+media.getFilename(),summary); - } - catch (Exception e) { - Timber.d(e.getMessage()); - return FAILED; - } String logPageString = "\n{{Commons:Deletion requests/" + media.getFilename() + "}}\n"; SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); String date = sdf.format(calendar.getTime()); - try{ - mwApi.appendEdit(editToken,logPageString+"\n", - "Commons:Deletion_requests/"+date,summary); - } - catch (Exception e) { - Timber.d(e.getMessage()); - return FAILED; - } String userPageString = "\n{{subst:idw|" + media.getFilename() + "}} ~~~~"; - try{ + + try { + editToken = mwApi.getEditToken(); + if (editToken.equals("+\\")) { + return false; + } + publishProgress(1); + + mwApi.prependEdit(editToken,fileDeleteString+"\n", + media.getFilename(),summary); + publishProgress(2); + + mwApi.edit(editToken,subpageString+"\n", + "Commons:Deletion_requests/"+media.getFilename(),summary); + publishProgress(3); + + mwApi.appendEdit(editToken,logPageString+"\n", + "Commons:Deletion_requests/"+date,summary); + publishProgress(4); + mwApi.appendEdit(editToken,userPageString+"\n", "User_Talk:"+sessionManager.getCurrentAccount().name,summary); + publishProgress(5); } catch (Exception e) { Timber.d(e.getMessage()); - return FAILED; + return false; } - return SUCCESS; + return true; } @Override - protected void onPostExecute(Integer result) { + protected void onProgressUpdate (Integer... values){ + super.onProgressUpdate(values); + String message = ""; - String title = ""; - switch (result){ - case SUCCESS: - title = "Success"; - message = "Successfully nominated " + media.getDisplayTitle() + " deletion.\n" + - "Check the webpage for more details"; + switch (values[0]){ + case 0: + message = "Getting token"; break; - case FAILED: - title = "Failed"; - message = "Could not request deletion. Something went wrong."; + case 1: + message = "Adding delete message to file"; break; - case ALREADY_DELETED: - title = "Already Nominated"; - message = media.getDisplayTitle() + " has already been nominated for deletion.\n" + - "Check the webpage for more details"; + case 2: + message = "Creating Delete requests sub-page"; + break; + case 3: + message = "Adding file to Delete requests log"; + break; + case 4: + message = "Notifying User on Talk page"; + break; + case 5: + message = "Done"; break; } - AlertDialog alert; - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(title); - builder.setMessage(message); - builder.setCancelable(true); - builder.setPositiveButton( - R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) {} - }); - builder.setNeutralButton(R.string.view_browser, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, media.getFilePageTitle().getMobileUri()); - startActivity(context,browserIntent,null); - } - }); - alert = builder.create(); - alert.show(); + + notificationBuilder.setContentTitle("Nominating "+media.getDisplayTitle()+" for deletion") + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(5, values[0], false) + .setOngoing(true); + notificationManager.notify(NOTIFICATION_DELETE, notificationBuilder.build()); + } + + @Override + protected void onPostExecute(Boolean result) { + String message = ""; + String title = "Nominating for Deletion"; + + if (result){ + title += ": Success"; + message = "Successfully nominated " + media.getDisplayTitle() + " deletion."; + } + else { + title += ": Failed"; + message = "Could not request deletion."; + } + + notificationBuilder.setDefaults(DEFAULT_ALL) + .setContentTitle(title) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(0,0,false) + .setOngoing(false) + .setPriority(PRIORITY_HIGH); + notificationManager.notify(NOTIFICATION_DELETE, notificationBuilder.build()); } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 70dcc780e..5acfb218e 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -15,7 +15,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; -import android.view.WindowManager; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; @@ -42,6 +41,7 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.delete.DeleteTask; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.ui.widget.CompatTextView; import timber.log.Timber; @@ -71,6 +71,9 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { @Inject Provider mediaDataExtractorProvider; + @Inject + MediaWikiApi mwApi; + private MediaWikiImageView image; private MediaDetailSpacer spacer; @@ -82,6 +85,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { private TextView license; private TextView coordinates; private TextView uploadedDate; + private TextView seeMore; + private LinearLayout nominatedforDeletion; private LinearLayout categoryContainer; private LinearLayout authorLayout; private Button delete; @@ -142,6 +147,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { license = (TextView) view.findViewById(R.id.mediaDetailLicense); coordinates = (TextView) view.findViewById(R.id.mediaDetailCoordinates); uploadedDate = (TextView) view.findViewById(R.id.mediaDetailuploadeddate); + seeMore = (TextView) view.findViewById(R.id.seeMore); + nominatedforDeletion = (LinearLayout) view.findViewById(R.id.nominatedDeletionBanner); delete = (Button) view.findViewById(R.id.nominateDeletion); categoryContainer = (LinearLayout) view.findViewById(R.id.mediaDetailCategoryContainer); authorLayout = (LinearLayout) view.findViewById(R.id.authorLinearLayout); @@ -247,7 +254,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { if (success) { extractor.fill(media); - setTextFields(media); setOnClickListeners(media); } else { @@ -300,21 +306,24 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { } rebuildCatList(); - delete.setVisibility(View.VISIBLE); + checkDeletion(media); } private void setOnClickListeners(final Media media) { if (licenseLink(media) != null) { license.setOnClickListener(v -> openWebBrowser(licenseLink(media))); - } else { + } else { Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT); toast.show(); - } + } if (media.getCoordinates() != null) { coordinates.setOnClickListener(v -> openMap(media.getCoordinates())); } - if (delete.getVisibility()==View.VISIBLE){ + if (delete.getVisibility() == View.VISIBLE) { + enableDeleteButton(true); + delete.setOnClickListener(v -> { + AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); alert.setMessage("Why should this file be deleted?"); final EditText input = new EditText(getActivity()); @@ -325,6 +334,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { String reason = input.getText().toString(); DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason); deleteTask.execute(); + enableDeleteButton(false); } }); alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @@ -359,6 +369,20 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); }); } + if (nominatedforDeletion.getVisibility() == View.VISIBLE){ + seeMore.setOnClickListener(v -> { + openWebBrowser(media.getFilePageTitle().getMobileUri().toString()); + }); + } + } + + private void enableDeleteButton(boolean visibility) { + delete.setEnabled(visibility); + if(visibility) { + delete.setTextColor(getResources().getColor(R.color.primaryTextColor)); + } else { + delete.setTextColor(getResources().getColor(R.color.deleteButtonLight)); + } } private void rebuildCatList() { @@ -382,7 +406,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { viewIntent.setAction(Intent.ACTION_VIEW); viewIntent.setData(new PageTitle(selectedCategoryTitle).getCanonicalUri()); //check if web browser available - if(viewIntent.resolveActivity(getActivity().getPackageManager()) != null){ + if (viewIntent.resolveActivity(getActivity().getPackageManager()) != null) { startActivity(viewIntent); } else { Toast toast = Toast.makeText(getContext(), getString(R.string.no_web_browser), LENGTH_SHORT); @@ -450,6 +474,16 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { return media.getCoordinates().getPrettyCoordinateString(); } + private void checkDeletion(Media media){ + if (media.getRequestedDeletion()){ + delete.setVisibility(View.GONE); + nominatedforDeletion.setVisibility(View.VISIBLE); + } + else{ + delete.setVisibility(View.VISIBLE); + nominatedforDeletion.setVisibility(View.GONE); + } + } private @Nullable String licenseLink(Media media) { diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 32ca96174..78051abd8 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -23,7 +23,6 @@ import org.apache.http.params.CoreProtocolPNames; import org.apache.http.util.EntityUtils; import org.mediawiki.api.ApiResult; import org.mediawiki.api.MWApi; -import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.IOException; @@ -41,16 +40,12 @@ import java.util.concurrent.Callable; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.notification.Notification; +import fr.free.nrw.commons.notification.NotificationUtils; import in.yuvi.http.fluent.Http; import io.reactivex.Observable; import io.reactivex.Single; import timber.log.Timber; -import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN; -import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationFromApiResult; -import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationType; -import static fr.free.nrw.commons.notification.NotificationUtils.isCommonsNotification; - /** * @author Addshore */ @@ -434,33 +429,25 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { .param("notprop", "list") .param("format", "xml") .param("meta", "notifications") - .param("notfilter", "!read") +// .param("meta", "notifications") + .param("notformat", "model") .get() .getNode("/api/query/notifications/list"); } catch (IOException e) { Timber.e("Failed to obtain searchCategories", e); } - if (notificationNode == null) { + if (notificationNode == null + || notificationNode.getDocument() == null + || notificationNode.getDocument().getChildNodes() == null + || notificationNode.getDocument().getChildNodes().getLength() == 0) { return new ArrayList<>(); } - List notifications = new ArrayList<>(); - NodeList childNodes = notificationNode.getDocument().getChildNodes(); - - for (int i = 0; i < childNodes.getLength(); i++) { - Node node = childNodes.item(i); - if (isCommonsNotification(node) - && !getNotificationType(node).equals(UNKNOWN)) { - notifications.add(getNotificationFromApiResult(context, node)); - } - } - - return notifications; + return NotificationUtils.getNotificationsFromList(context, childNodes); } - @Override public boolean existingFile(String fileSha1) throws IOException { return api.action("query") diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java index b58afa82a..7ab427b2d 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java @@ -8,6 +8,7 @@ import android.support.v7.app.AlertDialog; import fr.free.nrw.commons.R; import fr.free.nrw.commons.contributions.ContributionController; +import timber.log.Timber; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; @@ -23,46 +24,25 @@ class DirectUpload { this.controller = controller; } - void initiateCameraUpload() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (ContextCompat.checkSelfPermission(fragment.getActivity(), WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) { - if (fragment.getActivity().shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { - new AlertDialog.Builder(fragment.getActivity()) - .setMessage(fragment.getActivity().getString(R.string.write_storage_permission_rationale)) - .setPositiveButton("OK", (dialog, which) -> { - fragment.getActivity().requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 3); - dialog.dismiss(); - }) - .setNegativeButton("Cancel", null) - .create() - .show(); - } else { - fragment.getActivity().requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 3); - } - } else { - controller.startCameraCapture(); - } - } else { - controller.startCameraCapture(); - } - } - + // These permission requests will be handled by the Fragments. + // Do not use requestCode 1 as it will conflict with NearbyActivity's requestCodes void initiateGalleryUpload() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (ContextCompat.checkSelfPermission(fragment.getActivity(), READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) { - if (fragment.getActivity().shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE)) { + if (fragment.shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE)) { new AlertDialog.Builder(fragment.getActivity()) .setMessage(fragment.getActivity().getString(R.string.read_storage_permission_rationale)) .setPositiveButton("OK", (dialog, which) -> { - fragment.getActivity().requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, 1); + Timber.d("Requesting permissions for read external storage"); + fragment.requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, 4); dialog.dismiss(); }) .setNegativeButton("Cancel", null) .create() .show(); } else { - fragment.getActivity().requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, - 1); + fragment.requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, + 4); } } else { controller.startGalleryPick(); @@ -72,4 +52,28 @@ class DirectUpload { controller.startGalleryPick(); } } + + void initiateCameraUpload() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission(fragment.getActivity(), WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) { + if (fragment.shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { + new AlertDialog.Builder(fragment.getActivity()) + .setMessage(fragment.getActivity().getString(R.string.write_storage_permission_rationale)) + .setPositiveButton("OK", (dialog, which) -> { + fragment.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 5); + dialog.dismiss(); + }) + .setNegativeButton("Cancel", null) + .create() + .show(); + } else { + fragment.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 5); + } + } else { + controller.startCameraCapture(); + } + } else { + controller.startCameraCapture(); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java index ed954ed42..ea88de341 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java @@ -1,13 +1,15 @@ package fr.free.nrw.commons.nearby; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.design.widget.BottomSheetBehavior; - import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AlertDialog; import android.view.Menu; @@ -16,7 +18,6 @@ import android.view.MenuItem; import android.view.View; import android.widget.LinearLayout; import android.widget.ProgressBar; -import android.widget.Toast; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -27,20 +28,18 @@ import javax.inject.Inject; import butterknife.BindView; import butterknife.ButterKnife; - import fr.free.nrw.commons.R; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationUpdateListener; import fr.free.nrw.commons.theme.NavigationBaseActivity; +import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.UriSerializer; - import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; - import timber.log.Timber; @@ -63,17 +62,20 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp @Inject NearbyController nearbyController; - private LatLng curLatLang; + private LatLng curLatLng; private Bundle bundle; private Disposable placesDisposable; private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed private BottomSheetBehavior bottomSheetBehavior; // Behavior for list bottom sheet private BottomSheetBehavior bottomSheetBehaviorForDetails; // Behavior for details bottom sheet - private NearbyMapFragment nearbyMapFragment; + public NearbyMapFragment nearbyMapFragment; private NearbyListFragment nearbyListFragment; private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName(); private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName(); + private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; + private BroadcastReceiver broadcastReceiver; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -128,11 +130,17 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp @Override public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection switch (item.getItemId()) { case R.id.action_display_list: - bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_COLLAPSED || bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_HIDDEN){ + bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + }else if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_EXPANDED){ + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + return true; default: return super.onOptionsItemSelected(item); @@ -157,6 +165,11 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp showLocationPermissionDeniedErrorDialog(); } } + break; + + default: + // This is needed to allow the request codes from the Fragments to be routed appropriately + super.onRequestPermissionsResult(requestCode, permissions, grantResults); } } @@ -254,8 +267,6 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp @Override protected void onStop() { super.onStop(); - locationManager.removeLocationListener(this); - locationManager.unregisterLocationManager(); } @Override @@ -271,6 +282,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp super.onResume(); lockNearbyView = false; checkGps(); + addNetworkBroadcastReceiver(); } @Override @@ -283,9 +295,33 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp // to the retained fragment object to perform its own cleanup. removeMapFragment(); removeListFragment(); + } + unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + locationManager.removeLocationListener(this); + locationManager.unregisterLocationManager(); + } + private void addNetworkBroadcastReceiver() { + IntentFilter intentFilter = new IntentFilter(NETWORK_INTENT_ACTION); + + broadcastReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (NetworkUtils.isInternetConnectionEstablished(NearbyActivity.this)) { + refreshView(LocationServiceManager + .LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + } else { + ViewUtil.showLongToast(NearbyActivity.this, getString(R.string.no_internet)); + } + } + }; + + this.registerReceiver(broadcastReceiver, intentFilter); + } /** @@ -297,15 +333,21 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp if (lockNearbyView) { return; } + + if (!NetworkUtils.isInternetConnectionEstablished(this)) { + hideProgressBar(); + return; + } + locationManager.registerLocationManager(); LatLng lastLocation = locationManager.getLastLocation(); - if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed + if (curLatLng != null && curLatLng.equals(lastLocation)) { //refresh view only if location has changed return; } - curLatLang = lastLocation; + curLatLng = lastLocation; - if (curLatLang == null) { + if (curLatLng == null) { Timber.d("Skipping update of nearby places as location is unavailable"); return; } @@ -314,7 +356,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp .equals(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) { progressBar.setVisibility(View.VISIBLE); placesDisposable = Observable.fromCallable(() -> nearbyController - .loadAttractionsFromLocation(curLatLang)) + .loadAttractionsFromLocation(curLatLng)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::populatePlaces); @@ -323,7 +365,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriSerializer()) .create(); - String gsonCurLatLng = gson.toJson(curLatLang); + String gsonCurLatLng = gson.toJson(curLatLng); bundle.putString("CurLatLng", gsonCurLatLng); updateMapFragment(true); } @@ -336,7 +378,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp .registerTypeAdapter(Uri.class, new UriSerializer()) .create(); String gsonPlaceList = gson.toJson(placeList); - String gsonCurLatLng = gson.toJson(curLatLang); + String gsonCurLatLng = gson.toJson(curLatLng); String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates); if (placeList.size() == 0) { @@ -388,6 +430,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp if (nearbyMapFragment != null) { android.support.v4.app.FragmentManager fm = getSupportFragmentManager(); fm.beginTransaction().remove(nearbyMapFragment).commit(); + nearbyMapFragment = null; } } @@ -399,6 +442,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp if (nearbyListFragment != null) { android.support.v4.app.FragmentManager fm = getSupportFragmentManager(); fm.beginTransaction().remove(nearbyListFragment).commit(); + nearbyListFragment = null; } } @@ -413,34 +457,34 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp NearbyMapFragment nearbyMapFragment = getMapFragment(); - if (nearbyMapFragment != null && curLatLang != null) { + if (nearbyMapFragment != null && curLatLng != null) { hideProgressBar(); // In case it is visible (this happens, not an impossible case) /* * If we are close to nearby places boundaries, we need a significant update to * get new nearby places. Check order is south, north, west, east * */ if (nearbyMapFragment.boundaryCoordinates != null - && (curLatLang.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude() - || curLatLang.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude() - || curLatLang.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude() - || curLatLang.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) { + && (curLatLng.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude() + || curLatLng.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude() + || curLatLng.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude() + || curLatLng.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) { // populate places placesDisposable = Observable.fromCallable(() -> nearbyController - .loadAttractionsFromLocation(curLatLang)) + .loadAttractionsFromLocation(curLatLng)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::populatePlaces); - nearbyMapFragment.setArguments(bundle); + nearbyMapFragment.setBundleForUpdtes(bundle); nearbyMapFragment.updateMapSignificantly(); updateListFragment(); return; } if (isSlightUpdate) { - nearbyMapFragment.setArguments(bundle); + nearbyMapFragment.setBundleForUpdtes(bundle); nearbyMapFragment.updateMapSlightly(); } else { - nearbyMapFragment.setArguments(bundle); + nearbyMapFragment.setBundleForUpdtes(bundle); nearbyMapFragment.updateMapSignificantly(); updateListFragment(); } @@ -454,7 +498,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } private void updateListFragment() { - nearbyListFragment.setArguments(bundle); + nearbyListFragment.setBundleForUpdates(bundle); nearbyListFragment.updateNearbyListSignificantly(); } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index 0ecf09160..015d22135 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -54,41 +54,47 @@ public class NearbyController { } List places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage()); - LatLng[] boundaryCoordinates = {places.get(0).location, // south - places.get(0).location, // north - places.get(0).location, // west - places.get(0).location};// east, init with a random location + if (places.size() > 0) { + LatLng[] boundaryCoordinates = {places.get(0).location, // south + places.get(0).location, // north + places.get(0).location, // west + places.get(0).location};// east, init with a random location - if (curLatLng != null) { - Timber.d("Sorting places by distance..."); - final Map distances = new HashMap<>(); - for (Place place: places) { - distances.put(place, computeDistanceBetween(place.location, curLatLng)); - // Find boundaries with basic find max approach - if (place.location.getLatitude() < boundaryCoordinates[0].getLatitude()) { - boundaryCoordinates[0] = place.location; - } - if (place.location.getLatitude() > boundaryCoordinates[1].getLatitude()) { - boundaryCoordinates[1] = place.location; - } - if (place.location.getLongitude() < boundaryCoordinates[2].getLongitude()) { - boundaryCoordinates[2] = place.location; - } - if (place.location.getLongitude() > boundaryCoordinates[3].getLongitude()) { - boundaryCoordinates[3] = place.location; - } - } - Collections.sort(places, - (lhs, rhs) -> { - double lhsDistance = distances.get(lhs); - double rhsDistance = distances.get(rhs); - return (int) (lhsDistance - rhsDistance); + + if (curLatLng != null) { + Timber.d("Sorting places by distance..."); + final Map distances = new HashMap<>(); + for (Place place : places) { + distances.put(place, computeDistanceBetween(place.location, curLatLng)); + // Find boundaries with basic find max approach + if (place.location.getLatitude() < boundaryCoordinates[0].getLatitude()) { + boundaryCoordinates[0] = place.location; } - ); + if (place.location.getLatitude() > boundaryCoordinates[1].getLatitude()) { + boundaryCoordinates[1] = place.location; + } + if (place.location.getLongitude() < boundaryCoordinates[2].getLongitude()) { + boundaryCoordinates[2] = place.location; + } + if (place.location.getLongitude() > boundaryCoordinates[3].getLongitude()) { + boundaryCoordinates[3] = place.location; + } + } + Collections.sort(places, + (lhs, rhs) -> { + double lhsDistance = distances.get(lhs); + double rhsDistance = distances.get(rhs); + return (int) (lhsDistance - rhsDistance); + } + ); + } + nearbyPlacesInfo.placeList = places; + nearbyPlacesInfo.boundaryCoordinates = boundaryCoordinates; + return nearbyPlacesInfo; + } + else { + return null; } - nearbyPlacesInfo.placeList = places; - nearbyPlacesInfo.boundaryCoordinates = boundaryCoordinates; - return nearbyPlacesInfo; } /** diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java index dcc7f5e24..4a2be8476 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java @@ -33,6 +33,8 @@ import static android.app.Activity.RESULT_OK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; public class NearbyListFragment extends DaggerFragment { + private Bundle bundleForUpdates; // Carry information from activity about changed nearby places and current location + private static final Type LIST_TYPE = new TypeToken>() { }.getType(); private static final Type CUR_LAT_LNG_TYPE = new TypeToken() { @@ -80,8 +82,7 @@ public class NearbyListFragment extends DaggerFragment { } public void updateNearbyListSignificantly() { - Bundle bundle = this.getArguments(); - adapterFactory.updateAdapterData(getPlaceListFromBundle(bundle), + adapterFactory.updateAdapterData(getPlaceListFromBundle(bundleForUpdates), (RVRendererAdapter) recyclerView.getAdapter()); } @@ -106,8 +107,8 @@ public class NearbyListFragment extends DaggerFragment { Timber.d("onRequestPermissionsResult: req code = " + " perm = " + permissions + " grant =" + grantResults); switch (requestCode) { - // 1 = "Read external storage" allowed when gallery selected - case 1: { + // 4 = "Read external storage" allowed when gallery selected + case 4: { if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) { Timber.d("Call controller.startGalleryPick()"); controller.startGalleryPick(); @@ -115,8 +116,8 @@ public class NearbyListFragment extends DaggerFragment { } break; - // 3 = "Write external storage" allowed when camera selected - case 3: { + // 5 = "Write external storage" allowed when camera selected + case 5: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Timber.d("Call controller.startCameraCapture()"); controller.startCameraCapture(); @@ -140,4 +141,8 @@ public class NearbyListFragment extends DaggerFragment { } } + public void setBundleForUpdates(Bundle bundleForUpdates) { + this.bundleForUpdates = bundleForUpdates; + } + } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java index 7d0cda450..b84e04a57 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -53,8 +53,10 @@ import javax.inject.Named; import dagger.android.support.DaggerFragment; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.utils.UriDeserializer; +import fr.free.nrw.commons.utils.ViewUtil; import timber.log.Timber; import static android.app.Activity.RESULT_OK; @@ -97,6 +99,7 @@ public class NearbyMapFragment extends DaggerFragment { private Animation fab_open; private Animation rotate_forward; private ContributionController controller; + private DirectUpload directUpload; private Place place; private Marker selected; @@ -105,7 +108,10 @@ public class NearbyMapFragment extends DaggerFragment { private PolygonOptions currentLocationPolygonOptions; private boolean isBottomListSheetExpanded; - private final double CAMERA_TARGET_SHIFT_FACTOR = 0.06; + private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.06; + private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04; + + private Bundle bundleForUpdtes;// Carry information from activity about changed nearby places and current location @Inject @Named("prefs") @@ -120,6 +126,10 @@ public class NearbyMapFragment extends DaggerFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + controller = new ContributionController(this); + directUpload = new DirectUpload(this, controller); + Bundle bundle = this.getArguments(); Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriDeserializer()) @@ -184,14 +194,12 @@ public class NearbyMapFragment extends DaggerFragment { } public void updateMapSlightly() { - // Get arguments from bundle for new location - Bundle bundle = this.getArguments(); if (mapboxMap != null) { Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriDeserializer()) .create(); - if (bundle != null) { - String gsonLatLng = bundle.getString("CurLatLng"); + if (bundleForUpdtes != null) { + String gsonLatLng = bundleForUpdtes.getString("CurLatLng"); Type curLatLngType = new TypeToken() {}.getType(); curLatLng = gson.fromJson(gsonLatLng, curLatLngType); } @@ -201,17 +209,15 @@ public class NearbyMapFragment extends DaggerFragment { } public void updateMapSignificantly() { - - Bundle bundle = this.getArguments(); if (mapboxMap != null) { - if (bundle != null) { + if (bundleForUpdtes != null) { Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriDeserializer()) .create(); - String gsonPlaceList = bundle.getString("PlaceList"); - String gsonLatLng = bundle.getString("CurLatLng"); - String gsonBoundaryCoordinates = bundle.getString("BoundaryCoord"); + String gsonPlaceList = bundleForUpdtes.getString("PlaceList"); + String gsonLatLng = bundleForUpdtes.getString("CurLatLng"); + String gsonBoundaryCoordinates = bundleForUpdtes.getString("BoundaryCoord"); Type listType = new TypeToken>() {}.getType(); List placeList = gson.fromJson(gsonPlaceList, listType); Type curLatLngType = new TypeToken() {}.getType(); @@ -253,13 +259,28 @@ public class NearbyMapFragment extends DaggerFragment { } // Make camera to follow user on location change - CameraPosition position = new CameraPosition.Builder() - .target(isBottomListSheetExpanded ? - new LatLng(curMapBoxLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR, - curMapBoxLatLng.getLongitude()) - : curMapBoxLatLng ) // Sets the new camera position - .zoom(mapboxMap.getCameraPosition().zoom) // Same zoom level - .build(); + CameraPosition position ; + if(ViewUtil.isPortrait(getActivity())){ + position = new CameraPosition.Builder() + .target(isBottomListSheetExpanded ? + new LatLng(curMapBoxLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT, + curMapBoxLatLng.getLongitude()) + : curMapBoxLatLng ) // Sets the new camera position + .zoom(isBottomListSheetExpanded ? + 11 // zoom level is fixed to 11 when bottom sheet is expanded + :mapboxMap.getCameraPosition().zoom) // Same zoom level + .build(); + }else { + position = new CameraPosition.Builder() + .target(isBottomListSheetExpanded ? + new LatLng(curMapBoxLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE, + curMapBoxLatLng.getLongitude()) + : curMapBoxLatLng ) // Sets the new camera position + .zoom(isBottomListSheetExpanded ? + 11 // zoom level is fixed to 11 when bottom sheet is expanded + :mapboxMap.getCameraPosition().zoom) // Same zoom level + .build(); + } mapboxMap.animateCamera(CameraUpdateFactory .newCameraPosition(position), 1000); @@ -273,12 +294,21 @@ public class NearbyMapFragment extends DaggerFragment { if (mapboxMap != null && curLatLng != null) { if (isBottomListSheetExpanded) { // Make camera to follow user on location change - position = new CameraPosition.Builder() - .target(new LatLng(curLatLng.getLatitude() - CAMERA_TARGET_SHIFT_FACTOR, - curLatLng.getLongitude())) // Sets the new camera target above - // current to make it visible when sheet is expanded - .zoom(11) // Same zoom level - .build(); + if(ViewUtil.isPortrait(getActivity())) { + position = new CameraPosition.Builder() + .target(new LatLng(curLatLng.getLatitude() - CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT, + curLatLng.getLongitude())) // Sets the new camera target above + // current to make it visible when sheet is expanded + .zoom(11) // Fixed zoom level + .build(); + } else { + position = new CameraPosition.Builder() + .target(new LatLng(curLatLng.getLatitude() - CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE, + curLatLng.getLongitude())) // Sets the new camera target above + // current to make it visible when sheet is expanded + .zoom(11) // Fixed zoom level + .build(); + } } else { // Make camera to follow user on location change @@ -344,10 +374,29 @@ public class NearbyMapFragment extends DaggerFragment { fabRecenter.setOnClickListener(view -> { if (curLatLng != null) { mapView.getMapAsync(mapboxMap -> { - CameraPosition position = new CameraPosition.Builder() - .target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude())) // Sets the new camera position - .zoom(11) // Sets the zoom - .build(); // Creates a CameraPosition from the builder + CameraPosition position; + + if(ViewUtil.isPortrait(getActivity())){ + position = new CameraPosition.Builder() + .target(isBottomListSheetExpanded ? + new LatLng(curLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT, + curLatLng.getLongitude()) + : new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude(), 0)) // Sets the new camera position + .zoom(isBottomListSheetExpanded ? + 11 // zoom level is fixed to 11 when bottom sheet is expanded + :mapboxMap.getCameraPosition().zoom) // Same zoom level + .build(); + }else { + position = new CameraPosition.Builder() + .target(isBottomListSheetExpanded ? + new LatLng(curLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE, + curLatLng.getLongitude()) + : new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude(), 0)) // Sets the new camera position + .zoom(isBottomListSheetExpanded ? + 11 // zoom level is fixed to 11 when bottom sheet is expanded + :mapboxMap.getCameraPosition().zoom) // Same zoom level + .build(); + } mapboxMap.animateCamera(CameraUpdateFactory .newCameraPosition(position), 1000); @@ -394,11 +443,9 @@ public class NearbyMapFragment extends DaggerFragment { } }); - // Remove texts if it doesnt fit - if (wikipediaButtonText.getLineCount() > 1 - || wikidataButtonText.getLineCount() > 1 - || commonsButtonText.getLineCount() > 1 - || directionsButtonText.getLineCount() > 1) { + // Remove button text if they exceed 1 line or if internal layout has not been built + // Only need to check for directions button because it is the longest + if (directionsButtonText.getLineCount() > 1 || directionsButtonText.getLineCount() == 0) { wikipediaButtonText.setVisibility(View.GONE); wikidataButtonText.setVisibility(View.GONE); commonsButtonText.setVisibility(View.GONE); @@ -408,6 +455,8 @@ public class NearbyMapFragment extends DaggerFragment { private void setupMapView(Bundle savedInstanceState) { MapboxMapOptions options = new MapboxMapOptions() + .compassGravity(Gravity.BOTTOM | Gravity.LEFT) + .compassMargins(new int[]{12, 0, 0, 24}) .styleUrl(Style.OUTDOORS) .logoEnabled(false) .attributionEnabled(false) @@ -536,7 +585,9 @@ public class NearbyMapFragment extends DaggerFragment { transparentView.setAlpha(0); closeFabs(isFabOpen); hideFAB(); - this.getView().requestFocus(); + if (this.getView() != null) { + this.getView().requestFocus(); + } break; } } @@ -583,7 +634,7 @@ public class NearbyMapFragment extends DaggerFragment { /* - * Add amnchors back before making them visible again. + * Add anchors back before making them visible again. * */ private void addAnchorToBigFABs(FloatingActionButton floatingActionButton, int anchorID) { CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams @@ -594,7 +645,7 @@ public class NearbyMapFragment extends DaggerFragment { } /* - * Add amnchors back before making them visible again. Big and small fabs have different anchor + * Add anchors back before making them visible again. Big and small fabs have different anchor * gravities, therefore the are two methods. * */ private void addAnchorToSmallFABs(FloatingActionButton floatingActionButton, int anchorID) { @@ -615,7 +666,7 @@ public class NearbyMapFragment extends DaggerFragment { directionsButton.setOnClickListener(view -> { //Open map app at given position - Intent mapIntent = new Intent(Intent.ACTION_VIEW, place.location.getGmmIntentUri()); + Intent mapIntent = new Intent(Intent.ACTION_VIEW, this.place.location.getGmmIntentUri()); if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) { startActivity(mapIntent); } @@ -635,9 +686,6 @@ public class NearbyMapFragment extends DaggerFragment { fabCamera.setOnClickListener(view -> { if (fabCamera.isShown()) { Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); - controller = new ContributionController(this); - - DirectUpload directUpload = new DirectUpload(this, controller); storeSharedPrefs(); directUpload.initiateCameraUpload(); } @@ -646,14 +694,8 @@ public class NearbyMapFragment extends DaggerFragment { fabGallery.setOnClickListener(view -> { if (fabGallery.isShown()) { Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); - controller = new ContributionController(this); - - DirectUpload directUpload = new DirectUpload(this, controller); storeSharedPrefs(); directUpload.initiateGalleryUpload(); - - //TODO: App crashes after image upload completes - //TODO: Handle onRequestPermissionsResult } }); } @@ -670,9 +712,10 @@ public class NearbyMapFragment extends DaggerFragment { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Timber.d("onRequestPermissionsResult: req code = " + " perm = " + permissions + " grant =" + grantResults); + // Do not use requestCode 1 as it will conflict with NearbyActivity's requestCodes switch (requestCode) { - // 1 = "Read external storage" allowed when gallery selected - case 1: { + // 4 = "Read external storage" allowed when gallery selected + case 4: { if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) { Timber.d("Call controller.startGalleryPick()"); controller.startGalleryPick(); @@ -680,8 +723,8 @@ public class NearbyMapFragment extends DaggerFragment { } break; - // 3 = "Write external storage" allowed when camera selected - case 3: { + // 5 = "Write external storage" allowed when camera selected + case 5: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Timber.d("Call controller.startCameraCapture()"); controller.startCameraCapture(); @@ -705,8 +748,7 @@ public class NearbyMapFragment extends DaggerFragment { } private void openWebView(Uri link) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, link); - startActivity(browserIntent); + Utils.handleWebUrl(getContext(), link); } private void animateFAB(boolean isFabOpen) { @@ -729,7 +771,7 @@ public class NearbyMapFragment extends DaggerFragment { } } - private void closeFabs ( boolean isFabOpen){ + private void closeFabs ( boolean isFabOpen){ if (isFabOpen) { fabPlus.startAnimation(rotate_backward); fabCamera.startAnimation(fab_close); @@ -740,6 +782,11 @@ public class NearbyMapFragment extends DaggerFragment { } } + public void setBundleForUpdtes(Bundle bundleForUpdtes) { + this.bundleForUpdtes = bundleForUpdtes; + } + + @Override public void onStart() { if (mapView != null) { @@ -781,6 +828,9 @@ public class NearbyMapFragment extends DaggerFragment { if (mapView != null) { mapView.onDestroy(); } + selected = null; + currentLocationMarker = null; + super.onDestroyView(); } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java index 5216dc36d..9cbe28db4 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java @@ -27,6 +27,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.di.ApplicationlessInjection; import timber.log.Timber; @@ -200,8 +201,7 @@ public class PlaceRenderer extends Renderer { } private void openWebView(Uri link) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, link); - view.getContext().startActivity(browserIntent); + Utils.handleWebUrl(getContext(), link); } private boolean showMenu() { diff --git a/app/src/main/java/fr/free/nrw/commons/notification/Notification.java b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java index e4efbff6c..e6d759f66 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/Notification.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java @@ -10,12 +10,14 @@ public class Notification { public String date; public String description; public String link; + public String iconUrl; - public Notification(NotificationType notificationType, String notificationText, String date, String description, String link) { + public Notification(NotificationType notificationType, String notificationText, String date, String description, String link, String iconUrl) { this.notificationType = notificationType; this.notificationText = notificationText; this.date = date; this.description = description; this.link = link; + this.iconUrl = iconUrl; } } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java index d6fcd2685..dc52f198a 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -6,13 +6,18 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.support.design.widget.Snackbar; import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import android.widget.Toast; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; import com.pedrogomez.renderers.RVRendererAdapter; +import java.lang.ref.WeakReference; +import java.util.Collections; import java.util.List; import javax.inject.Inject; @@ -20,14 +25,15 @@ import javax.inject.Inject; import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.theme.NavigationBaseActivity; +import fr.free.nrw.commons.utils.NetworkUtils; +import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; -import static android.widget.Toast.LENGTH_SHORT; - /** * Created by root on 18.12.2017. */ @@ -36,6 +42,8 @@ public class NotificationActivity extends NavigationBaseActivity { NotificationAdapterFactory notificationAdapterFactory; @BindView(R.id.listView) RecyclerView recyclerView; + @BindView(R.id.progressBar) ProgressBar progressBar; + @BindView(R.id.container) RelativeLayout relativeLayout; @Inject NotificationController controller; @@ -57,22 +65,44 @@ public class NotificationActivity extends NavigationBaseActivity { recyclerView.setLayoutManager(new LinearLayoutManager(this)); DividerItemDecoration itemDecor = new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL); recyclerView.addItemDecoration(itemDecor); - addNotifications(); + refresh(); } + private void refresh() { + if (!NetworkUtils.isInternetConnectionEstablished(this)) { + progressBar.setVisibility(View.GONE); + Snackbar.make(relativeLayout , R.string.no_internet, Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.retry, view -> { + refresh(); + }).show(); + }else { + progressBar.setVisibility(View.VISIBLE); + addNotifications(); + } + } + + @SuppressLint("CheckResult") private void addNotifications() { Timber.d("Add notifications"); if(mNotificationWorkerFragment == null){ - Observable.fromCallable(() -> controller.getNotifications()) + Observable.fromCallable(() -> { + progressBar.setVisibility(View.VISIBLE); + return controller.getNotifications(); + }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(notificationList -> { + Collections.reverse(notificationList); Timber.d("Number of notifications is %d", notificationList.size()); - initializeAndSetNotificationList(notificationList); setAdapter(notificationList); - }, throwable -> Timber.e(throwable, "Error occurred while loading notifications")); + progressBar.setVisibility(View.GONE); + }, throwable -> { + Timber.e(throwable, "Error occurred while loading notifications"); + ViewUtil.showSnackbar(relativeLayout, R.string.error_notifications); + progressBar.setVisibility(View.GONE); + }); } else { setAdapter(mNotificationWorkerFragment.getNotificationList()); } @@ -82,17 +112,14 @@ public class NotificationActivity extends NavigationBaseActivity { if (url == null || url.equals("")) { return; } - Intent browser = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - //check if web browser available - if(browser.resolveActivity(this.getPackageManager()) != null){ - startActivity(browser); - } else { - Toast toast = Toast.makeText(this, getString(R.string.no_web_browser), LENGTH_SHORT); - toast.show(); - } + Utils.handleWebUrl(this, Uri.parse(url)); } private void setAdapter(List notificationList) { + if(notificationList == null || notificationList.isEmpty()) { + ViewUtil.showSnackbar(relativeLayout, R.string.no_notifications); + return; + } notificationAdapterFactory = new NotificationAdapterFactory(notification -> { Timber.d("Notification clicked %s", notification.link); handleUrl(notification.link); @@ -114,4 +141,4 @@ public class NotificationActivity extends NavigationBaseActivity { .commit(); mNotificationWorkerFragment.setNotificationList(notificationList); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java index a02b9eff4..17a318e74 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java @@ -20,7 +20,6 @@ import fr.free.nrw.commons.R; public class NotificationRenderer extends Renderer { @BindView(R.id.title) ReadMoreTextView title; - @BindView(R.id.description) ReadMoreTextView description; @BindView(R.id.time) TextView time; @BindView(R.id.icon) ImageView icon; private NotificationClicked listener; @@ -48,13 +47,10 @@ public class NotificationRenderer extends Renderer { @Override public void render() { Notification notification = getContent(); - StringBuilder str = new StringBuilder(notification.notificationText); - str.append(" " ); + String str = notification.notificationText.trim(); + str = str.concat(" "); title.setText(str); time.setText(notification.date); - StringBuilder desc = new StringBuilder(notification.description); - desc.append(" "); - description.setText(desc); switch (notification.notificationType) { case THANK_YOU_EDIT: icon.setImageResource(R.drawable.ic_edit_black_24dp); diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java index 7f32da126..68c3add1c 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java @@ -1,16 +1,24 @@ package fr.free.nrw.commons.notification; +import android.annotation.SuppressLint; import android.content.Context; +import android.support.annotation.NonNull; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import java.util.ArrayList; +import java.util.List; + import javax.annotation.Nullable; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.R; +import static fr.free.nrw.commons.notification.NotificationType.THANK_YOU_EDIT; +import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN; + public class NotificationUtils { private static final String COMMONS_WIKI = "commonswiki"; @@ -29,27 +37,124 @@ public class NotificationUtils { return NotificationType.handledValueOf(type); } + public static List getNotificationsFromBundle(Context context, Node document) { + Element bundledNotifications = getBundledNotifications(document); + NodeList childNodes = bundledNotifications.getChildNodes(); + + List notifications = new ArrayList<>(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node node = childNodes.item(i); + if (isUsefulNotification(node)) { + notifications.add(getNotificationFromApiResult(context, node)); + } + } + return notifications; + } + + @NonNull + public static List getNotificationsFromList(Context context, NodeList childNodes) { + List notifications = new ArrayList<>(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node node = childNodes.item(i); + if (isUsefulNotification(node)) { + if (isBundledNotification(node)) { + notifications.addAll(getNotificationsFromBundle(context, node)); + } else { + notifications.add(getNotificationFromApiResult(context, node)); + } + } + } + + return notifications; + } + + private static boolean isUsefulNotification(Node node) { + return isCommonsNotification(node) + && !getNotificationType(node).equals(UNKNOWN) + && !getNotificationType(node).equals(THANK_YOU_EDIT); + } + + public static boolean isBundledNotification(Node document) { + Element bundleElement = getBundledNotifications(document); + if (bundleElement == null) { + return false; + } + + return bundleElement.getChildNodes().getLength() > 0; + } + + private static Element getBundledNotifications(Node document) { + return (Element) getNode(document, "bundledNotifications"); + } + public static Notification getNotificationFromApiResult(Context context, Node document) { NotificationType type = getNotificationType(document); String notificationText = ""; - String link = getNotificationLink(document); + String link = getPrimaryLink(document); String description = getNotificationDescription(document); + String iconUrl = getNotificationIconUrl(document); + switch (type) { case THANK_YOU_EDIT: notificationText = context.getString(R.string.notifications_thank_you_edit); break; case EDIT_USER_TALK: - notificationText = getUserTalkMessage(context, document); + notificationText = getNotificationText(document); break; case MENTION: notificationText = getMentionMessage(context, document); + description = getMentionDescription(document); break; case WELCOME: notificationText = getWelcomeMessage(context, document); break; } - return new Notification(type, notificationText, getTimestamp(document), description, link); + return new Notification(type, notificationText, getTimestamp(document), description, link, iconUrl); + } + + private static String getNotificationText(Node document) { + String notificationBody = getNotificationBody(document); + if (notificationBody == null || notificationBody.trim().equals("")) { + return getNotificationHeader(document); + } + return notificationBody; + } + + private static String getNotificationHeader(Node document) { + Node body = getNode(getModel(document), "header"); + if (body != null) { + String textContent = body.getTextContent(); + return textContent.replace("", "").replace("", ""); + } else { + return ""; + } + } + + private static String getNotificationBody(Node document) { + Node body = getNode(getModel(document), "body"); + if (body != null) { + String textContent = body.getTextContent(); + return textContent.replace("", "").replace("", ""); + } else { + return ""; + } + } + + private static String getMentionDescription(Node document) { + Node body = getNode(getModel(document), "body"); + return body != null ? body.getTextContent() : ""; + } + + private static String getNotificationIconUrl(Node document) { + String format = "%s%s"; + Node iconUrl = getNode(getModel(document), "iconUrl"); + if(iconUrl == null) { + return null; + } else { + String url = iconUrl.getTextContent(); + return String.format(format, BuildConfig.COMMONS_URL, url); + } } public static String getMentionMessage(Context context, Node document) { @@ -57,16 +162,31 @@ public class NotificationUtils { return String.format(format, getAgent(document), getNotificationDescription(document)); } + @SuppressLint("StringFormatMatches") public static String getUserTalkMessage(Context context, Node document) { String format = context.getString(R.string.notifications_talk_page_message); return String.format(format, getAgent(document)); } + @SuppressLint("StringFormatInvalid") public static String getWelcomeMessage(Context context, Node document) { String welcomeMessageFormat = context.getString(R.string.notifications_welcome); return String.format(welcomeMessageFormat, getAgent(document)); } + private static String getPrimaryLink(Node document) { + Node links = getNode(getModel(document), "links"); + Element primaryLink = (Element) getNode(links, "primary"); + if (primaryLink != null) { + return primaryLink.getAttribute("url"); + } + return ""; + } + + private static Node getModel(Node document) { + return getNode(document, "_.2A."); + } + private static String getAgent(Node document) { Element agentElement = (Element) getNode(document, "agent"); if (agentElement != null) { @@ -83,16 +203,6 @@ public class NotificationUtils { return ""; } - private static String getNotificationLink(Node document) { - String format = "%s%s"; - Element titleElement = (Element) getNode(document, "title"); - if (titleElement != null) { - String fullName = titleElement.getAttribute("full"); - return String.format(format, BuildConfig.HOME_URL, fullName); - } - return ""; - } - private static String getNotificationDescription(Node document) { Element titleElement = (Element) getNode(document, "title"); if (titleElement != null) { diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 741905e30..037f0d792 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -151,8 +151,9 @@ public class SettingsFragment extends PreferenceFragment { emailSelectorIntent.setData(Uri.parse("mailto:")); //initialize the emailIntent final Intent emailIntent = new Intent(Intent.ACTION_SEND); - emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{CommonsApplication.FEEDBACK_EMAIL}); - emailIntent.putExtra(Intent.EXTRA_SUBJECT, String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT, BuildConfig.VERSION_NAME)); + // Logs must be sent to the PRIVATE email. Please do not modify this without good reason! + emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{CommonsApplication.LOGS_PRIVATE_EMAIL}); + emailIntent.putExtra(Intent.EXTRA_SUBJECT, String.format(CommonsApplication.LOGS_PRIVATE_EMAIL_SUBJECT, BuildConfig.VERSION_NAME)); emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); emailIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); emailIntent.setSelector( emailSelectorIntent ); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java index b383601ec..ab9fa5602 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java @@ -1,12 +1,18 @@ package fr.free.nrw.commons.upload; +import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.graphics.BitmapRegionDecoder; import android.net.Uri; import android.os.AsyncTask; +import android.support.v7.app.AlertDialog; import java.io.IOException; +import java.lang.ref.WeakReference; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.utils.ImageUtils; import timber.log.Timber; @@ -21,16 +27,13 @@ import timber.log.Timber; public class DetectUnwantedPicturesAsync extends AsyncTask { - interface Callback { - void onResult(ImageUtils.Result result); - } - - private final Callback callback; private final String imageMediaFilePath; + public final WeakReference activityWeakReference; - DetectUnwantedPicturesAsync(String imageMediaFilePath, Callback callback) { - this.callback = callback; + DetectUnwantedPicturesAsync(WeakReference activityWeakReference, String imageMediaFilePath) { + //this.callback = callback; this.imageMediaFilePath = imageMediaFilePath; + this.activityWeakReference = activityWeakReference; } @Override @@ -53,7 +56,29 @@ public class DetectUnwantedPicturesAsync extends AsyncTask { + //user does not wish to upload the picture, take them back to ContributionsActivity + Intent intent = new Intent(activity, ContributionsActivity.class); + dialogInterface.dismiss(); + activity.startActivity(intent); + }); + errorDialogBuilder.setNegativeButton(activity.getString(R.string.yes), (dialogInterface, i) -> { + //user wishes to go ahead with the upload of this picture, just dismiss this dialog + dialogInterface.dismiss(); + }); + + AlertDialog errorDialog = errorDialogBuilder.create(); + if (!activity.isFinishing()) { + errorDialog.show(); + } + } } } 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 f5e1820b8..aca17601c 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 @@ -415,31 +415,9 @@ public class ShareActivity private void performUnwantedPictureDetectionProcess() { String imageMediaFilePath = FileUtils.getPath(this,mediaUri); - DetectUnwantedPicturesAsync detectUnwantedPicturesAsync = new DetectUnwantedPicturesAsync(imageMediaFilePath, result -> { - - if (result != ImageUtils.Result.IMAGE_OK) { - //show appropriate error message - String errorMessage = result == ImageUtils.Result.IMAGE_DARK ? getString(R.string.upload_image_too_dark) : getString(R.string.upload_image_blurry); - AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(this); - errorDialogBuilder.setMessage(errorMessage); - errorDialogBuilder.setTitle(getString(R.string.warning)); - errorDialogBuilder.setPositiveButton(getString(R.string.no), (dialogInterface, i) -> { - //user does not wish to upload the picture, take them back to ContributionsActivity - Intent intent = new Intent(ShareActivity.this, ContributionsActivity.class); - dialogInterface.dismiss(); - startActivity(intent); - }); - errorDialogBuilder.setNegativeButton(getString(R.string.yes), (dialogInterface, i) -> { - //user wishes to go ahead with the upload of this picture, just dismiss this dialog - dialogInterface.dismiss(); - }); - - AlertDialog errorDialog = errorDialogBuilder.create(); - if (!isFinishing()) { - errorDialog.show(); - } - } - }); + DetectUnwantedPicturesAsync detectUnwantedPicturesAsync + = new DetectUnwantedPicturesAsync(new WeakReference(this) + , imageMediaFilePath); detectUnwantedPicturesAsync.execute(); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java index 0fa98e530..a88d03c03 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java @@ -14,7 +14,9 @@ import android.support.annotation.NonNull; import android.support.v4.view.ViewCompat; import android.support.v7.app.AlertDialog; import android.text.Editable; +import android.text.Html; import android.text.TextWatcher; +import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -225,18 +227,6 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { .commit(); } - @OnTouch(R.id.share_license_summary) - boolean showLicence(View view, MotionEvent motionEvent) { - if (motionEvent.getActionMasked() == ACTION_DOWN) { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(licenseUrlFor(license))); - startActivity(intent); - return true; - } else { - return false; - } - } @OnClick(R.id.titleDescButton) void setTitleDescButton() { @@ -294,8 +284,10 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { @SuppressLint("StringFormatInvalid") private void setLicenseSummary(String license) { - licenseSummaryView.setText(getString(R.string.share_license_summary, getString(Utils.licenseNameFor(license)))); - } + String licenseHyperLink = ""+ getString(Utils.licenseNameFor(license)) + "
"; + licenseSummaryView.setMovementMethod(LinkMovementMethod.getInstance()); + licenseSummaryView.setText(Html.fromHtml(getString(R.string.share_license_summary, licenseHyperLink))); + } @Override public void onActivityCreated(Bundle savedInstanceState) { diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java new file mode 100644 index 000000000..b9da22e6e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java @@ -0,0 +1,18 @@ +package fr.free.nrw.commons.utils; + + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +public class NetworkUtils { + + public static boolean isInternetConnectionEstablished(Context context) { + ConnectivityManager cm = + (ConnectivityManager)context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); + + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + return activeNetwork != null && + activeNetwork.isConnectedOrConnecting(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java index 82227c59a..b4b26746b 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java @@ -1,7 +1,11 @@ package fr.free.nrw.commons.utils; +import android.app.Activity; +import android.content.Context; import android.support.design.widget.Snackbar; +import android.view.Display; import android.view.View; +import android.widget.Toast; public class ViewUtil { @@ -9,4 +13,18 @@ public class ViewUtil { Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show(); } + public static void showLongToast(Context context, String text) { + Toast.makeText(context, text, + Toast.LENGTH_LONG).show(); + } + + public static boolean isPortrait(Context context) { + Display orientation = ((Activity)context).getWindowManager().getDefaultDisplay(); + if(orientation.getWidth() < orientation.getHeight()){ + return true; + } else { + return false; + } + } + } diff --git a/app/src/main/res/drawable-mdpi/share.png b/app/src/main/res/drawable-mdpi/share.png new file mode 100644 index 000000000..17473572e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/share.png differ diff --git a/app/src/main/res/drawable/bg_delete_button.xml b/app/src/main/res/drawable/bg_delete_button.xml new file mode 100644 index 000000000..199940e1b --- /dev/null +++ b/app/src/main/res/drawable/bg_delete_button.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_share_black_24dp.xml b/app/src/main/res/drawable/ic_share_black_24dp.xml new file mode 100644 index 000000000..01c81322d --- /dev/null +++ b/app/src/main/res/drawable/ic_share_black_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index 9551e6e64..60519f591 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -111,7 +111,7 @@ android:layout_marginTop="@dimen/standard_gap" android:gravity="center" android:textColor="@color/primaryColor" - android:text="@string/about_rate_us" /> + /> + /> + /> + /> - - diff --git a/app/src/main/res/layout/activity_nearby.xml b/app/src/main/res/layout/activity_nearby.xml index 31eceff07..26d6473f3 100644 --- a/app/src/main/res/layout/activity_nearby.xml +++ b/app/src/main/res/layout/activity_nearby.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - @@ -37,33 +38,14 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" /> - - + @@ -88,7 +70,7 @@ - + @@ -15,6 +16,12 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> + + android:layout_gravity="bottom" + android:padding="5dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_details.xml b/app/src/main/res/layout/bottom_sheet_details.xml index 2be0f6cf7..c964fda87 100644 --- a/app/src/main/res/layout/bottom_sheet_details.xml +++ b/app/src/main/res/layout/bottom_sheet_details.xml @@ -37,8 +37,11 @@ android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textSize="16sp" /> - + android:textSize="16sp" + android:layout_marginRight="50dp" + android:maxLines="2" + android:ellipsize="end" + /> - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_media_detail.xml b/app/src/main/res/layout/fragment_media_detail.xml index 6b530af1c..f265bc3ea 100644 --- a/app/src/main/res/layout/fragment_media_detail.xml +++ b/app/src/main/res/layout/fragment_media_detail.xml @@ -284,10 +284,38 @@ android:layout_width="match_parent" android:layout_height="@dimen/small_gap" /> + + + + +