Merge branch 'master' into addedReasonList

This commit is contained in:
Vivek Maskara 2018-11-20 21:16:51 +05:30 committed by GitHub
commit 2bcc8c10b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
248 changed files with 7414 additions and 4619 deletions

View file

@ -2,24 +2,28 @@
Summarize your issue in one sentence (what goes wrong, what did you expect to happen)
_Before creating an issue, please search the existing issues to see if a similar one has already been created. You can search issues by specific labels (e.g. `label:nearby `) or just by typing keywords into the search filter._
_Before creating an issue, please search the existing issues to see if a similar one has already been created. You can search issues by specific labels (e.g. `label:nearby`) or just by typing keywords into the search filter._
**Steps to reproduce:**
How can we reproduce the issue?
What did you expect the app to do, and what did you see instead?
**Add System logs:**
**System logs:**
```
Add logcat files here (if possible).
Need help? See https://github.com/commons-app/apps-android-commons/wiki/Getting-app-logs-from-Android-Studio
```
**Device and Android version:**
What make and model device (e.g., Samsung J7) did you encounter this on? What Android
version (e.g., Android 4.0 Ice Cream Sandwich or Android 6.0 Marshmallow) are you running? Is it
the stock version from the manufacturer or a custom ROM ?
What make and model device (e.g., Samsung J7) did you encounter this on?
What Android version (e.g., Android 4.0 Ice Cream Sandwich or Android 6.0 Marshmallow) are you running?
Is it the stock version from the manufacturer or a custom ROM ?
**Commons app version:**
**Commons app version:**
You can find this information by going to the navigation drawer in the app and tapping 'About'. If you are building from our codebase instead of downloading the app, please also mention the branch and build variant (e.g. master and prodDebug).

View file

@ -1,19 +1,17 @@
## Title (required)
**Description (required)**
Fixes #{GitHub issue number and title (Please do not forget adding title) }
Fixes #{GitHub issue number} {Github issue title}
## Description (required)
What changes did you make and why?
Fixes #{GitHub issue number and title}
**Tests performed (required)**
{Describe the changes made and why they were made.}
Tested {build variant, e.g. ProdDebug} on {name of device or emulator} with API level {API level}.
## Tests performed (required)
**Screenshots showing what changed (optional - for UI changes)**
Tested on {API level & name of device/emulator}, with {build variant, e.g. ProdDebug}.
Need help? See https://support.google.com/android/answer/9075928
## Screenshots showing what changed (optional)
{Only for user interface changes, otherwise remove this section. See [how to take a screenshot](https://android.stackexchange.com/questions/1759/how-to-take-a-screenshot-with-an-android-device)}
---
_Note: Please ensure that you have read CONTRIBUTING.md if this is your first pull request._

View file

@ -1,4 +1,4 @@
# Wikimedia Commons Android app [![Build status](https://api.travis-ci.org/commons-app/apps-android-commons.svg)](https://travis-ci.org/commons-app/apps-android-commons)
# Wikimedia Commons Android app [![Build status](https://api.travis-ci.org/commons-app/apps-android-commons.svg?branch=master)](https://travis-ci.org/commons-app/apps-android-commons)
The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][1], or view our [website][2].
@ -11,37 +11,13 @@ Initially started by the Wikimedia Foundation, this app is now maintained by gra
## Documentation
We try to have an extensive documentation at [our wiki here at Github][5]:
We try to have an extensive documentation at [our wiki here at Github][4]:
* [User Documentation][6]
* [Contributor Documentation][7]
* [Volunteers Welcome!][9]
* [User Documentation][5]
* [Contributor Documentation][6]
* [Volunteers Welcome!][7]
* [Developer Documentation][8]
## Libraries Used ##
* [Picasso][11]
* [RSS-Parser][12]
* [ViewPagerIndicator][13]
* [PhotoView][14]
* [Acra][15]
* [Renderers][16]
* [Gson][17]
* [Timber][18]
* [Java-String-Similarity][19]
* [ReadMoreTextView][20]
* [MaterialShowcaseView][21]
* [Butterknife][22]
* [OKHttp][23]
* [Okio][24]
* [RxJava][25]
* [JSoup][26]
* [Fresco][27]
* [Stetho][28]
* [Dagger][29]
* [Java-HTTP-Fluent][30]
* [CircleProgressBar][31]
* [Leak Canary][32]
* [Libraries Used][9]
## Contributors ##
@ -60,37 +36,18 @@ Thank you all for your work!
## License ##
This software is open source, licensed under the [Apache License 2.0][4].
This software is open source, licensed under the [Apache License 2.0][10].
[1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
[2]: https://commons-app.github.io/
[3]: https://github.com/commons-app/apps-android-commons/issues
[4]: https://www.apache.org/licenses/LICENSE-2.0
[5]: https://github.com/commons-app/apps-android-commons/wiki
[6]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation
[7]: https://github.com/commons-app/apps-android-commons/wiki#contributor-documentation
[4]: https://github.com/commons-app/apps-android-commons/wiki
[5]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation
[6]: https://github.com/commons-app/apps-android-commons/wiki#contributor-documentation
[7]: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21
[8]: https://github.com/commons-app/apps-android-commons/wiki#developer-documentation
[9]: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21
[10]: https://meta.wikimedia.org/wiki/Grants:Project/Improve_%27Upload_to_Commons%27_Android_App/Renewal
[11]: https://github.com/square/picasso
[13]: https://github.com/avianey/Android-ViewPagerIndicator
[14]: https://github.com/chrisbanes/PhotoView
[15]: https://github.com/ACRA/acra
[16]: https://github.com/pedrovgs/Renderers
[17]: https://github.com/google/gson
[18]: https://github.com/JakeWharton/timber
[19]: https://github.com/tdebatty/java-string-similarity
[20]: https://github.com/bravoborja/ReadMoreTextView
[21]: https://github.com/deano2390/MaterialShowcaseView
[22]: https://github.com/JakeWharton/butterknife
[23]: https://github.com/square/okhttp
[24]: https://github.com/square/okio
[25]: https://github.com/ReactiveX/RxJava
[27]: https://github.com/facebook/fresco
[28]: https://github.com/facebook/stetho
[29]: https://github.com/google/dagger
[30]: https://github.com/yuvipanda/java-http-fluent/blob/master/src/main/java/in/yuvi/http/fluent/Http.java
[31]: https://github.com/dinuscxj/CircleProgressBar
[32]: https://github.com/square/leakcanary
[9]: https://github.com/commons-app/apps-android-commons/wiki/Libraries-used
[10]: https://www.apache.org/licenses/LICENSE-2.0

View file

@ -13,7 +13,6 @@ dependencies {
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.5'
@ -32,6 +31,7 @@ dependencies {
transitive = true
}
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
//noinspection GradleCompatible
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
@ -44,6 +44,7 @@ dependencies {
implementation 'com.squareup.okio:okio:1.14.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
// Because RxAndroid releases are few and far between, it is recommended you also
// explicitly depend on RxJava's latest version for bug fixes and new features.
implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
@ -153,8 +154,8 @@ android {
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\""
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\""
buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\""
buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\""
buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\""
buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\""
buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\""
dimension 'tier'
}

View file

@ -1,30 +0,0 @@
package fr.free.nrw.commons.upload;
import android.net.Uri;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import fr.free.nrw.commons.BuildConfig;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
@RunWith(AndroidJUnit4.class)
public class FileUtilsTest {
@Test
public void isSelfOwned() throws Exception {
Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".provider/document/1");
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
assertThat(selfOwned, is(true));
}
@Test
public void isNotSelfOwned() throws Exception {
Uri uri = Uri.parse("content://com.android.providers.media.documents/document/1");
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
assertThat(selfOwned, is(false));
}
}

View file

@ -15,7 +15,7 @@
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
<uses-permission android:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
@ -41,56 +41,44 @@
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<activity android:name=".WelcomeActivity" />
<activity
android:name=".upload.ShareActivity"
<activity android:name=".upload.UploadActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
<intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="audio/ogg" />
</intent-filter>
</activity>
<activity
android:name=".upload.MultipleShareActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
<intent-filter android:label="@string/intent_share_upload_label">
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="audio/ogg" />
</intent-filter>
</activity>
<activity
android:name=".contributions.ContributionsActivity"
android:name=".contributions.MainActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" />
<activity
android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings" />
<activity
android:name=".AboutActivity"
android:label="@string/title_activity_about"
android:parentActivityName=".contributions.ContributionsActivity" />
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".auth.SignupActivity"
android:label="@string/title_activity_signup" />
<activity
android:name=".nearby.NearbyActivity"
android:label="@string/title_activity_nearby"
android:parentActivityName=".contributions.ContributionsActivity" />
<activity
android:name=".notification.NotificationActivity"
android:label="@string/navigation_item_notification" />
@ -104,18 +92,18 @@
<activity
android:name=".category.CategoryImagesActivity"
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.ContributionsActivity" />
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".category.CategoryDetailsActivity"
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.ContributionsActivity" />
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".explore.SearchActivity"
android:label="@string/title_activity_search"
android:configChanges="orientation|keyboardHidden"
android:parentActivityName=".contributions.ContributionsActivity"
android:parentActivityName=".contributions.MainActivity"
/>
<activity
@ -140,24 +128,24 @@
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service
android:name=".contributions.ContributionsSyncService"
android:exported="true">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/contributions_sync_adapter" />
</service>
<service
android:name=".modifications.ModificationsSyncService"
android:exported="true">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/modifications_sync_adapter" />
@ -177,21 +165,18 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name=".contributions.ContributionsContentProvider"
android:authorities="${applicationId}.contributions.contentprovider"
android:exported="false"
android:label="@string/provider_contributions"
android:syncable="true" />
<provider
android:name=".modifications.ModificationsContentProvider"
android:authorities="${applicationId}.modifications.contentprovider"
android:exported="false"
android:label="@string/provider_modifications"
android:syncable="true" />
<provider
android:name=".category.CategoryContentProvider"
android:authorities="${applicationId}.categories.contentprovider"

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
@ -14,12 +15,11 @@ import android.widget.Toast;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -110,6 +110,31 @@ public class Utils {
throw new RuntimeException("Unrecognized license value: " + license);
}
/**
* Generates license url with given ID
* @param license License ID
* @return Url of license
*/
@NonNull
public static String licenseUrlFor(String license) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
return "https://creativecommons.org/licenses/by/3.0/";
case Prefs.Licenses.CC_BY_4:
return "https://creativecommons.org/licenses/by/4.0/";
case Prefs.Licenses.CC_BY_SA_3:
return "https://creativecommons.org/licenses/by-sa/3.0/";
case Prefs.Licenses.CC_BY_SA_4:
return "https://creativecommons.org/licenses/by-sa/4.0/";
case Prefs.Licenses.CC0:
return "https://creativecommons.org/publicdomain/zero/1.0/";
default:
throw new RuntimeException("Unrecognized license value: " + license);
}
}
/**
* Adds extension to filename. Converts to .jpg if system provides .jpeg, adds .jpg if no extension detected
* @param title File name
@ -176,6 +201,18 @@ public class Utils {
customTabsIntent.launchUrl(context, url);
}
public static void handleGeoCoordinates(Context context, String coords) {
try {
Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + coords);
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
mapIntent.setPackage("com.google.android.apps.maps");
context.startActivity(mapIntent);
} catch (ActivityNotFoundException ex) {
Toast toast = Toast.makeText(context, context.getString(R.string.map_application_missing), LENGTH_SHORT);
toast.show();
}
}
/**
* To take screenshot of the screen and return it in Bitmap format
*
@ -190,4 +227,14 @@ public class Utils {
return bitmap;
}
public static <K,V> Map<K,V> arraysToMap(K[] kArray, V[] vArray){
if(kArray.length!=vArray.length)
throw new RuntimeException("arraysToMap array sizes don't match");
Map<K,V> map=new LinkedHashMap<>();
for (int i=0;i<vArray.length;i++){
map.put(kArray[i], vArray[i]);
}
return map;
}
}

View file

@ -33,7 +33,7 @@ public class WelcomeActivity extends BaseActivity {
moreInformation = this.getString(R.string.welcome_help_button_text);
if(getIntent() != null) {
if (getIntent() != null) {
Bundle bundle = getIntent().getExtras();
if (bundle != null) {
isQuiz = bundle.getBoolean("isQuiz");
@ -54,7 +54,7 @@ public class WelcomeActivity extends BaseActivity {
*/
@Override
public void onDestroy() {
if(isQuiz){
if (isQuiz){
Intent i = new Intent(WelcomeActivity.this, QuizActivity.class);
startActivity(i);
}

View file

@ -60,15 +60,15 @@ public class WelcomePagerAdapter extends PagerAdapter {
this.container=container;
LayoutInflater inflater = LayoutInflater.from(container.getContext());
ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false);
if( BuildConfig.FLAVOR == "beta"){
TextView textView = (TextView) layout.findViewById(R.id.welcomeYesButton);
if( textView.getVisibility() != View.VISIBLE){
if (BuildConfig.FLAVOR == "beta") {
TextView textView = layout.findViewById(R.id.welcomeYesButton);
if (textView.getVisibility() != View.VISIBLE) {
textView.setVisibility(View.VISIBLE);
}
ViewHolder holder = new ViewHolder(layout);
layout.setTag(holder);
if(position == PAGE_FINAL){
if (position == PAGE_FINAL){
TextView moreInfo = layout.findViewById(R.id.welcomeInfo);
moreInfo.setText(Html.fromHtml(WelcomeActivity.moreInformation));
ViewHolder holder1 = new ViewHolder(layout);

View file

@ -185,7 +185,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
* which then calls parseJson when results are fetched
*/
private void setAchievements() {
if(checkAccount()) {
if (checkAccount()) {
compositeDisposable.add(mediaWikiApi
.getAchievements(Objects.requireNonNull(sessionManager.getCurrentAccount()).name)
.subscribeOn(Schedulers.io())
@ -218,7 +218,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
* used to the count of images uploaded by user
*/
private void setUploadCount(Achievements achievements) {
if(checkAccount()) {
if (checkAccount()) {
compositeDisposable.add(mediaWikiApi
.getUploadCount(Objects.requireNonNull(sessionManager.getCurrentAccount()).name)
.subscribeOn(Schedulers.io())
@ -338,9 +338,9 @@ public class AchievementsActivity extends NavigationBaseActivity {
AlertDialog.Builder alertadd = new AlertDialog.Builder(AchievementsActivity.this);
LayoutInflater factory = LayoutInflater.from(AchievementsActivity.this);
final View view = factory.inflate(R.layout.image_alert_layout, null);
ImageView screenShotImage = (ImageView) view.findViewById(R.id.alert_image);
ImageView screenShotImage = view.findViewById(R.id.alert_image);
screenShotImage.setImageBitmap(screenshot);
TextView shareMessage = (TextView) view.findViewById(R.id.alert_text);
TextView shareMessage = view.findViewById(R.id.alert_text);
shareMessage.setText(R.string.achievements_share_message);
alertadd.setView(view);
alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot));
@ -387,7 +387,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
*/
private boolean checkAccount(){
Account currentAccount = sessionManager.getCurrentAccount();
if(currentAccount == null) {
if (currentAccount == null) {
Timber.d("Current account is null");
ViewUtil.showLongToast(this, getResources().getString(R.string.user_not_logged_in));
sessionManager.forceLogin(this);

View file

@ -19,7 +19,7 @@ public class BitmapUtils {
*/
public static BitmapDrawable writeOnDrawable(Bitmap bm, String text, Context context){
Bitmap.Config config = bm.getConfig();
if(config == null){
if (config == null){
config = Bitmap.Config.ARGB_8888;
}
Bitmap bitmap = Bitmap.createBitmap(bm.getWidth(),bm.getHeight(),config);

View file

@ -16,7 +16,8 @@ import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
@Inject SessionManager sessionManager;
@Inject
protected SessionManager sessionManager;
@Inject
MediaWikiApi mediaWikiApi;
private String authCookie;

View file

@ -43,7 +43,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
@ -139,7 +139,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
.show());
if(BuildConfig.FLAVOR.equals("beta")){
if (BuildConfig.FLAVOR.equals("beta")){
loginCredentials.setText(getString(R.string.login_credential));
} else {
loginCredentials.setVisibility(View.GONE);
@ -381,10 +381,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
super.onRestoreInstanceState(savedInstanceState);
loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGING_IN, false);
errorMessageShown = savedInstanceState.getBoolean(ERROR_MESSAGE_SHOWN, false);
if(loginCurrentlyInProgress){
if (loginCurrentlyInProgress){
performLogin();
}
if(errorMessageShown){
if (errorMessageShown){
resultantError = savedInstanceState.getString(RESULTANT_ERROR);
handleOtherResults(resultantError);
}
@ -399,7 +399,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
public void showMessageAndCancelDialog(@StringRes int resId) {
showMessage(resId, R.color.secondaryDarkColor);
if(progressDialog != null){
if (progressDialog != null){
progressDialog.cancel();
}
}
@ -415,7 +415,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
public void startMainActivity() {
NavigationBaseActivity.startActivityWithFlags(this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP);
NavigationBaseActivity.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP);
finish();
}

View file

@ -105,7 +105,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
*/
@SuppressLint("CheckResult")
private void initList() {
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
@ -179,7 +179,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
* @param collection List of new Media to be displayed
*/
private void handleSuccess(List<Media> collection) {
if(collection == null) {
if (collection == null) {
initErrorView();
return;
}
@ -188,7 +188,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
return;
}
if(gridAdapter == null) {
if (gridAdapter == null) {
setAdapter(collection);
} else {
if (gridAdapter.containsAll(collection)) {

View file

@ -1,24 +1,23 @@
package fr.free.nrw.commons.category;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
import java.util.Collections;
import java.util.List;
class CategoriesAdapterFactory {
private final CategoriesRenderer.CategoryClickedListener listener;
public class CategoriesAdapterFactory {
private final CategoryClickedListener listener;
CategoriesAdapterFactory(CategoriesRenderer.CategoryClickedListener listener) {
public CategoriesAdapterFactory(CategoryClickedListener listener) {
this.listener = listener;
}
public RVRendererAdapter<CategoryItem> create(List<CategoryItem> placeList) {
public CategoryRendererAdapter create(List<CategoryItem> placeList) {
RendererBuilder<CategoryItem> builder = new RendererBuilder<CategoryItem>()
.bind(CategoryItem.class, new CategoriesRenderer(listener));
ListAdapteeCollection<CategoryItem> collection = new ListAdapteeCollection<>(
placeList != null ? placeList : Collections.<CategoryItem>emptyList());
return new RVRendererAdapter<>(builder, collection);
return new CategoryRendererAdapter(builder, collection);
}
}

View file

@ -0,0 +1,227 @@
package fr.free.nrw.commons.category;
import android.content.SharedPreferences;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.upload.GpsCategoryModel;
import fr.free.nrw.commons.utils.StringSortingUtils;
import io.reactivex.Observable;
import timber.log.Timber;
public class CategoriesModel implements CategoryClickedListener {
private static final int SEARCH_CATS_LIMIT = 25;
private final MediaWikiApi mwApi;
private final CategoryDao categoryDao;
private final SharedPreferences prefs;
private final SharedPreferences directPrefs;
private HashMap<String, ArrayList<String>> categoriesCache;
private List<CategoryItem> selectedCategories;
@Inject GpsCategoryModel gpsCategoryModel;
@Inject
public CategoriesModel(MediaWikiApi mwApi,
CategoryDao categoryDao,
@Named("default_preferences") SharedPreferences prefs,
@Named("direct_nearby_upload_prefs") SharedPreferences directPrefs) {
this.mwApi = mwApi;
this.categoryDao = categoryDao;
this.prefs = prefs;
this.directPrefs = directPrefs;
this.categoriesCache = new HashMap<>();
this.selectedCategories = new ArrayList<>();
}
//region Misc. utility methods
public Comparator<CategoryItem> sortBySimilarity(final String filter) {
Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
return (firstItem, secondItem) -> stringSimilarityComparator
.compare(firstItem.getName(), secondItem.getName());
}
public boolean containsYear(String item) {
//Check for current and previous year to exclude these categories from removal
Calendar now = Calendar.getInstance();
int year = now.get(Calendar.YEAR);
String yearInString = String.valueOf(year);
int prevYear = year - 1;
String prevYearInString = String.valueOf(prevYear);
Timber.d("Previous year: %s", prevYearInString);
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
//And that item does not equal the current year or previous year
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
//Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")
|| (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
}
public void updateCategoryCount(CategoryItem item) {
Category category = categoryDao.find(item.getName());
// Newly used category...
if (category == null) {
category = new Category(null, item.getName(), new Date(), 0);
}
category.incTimesUsed();
categoryDao.save(category);
}
//endregion
//region Category Caching
public void cacheAll(HashMap<String, ArrayList<String>> categories) {
categoriesCache.putAll(categories);
}
public HashMap<String, ArrayList<String>> getCategoriesCache() {
return categoriesCache;
}
boolean cacheContainsKey(String term) {
return categoriesCache.containsKey(term);
}
//endregion
//region Category searching
public Observable<CategoryItem> searchAll(String term, List<String> imageTitleList) {
//If user hasn't typed anything in yet, get GPS and recent items
if (TextUtils.isEmpty(term)) {
return gpsCategories()
.concatWith(titleCategories(imageTitleList))
.concatWith(recentCategories());
}
//if user types in something that is in cache, return cached category
if (cacheContainsKey(term)) {
return Observable.fromIterable(getCachedCategories(term))
.map(name -> new CategoryItem(name, false));
}
//otherwise, search API for matching categories
return mwApi
.allCategories(term, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
public Observable<CategoryItem> searchCategories(String term, List<String> imageTitleList) {
//If user hasn't typed anything in yet, get GPS and recent items
if (TextUtils.isEmpty(term)) {
return gpsCategories()
.concatWith(titleCategories(imageTitleList))
.concatWith(recentCategories());
}
return mwApi
.searchCategories(term, SEARCH_CATS_LIMIT)
.map(s -> new CategoryItem(s, false));
}
private ArrayList<String> getCachedCategories(String term) {
return categoriesCache.get(term);
}
public Observable<CategoryItem> defaultCategories(List<String> titleList) {
Observable<CategoryItem> directCat = directCategories();
if (hasDirectCategories()) {
Timber.d("Image has direct Cat");
return directCat
.concatWith(gpsCategories())
.concatWith(titleCategories(titleList))
.concatWith(recentCategories());
} else {
Timber.d("Image has no direct Cat");
return gpsCategories()
.concatWith(titleCategories(titleList))
.concatWith(recentCategories());
}
}
private boolean hasDirectCategories() {
return !directPrefs.getString("Category", "").equals("");
}
private Observable<CategoryItem> directCategories() {
String directCategory = directPrefs.getString("Category", "");
List<String> categoryList = new ArrayList<>();
Timber.d("Direct category found: " + directCategory);
if (!directCategory.equals("")) {
categoryList.add(directCategory);
Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList);
}
return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false));
}
Observable<CategoryItem> gpsCategories() {
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
.map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> titleCategories(List<String> titleList) {
return Observable.fromIterable(titleList)
.concatMap(this::getTitleCategories);
}
private Observable<CategoryItem> getTitleCategories(String title) {
return mwApi.searchTitles(title, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> recentCategories() {
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
.map(s -> new CategoryItem(s, false));
}
//endregion
//region Category Selection
@Override
public void categoryClicked(CategoryItem item) {
if (item.isSelected()) {
selectCategory(item);
updateCategoryCount(item);
} else {
unselectCategory(item);
}
}
public void selectCategory(CategoryItem item) {
selectedCategories.add(item);
}
public void unselectCategory(CategoryItem item) {
selectedCategories.remove(item);
}
public int selectedCategoriesCount() {
return selectedCategories.size();
}
public List<CategoryItem> getSelectedCategories() {
return selectedCategories;
}
public List<String> getCategoryStringList() {
List<String> output = new ArrayList<>();
for (CategoryItem item : selectedCategories) {
output.add(item.getName());
}
return output;
}
//endregion
}

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.category;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -11,7 +12,7 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
class CategoriesRenderer extends Renderer<CategoryItem> {
public class CategoriesRenderer extends Renderer<CategoryItem> {
@BindView(R.id.tvName) CheckedTextView checkedView;
private final CategoryClickedListener listener;
@ -44,11 +45,8 @@ class CategoriesRenderer extends Renderer<CategoryItem> {
@Override
public void render() {
CategoryItem item = getContent();
Log.e("Commons", "Rendering: "+item);
checkedView.setChecked(item.isSelected());
checkedView.setText(item.getName());
}
interface CategoryClickedListener {
void categoryClicked(CategoryItem item);
}
}

View file

@ -1,421 +0,0 @@
package fr.free.nrw.commons.category;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxTextView;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.upload.GpsCategoryModel;
import fr.free.nrw.commons.utils.StringSortingUtils;
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.view.KeyEvent.ACTION_UP;
import static android.view.KeyEvent.KEYCODE_BACK;
/**
* Displays the category suggestion and selection screen. Category search is initiated here.
*/
public class CategorizationFragment extends CommonsDaggerSupportFragment {
public static final int SEARCH_CATS_LIMIT = 25;
@BindView(R.id.categoriesListBox)
RecyclerView categoriesList;
@BindView(R.id.categoriesSearchBox)
EditText categoriesFilter;
@BindView(R.id.categoriesSearchInProgress)
ProgressBar categoriesSearchInProgress;
@BindView(R.id.categoriesNotFound)
TextView categoriesNotFoundView;
@BindView(R.id.categoriesExplanation)
TextView categoriesSkip;
@Inject MediaWikiApi mwApi;
@Inject @Named("default_preferences") SharedPreferences prefs;
@Inject @Named("prefs") SharedPreferences prefsPrefs;
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
@Inject CategoryDao categoryDao;
@Inject GpsCategoryModel gpsCategoryModel;
private RVRendererAdapter<CategoryItem> categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler;
private HashMap<String, ArrayList<String>> categoriesCache;
private List<CategoryItem> selectedCategories = new ArrayList<>();
private TitleTextWatcher textWatcher = new TitleTextWatcher();
private boolean hasDirectCategories = false;
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
if (item.isSelected()) {
selectedCategories.add(item);
updateCategoryCount(item);
} else {
selectedCategories.remove(item);
}
});
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_categorization, container, false);
ButterKnife.bind(this, rootView);
categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
ArrayList<CategoryItem> items = new ArrayList<>();
categoriesCache = new HashMap<>();
if (savedInstanceState != null) {
items.addAll(savedInstanceState.getParcelableArrayList("currentCategories"));
//noinspection unchecked
categoriesCache.putAll((HashMap<String, ArrayList<String>>) savedInstanceState
.getSerializable("categoriesCache"));
}
categoriesAdapter = adapterFactory.create(items);
categoriesList.setAdapter(categoriesAdapter);
categoriesFilter.addTextChangedListener(textWatcher);
categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
ViewUtil.hideKeyboard(v);
}
});
RxTextView.textChanges(categoriesFilter)
.takeUntil(RxView.detaches(categoriesFilter))
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(filter -> updateCategoryList(filter.toString()));
return rootView;
}
@Override
public void onDestroyView() {
categoriesFilter.removeTextChangedListener(textWatcher);
super.onDestroyView();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
menu.clear();
inflater.inflate(R.menu.fragment_categorization, menu);
}
@Override
public void onResume() {
super.onResume();
View rootView = getView();
if (rootView != null) {
rootView.setFocusableInTouchMode(true);
rootView.requestFocus();
rootView.setOnKeyListener((v, keyCode, event) -> {
if (event.getAction() == ACTION_UP && keyCode == KEYCODE_BACK) {
showBackButtonDialog();
return true;
}
return false;
});
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
int itemCount = categoriesAdapter.getItemCount();
ArrayList<CategoryItem> items = new ArrayList<>(itemCount);
for (int i = 0; i < itemCount; i++) {
items.add(categoriesAdapter.getItem(i));
}
outState.putParcelableArrayList("currentCategories", items);
outState.putSerializable("categoriesCache", categoriesCache);
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.menu_save_categories:
if (selectedCategories.size() > 0) {
//Some categories selected, proceed to submission
onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories));
} else {
//No categories selected, prompt the user to select some
showConfirmationDialog();
}
return true;
default:
return super.onOptionsItemSelected(menuItem);
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
getActivity().setTitle(R.string.categories_activity_title);
}
private void updateCategoryList(String filter) {
Observable.fromIterable(selectedCategories)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(disposable -> {
categoriesSearchInProgress.setVisibility(View.VISIBLE);
categoriesNotFoundView.setVisibility(View.GONE);
categoriesSkip.setVisibility(View.GONE);
categoriesAdapter.clear();
})
.observeOn(Schedulers.io())
.concatWith(
searchAll(filter)
.mergeWith(searchCategories(filter))
.concatWith(TextUtils.isEmpty(filter)
? defaultCategories() : Observable.empty())
)
.filter(categoryItem -> !containsYear(categoryItem.getName()))
.distinct()
.sorted(sortBySimilarity(filter))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
s -> categoriesAdapter.add(s),
Timber::e,
() -> {
categoriesAdapter.notifyDataSetChanged();
categoriesSearchInProgress.setVisibility(View.GONE);
if (categoriesAdapter.getItemCount() == selectedCategories.size()) {
// There are no suggestions
if (TextUtils.isEmpty(filter)) {
// Allow to send image with no categories
categoriesSkip.setVisibility(View.VISIBLE);
} else {
// Inform the user that the searched term matches no category
categoriesNotFoundView.setText(getString(R.string.categories_not_found, filter));
categoriesNotFoundView.setVisibility(View.VISIBLE);
}
}
}
);
}
private Comparator<CategoryItem> sortBySimilarity(final String filter) {
Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
return (firstItem, secondItem) -> stringSimilarityComparator
.compare(firstItem.getName(), secondItem.getName());
}
private List<String> getStringList(List<CategoryItem> input) {
List<String> output = new ArrayList<>();
for (CategoryItem item : input) {
output.add(item.getName());
}
return output;
}
private Observable<CategoryItem> defaultCategories() {
Observable<CategoryItem> directCat = directCategories();
if (hasDirectCategories) {
Timber.d("Image has direct Cat");
return directCat
.concatWith(gpsCategories())
.concatWith(titleCategories())
.concatWith(recentCategories());
}
else {
Timber.d("Image has no direct Cat");
return gpsCategories()
.concatWith(titleCategories())
.concatWith(recentCategories());
}
}
private Observable<CategoryItem> directCategories() {
String directCategory = directPrefs.getString("Category", "");
// Strip newlines to prevent blank categories, and to tidy existing categories
directCategory = directCategory.replace("\n", "");
List<String> categoryList = new ArrayList<>();
Timber.d("Direct category found: " + "'" + directCategory + "'");
if (!directCategory.equals("")) {
hasDirectCategories = true;
categoryList.add(directCategory);
Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList);
}
return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> gpsCategories() {
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
.map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> titleCategories() {
//Retrieve the title that was saved when user tapped submit icon
String title = prefs.getString("Title", "");
return mwApi
.searchTitles(title, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> recentCategories() {
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
.map(s -> new CategoryItem(s, false));
}
private Observable<CategoryItem> searchAll(String term) {
//If user hasn't typed anything in yet, get GPS and recent items
if (TextUtils.isEmpty(term)) {
return Observable.empty();
}
//if user types in something that is in cache, return cached category
if (categoriesCache.containsKey(term)) {
return Observable.fromIterable(categoriesCache.get(term))
.map(name -> new CategoryItem(name, false));
}
//otherwise, search API for matching categories
return mwApi
.allCategories(term, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> searchCategories(String term) {
//If user hasn't typed anything in yet, get GPS and recent items
if (TextUtils.isEmpty(term)) {
return Observable.empty();
}
return mwApi
.searchCategories(term, SEARCH_CATS_LIMIT)
.map(s -> new CategoryItem(s, false));
}
private boolean containsYear(String item) {
//Check for current and previous year to exclude these categories from removal
Calendar now = Calendar.getInstance();
int year = now.get(Calendar.YEAR);
String yearInString = String.valueOf(year);
int prevYear = year - 1;
String prevYearInString = String.valueOf(prevYear);
Timber.d("Previous year: %s", prevYearInString);
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
//And that item does not equal the current year or previous year
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
//Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")
|| (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
}
private void updateCategoryCount(CategoryItem item) {
Category category = categoryDao.find(item.getName());
// Newly used category...
if (category == null) {
category = new Category(null, item.getName(), new Date(), 0);
}
category.incTimesUsed();
categoryDao.save(category);
}
public int getCurrentSelectedCount() {
return selectedCategories.size();
}
/**
* Show dialog asking for confirmation to leave without saving categories.
*/
public void showBackButtonDialog() {
new AlertDialog.Builder(getActivity())
.setMessage("Are you sure you want to go back? The image will not "
+ "have any categories saved.")
.setTitle("Warning")
.setPositiveButton(android.R.string.no, (dialog, id) -> {
//No need to do anything, user remains on categorization screen
})
.setNegativeButton(android.R.string.yes, (dialog, id) -> getActivity().finish())
.create()
.show();
}
private void showConfirmationDialog() {
new AlertDialog.Builder(getActivity())
.setMessage("Images without categories are rarely usable. "
+ "Are you sure you want to submit without selecting "
+ "categories?")
.setTitle("No Categories Selected")
.setPositiveButton(android.R.string.no, (dialog, id) -> {
//Exit menuItem so user can select their categories
})
.setNegativeButton(android.R.string.yes, (dialog, id) -> {
//Proceed to submission
onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories));
})
.create()
.show();
}
private class TitleTextWatcher implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void afterTextChanged(Editable editable) {
if (getActivity() != null) {
getActivity().invalidateOptionsMenu();
}
}
}
}

View file

@ -0,0 +1,5 @@
package fr.free.nrw.commons.category;
public interface CategoryClickedListener {
void categoryClicked(CategoryItem item);
}

View file

@ -101,7 +101,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
*/
@SuppressLint("CheckResult")
private void initList() {
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
@ -208,7 +208,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
*/
@SuppressLint("CheckResult")
private void fetchMoreImages() {
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
@ -227,13 +227,13 @@ public class CategoryImagesListFragment extends DaggerFragment {
* @param collection List of new Media to be displayed
*/
private void handleSuccess(List<Media> collection) {
if(collection == null || collection.isEmpty()) {
if (collection == null || collection.isEmpty()) {
initErrorView();
hasMoreImages = false;
return;
}
if(gridAdapter == null) {
if (gridAdapter == null) {
setAdapter(collection);
} else {
if (gridAdapter.containsAll(collection)) {

View file

@ -3,7 +3,7 @@ package fr.free.nrw.commons.category;
import android.os.Parcel;
import android.os.Parcelable;
class CategoryItem implements Parcelable {
public class CategoryItem implements Parcelable {
private final String name;
private boolean selected;
@ -71,4 +71,9 @@ class CategoryItem implements Parcelable {
public int hashCode() {
return name.hashCode();
}
@Override
public String toString() {
return "CategoryItem: '" + name + '\'';
}
}

View file

@ -0,0 +1,22 @@
package fr.free.nrw.commons.category;
import com.pedrogomez.renderers.AdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
import java.util.ArrayList;
public class CategoryRendererAdapter extends RVRendererAdapter<CategoryItem> {
CategoryRendererAdapter(RendererBuilder<CategoryItem> rendererBuilder, AdapteeCollection<CategoryItem> collection) {
super(rendererBuilder, collection);
}
protected ArrayList<CategoryItem> allItems() {
int itemCount = getItemCount();
ArrayList<CategoryItem> items = new ArrayList<>(itemCount);
for (int i = 0; i < itemCount; i++) {
items.add(getItem(i));
}
return items;
}
}

View file

@ -73,7 +73,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
categoryName = getArguments().getString("categoryName");
isParentCategory = getArguments().getBoolean("isParentCategory");
initSubCategoryList();
if(getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
}
else{
@ -91,7 +91,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
*/
public void initSubCategoryList() {
categoriesNotFoundView.setVisibility(GONE);
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
@ -118,7 +118,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
* @param subCategoryList
*/
private void handleSuccess(List<String> subCategoryList) {
if(subCategoryList == null || subCategoryList.isEmpty()) {
if (subCategoryList == null || subCategoryList.isEmpty()) {
initEmptyView();
}
else {

View file

@ -2,8 +2,11 @@ package fr.free.nrw.commons.contributions;
import android.net.Uri;
import android.os.Parcel;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.StringDef;
import java.lang.annotation.Retention;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
@ -13,6 +16,8 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.settings.Prefs;
import static java.lang.annotation.RetentionPolicy.SOURCE;
public class Contribution extends Media {
public static Creator<Contribution> CREATOR = new Creator<Contribution>() {
@ -33,6 +38,10 @@ public class Contribution extends Media {
public static final int STATE_QUEUED = 2;
public static final int STATE_IN_PROGRESS = 3;
@Retention(SOURCE)
@StringDef({SOURCE_CAMERA, SOURCE_GALLERY, SOURCE_EXTERNAL})
public @interface FileSource {}
public static final String SOURCE_CAMERA = "camera";
public static final String SOURCE_GALLERY = "gallery";
public static final String SOURCE_EXTERNAL = "external";
@ -40,7 +49,6 @@ public class Contribution extends Media {
private Uri contentUri;
private String source;
private String editSummary;
private Date timestamp;
private int state;
private long transferred;
private String decimalCoords;
@ -48,14 +56,13 @@ public class Contribution extends Media {
private String wikiDataEntityId;
private Uri contentProviderUri;
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp,
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date dateCreated,
int state, long dataLength, Date dateUploaded, long transferred,
String source, String description, String creator, boolean isMultiple,
int width, int height, String license) {
super(localUri, imageUrl, filename, description, dataLength, timestamp, dateUploaded, creator);
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
this.contentUri = contentUri;
this.state = state;
this.timestamp = timestamp;
this.transferred = transferred;
this.source = source;
this.isMultiple = isMultiple;
@ -69,14 +76,12 @@ public class Contribution extends Media {
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
this.decimalCoords = decimalCoords;
this.editSummary = editSummary;
timestamp = new Date(System.currentTimeMillis());
}
public Contribution(Parcel in) {
super(in);
contentUri = in.readParcelable(Uri.class.getClassLoader());
source = in.readString();
timestamp = (Date) in.readSerializable();
state = in.readInt();
transferred = in.readLong();
isMultiple = in.readInt() == 1;
@ -87,12 +92,13 @@ public class Contribution extends Media {
super.writeToParcel(parcel, flags);
parcel.writeParcelable(contentUri, flags);
parcel.writeString(source);
parcel.writeSerializable(timestamp);
parcel.writeInt(state);
parcel.writeLong(transferred);
parcel.writeInt(isMultiple ? 1 : 0);
}
public boolean getMultiple() {
return isMultiple;
}
@ -121,14 +127,6 @@ public class Contribution extends Media {
this.contentUri = contentUri;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public int getState() {
return state;
}
@ -141,10 +139,6 @@ public class Contribution extends Media {
this.dateUploaded = date;
}
public String getTrackingTemplates() {
return "{{subst:unc}}"; // Remove when we have categorization
}
public String getPageContents() {
StringBuilder buffer = new StringBuilder();
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
@ -169,8 +163,15 @@ public class Contribution extends Media {
buffer.append("== {{int:license-header}} ==\n")
.append(licenseTemplateFor(getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n")
.append(getTrackingTemplates());
.append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n");
if(categories!=null&&categories.size()!=0) {
for (int i = 0; i < categories.size(); i++) {
String category = categories.get(i);
buffer.append("\n[[Category:").append(category).append("]]");
}
}
else
buffer.append("{{subst:unc}}");
return buffer.toString();
}
@ -184,7 +185,7 @@ public class Contribution extends Media {
}
public Contribution() {
timestamp = new Date(System.currentTimeMillis());
}
public String getSource() {
@ -232,7 +233,7 @@ public class Contribution extends Media {
/**
* When the corresponding wikidata entity is known as in case of nearby uploads, it can be set
* using the setter method
* @param wikiDataEntityId
* @param wikiDataEntityId wikiDataEntityId
*/
public void setWikiDataEntityId(String wikiDataEntityId) {
this.wikiDataEntityId = wikiDataEntityId;

View file

@ -5,22 +5,26 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.FileProvider;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import fr.free.nrw.commons.upload.ShareActivity;
import fr.free.nrw.commons.upload.UploadActivity;
import timber.log.Timber;
import static android.content.Intent.ACTION_GET_CONTENT;
import static android.content.Intent.ACTION_SEND;
import static android.content.Intent.ACTION_SEND_MULTIPLE;
import static android.content.Intent.EXTRA_STREAM;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY;
@ -31,6 +35,7 @@ public class ContributionController {
public static final int SELECT_FROM_GALLERY = 1;
public static final int SELECT_FROM_CAMERA = 2;
public static final int PICK_IMAGE_MULTIPLE = 3;
private Fragment fragment;
@ -79,6 +84,14 @@ public class ContributionController {
}
public void startGalleryPick() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
startMultipleGalleryPick();
} else {
startSingleGalleryPick();
}
}
public void startSingleGalleryPick() {
//FIXME: Starts gallery (opens Google Photos)
Intent pickImageIntent = new Intent(ACTION_GET_CONTENT);
pickImageIntent.setType("image/*");
@ -87,15 +100,41 @@ public class ContributionController {
Timber.d("Fragment is not added, startActivityForResult cannot be called");
return;
}
Timber.d("startGalleryPick() called with pickImageIntent");
Timber.d("startSingleGalleryPick() called with pickImageIntent");
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public void startMultipleGalleryPick() {
Intent pickImageIntent = new Intent(ACTION_GET_CONTENT);
pickImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
pickImageIntent.setType("image/*");
if (!fragment.isAdded()) {
Timber.d("Fragment is not added, startActivityForResult cannot be called");
return;
}
Timber.d("startMultipleGalleryPick() called with pickImageIntent");
fragment.startActivityForResult(pickImageIntent, PICK_IMAGE_MULTIPLE);
}
public void handleImagesPicked(int requestCode, @Nullable ArrayList<Uri> uri) {
FragmentActivity activity = fragment.getActivity();
Intent shareIntent = new Intent(activity, UploadActivity.class);
shareIntent.setAction(ACTION_SEND_MULTIPLE);
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);
shareIntent.putExtra(EXTRA_STREAM, uri);
shareIntent.setType("image/jpeg");
if (activity != null) {
activity.startActivity(shareIntent);
}
}
public void handleImagePicked(int requestCode, @Nullable Uri uri, boolean isDirectUpload, String wikiDataEntityId) {
FragmentActivity activity = fragment.getActivity();
Timber.d("handleImagePicked() called with onActivityResult(). Boolean isDirectUpload: " + isDirectUpload + "String wikiDataEntityId: " + wikiDataEntityId);
Intent shareIntent = new Intent(activity, ShareActivity.class);
Intent shareIntent = new Intent(activity, UploadActivity.class);
shareIntent.setAction(ACTION_SEND);
switch (requestCode) {
case SELECT_FROM_GALLERY:

View file

@ -98,7 +98,8 @@ public class ContributionDao {
cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime());
}
cv.put(Table.COLUMN_LENGTH, contribution.getDataLength());
cv.put(Table.COLUMN_TIMESTAMP, contribution.getTimestamp().getTime());
//This was always meant to store the date created..If somehow date created is not fetched while actually saving the contribution, lets save today's date
cv.put(Table.COLUMN_TIMESTAMP, contribution.getDateCreated()==null?System.currentTimeMillis():contribution.getDateCreated().getTime());
cv.put(Table.COLUMN_STATE, contribution.getState());
cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred());
cv.put(Table.COLUMN_SOURCE, contribution.getSource());

View file

@ -15,10 +15,10 @@ class ContributionViewHolder {
final ProgressBar progressView;
ContributionViewHolder(View parent) {
imageView = (MediaWikiImageView) parent.findViewById(R.id.contributionImage);
titleView = (TextView)parent.findViewById(R.id.contributionTitle);
stateView = (TextView)parent.findViewById(R.id.contributionState);
seqNumView = (TextView)parent.findViewById(R.id.contributionSequenceNumber);
progressView = (ProgressBar)parent.findViewById(R.id.contributionProgress);
imageView = parent.findViewById(R.id.contributionImage);
titleView = parent.findViewById(R.id.contributionTitle);
stateView = parent.findViewById(R.id.contributionState);
seqNumView = parent.findViewById(R.id.contributionSequenceNumber);
progressView = parent.findViewById(R.id.contributionProgress);
}
}

View file

@ -1,382 +0,0 @@
package fr.free.nrw.commons.contributions;
import android.accounts.Account;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Adapter;
import android.widget.AdapterView;
import java.util.ArrayList;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.ButterKnife;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.quiz.QuizChecker;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.UploadService;
import fr.free.nrw.commons.utils.ContributionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static android.content.ContentResolver.requestSync;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
public class ContributionsActivity
extends AuthenticatedActivity
implements LoaderManager.LoaderCallbacks<Cursor>,
AdapterView.OnItemClickListener,
MediaDetailPagerFragment.MediaDetailProvider,
FragmentManager.OnBackStackChangedListener,
ContributionsListFragment.SourceRefresher {
@Inject MediaWikiApi mediaWikiApi;
@Inject SessionManager sessionManager;
@Inject @Named("default_preferences") SharedPreferences prefs;
@Inject ContributionDao contributionDao;
private Cursor allContributions;
private ContributionsListFragment contributionsList;
private MediaDetailPagerFragment mediaDetails;
private UploadService uploadService;
private boolean isUploadServiceConnected;
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>();
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private ServiceConnection uploadServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder binder) {
uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder)
.getService();
isUploadServiceConnected = true;
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
// this should never happen
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
}
};
@Override
protected void onDestroy() {
compositeDisposable.clear();
getSupportFragmentManager().removeOnBackStackChangedListener(this);
super.onDestroy();
if (isUploadServiceConnected) {
unbindService(uploadServiceConnection);
}
}
@Override
protected void onResume() {
super.onResume();
boolean isSettingsChanged = prefs.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
prefs.edit().putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false).apply();
if (isSettingsChanged) {
refreshSource();
}
}
@Override
protected void onAuthCookieAcquired(String authCookie) {
// Do a sync everytime we get here!
requestSync(sessionManager.getCurrentAccount(), BuildConfig.CONTRIBUTION_AUTHORITY, new Bundle());
Intent uploadServiceIntent = new Intent(this, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
startService(uploadServiceIntent);
bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
allContributions = contributionDao.loadAllContributions();
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contributions);
ButterKnife.bind(this);
// Activity can call methods in the fragment by acquiring a
// reference to the Fragment from FragmentManager, using findFragmentById()
FragmentManager supportFragmentManager = getSupportFragmentManager();
contributionsList = (ContributionsListFragment)supportFragmentManager
.findFragmentById(R.id.contributionsListFragment);
supportFragmentManager.addOnBackStackChangedListener(this);
if (savedInstanceState != null) {
mediaDetails = (MediaDetailPagerFragment)supportFragmentManager
.findFragmentById(R.id.contributionsFragmentContainer);
getSupportLoaderManager().initLoader(0, null, this);
}
requestAuthToken();
initDrawer();
setTitle(getString(R.string.title_activity_contributions));
if(checkAccount()) {
new QuizChecker(this,
sessionManager.getCurrentAccount().name,
mediaWikiApi);
}
if(!BuildConfig.FLAVOR.equalsIgnoreCase("beta")){
setUploadCount();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
boolean mediaDetailsVisible = mediaDetails != null && mediaDetails.isVisible();
outState.putBoolean("mediaDetailsVisible", mediaDetailsVisible);
}
/**
* Replace whatever is in the current contributionsFragmentContainer view with
* mediaDetailPagerFragment, and preserve previous state in back stack.
* Called when user selects a contribution.
*/
private void showDetail(int i) {
if (mediaDetails == null || !mediaDetails.isVisible()) {
mediaDetails = new MediaDetailPagerFragment();
FragmentManager supportFragmentManager = getSupportFragmentManager();
supportFragmentManager
.beginTransaction()
.replace(R.id.contributionsFragmentContainer, mediaDetails)
.addToBackStack(null)
.commit();
supportFragmentManager.executePendingTransactions();
}
mediaDetails.showImage(i);
}
public void retryUpload(int i) {
allContributions.moveToPosition(i);
Contribution c = contributionDao.fromCursor(allContributions);
if (c.getState() == STATE_FAILED) {
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c);
Timber.d("Restarting for %s", c.toString());
} else {
Timber.d("Skipping re-upload for non-failed %s", c.toString());
}
}
public void deleteUpload(int i) {
allContributions.moveToPosition(i);
Contribution c = contributionDao.fromCursor(allContributions);
if (c.getState() == STATE_FAILED) {
Timber.d("Deleting failed contrib %s", c.toString());
// If upload fails and then user decides to cancel upload at all, which means contribution
// object will be deleted. So we have to delete temp file for that contribution.
ContributionUtils.removeTemporaryFile(c.getLocalUri());
contributionDao.delete(c);
} else {
Timber.d("Skipping deletion for non-failed contrib %s", c.toString());
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
if (mediaDetails.isVisible()) {
getSupportFragmentManager().popBackStack();
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onAuthFailure() {
finish(); // If authentication failed, we just exit
}
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long item) {
showDetail(position);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return super.onCreateOptionsMenu(menu);
}
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
int uploads = prefs.getInt(UPLOADS_SHOWING, 100);
return new CursorLoader(this, BASE_URI,
ALL_FIELDS, "", null,
ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads);
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
contributionsList.changeProgressBarVisibility(false);
if (contributionsList.getAdapter() == null) {
contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(),
cursor, 0, contributionDao));
} else {
((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor);
}
if(contributionsList.getAdapter().getCount()>0){
contributionsList.changeEmptyScreen(false);
}
contributionsList.clearSyncMessage();
notifyAndMigrateDataSetObservers();
}
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
((CursorAdapter) contributionsList.getAdapter()).swapCursor(null);
}
//FIXME: Potential cause of wrong image display bug
@Override
public Media getMediaAtPosition(int i) {
if (contributionsList.getAdapter() == null) {
// not yet ready to return data
return null;
} else {
return contributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
}
}
@Override
public int getTotalMediaCount() {
if (contributionsList.getAdapter() == null) {
return 0;
}
return contributionsList.getAdapter().getCount();
}
@SuppressWarnings("ConstantConditions")
private void setUploadCount() {
compositeDisposable.add(mediaWikiApi
.getUploadCount(sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::displayUploadCount,
t -> Timber.e(t, "Fetching upload count failed")
));
}
private void displayUploadCount(Integer uploadCount) {
if (isFinishing()
|| getSupportActionBar() == null
|| getResources() == null) {
return;
}
getSupportActionBar().setSubtitle(getResources()
.getQuantityString(R.plurals.contributions_subtitle,
uploadCount, uploadCount));
}
public void betaSetUploadCount(int betaUploadCount) {
displayUploadCount(betaUploadCount);
}
@Override
public void notifyDatasetChanged() {
// Do nothing for now
}
private void notifyAndMigrateDataSetObservers() {
Adapter adapter = contributionsList.getAdapter();
// First, move the observers over to the adapter now that we have it.
for (DataSetObserver observer : observersWaitingForLoad) {
adapter.registerDataSetObserver(observer);
}
observersWaitingForLoad.clear();
// Now fire off a first notification...
for (DataSetObserver observer : observersWaitingForLoad) {
observer.onChanged();
}
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
Adapter adapter = contributionsList.getAdapter();
if (adapter == null) {
observersWaitingForLoad.add(observer);
} else {
adapter.registerDataSetObserver(observer);
}
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
Adapter adapter = contributionsList.getAdapter();
if (adapter == null) {
observersWaitingForLoad.remove(observer);
} else {
adapter.unregisterDataSetObserver(observer);
}
}
/**
* to ensure user is logged in
* @return
*/
private boolean checkAccount() {
Account currentAccount = sessionManager.getCurrentAccount();
if (currentAccount == null) {
Timber.d("Current account is null");
ViewUtil.showLongToast(this, getResources().getString(R.string.user_not_logged_in));
sessionManager.forceLogin(this);
return false;
}
return true;
}
@Override
public void onBackStackChanged() {
initBackButton();
}
@Override
public void refreshSource() {
getSupportLoaderManager().restartLoader(0, null, this);
}
}

View file

@ -0,0 +1,625 @@
package fr.free.nrw.commons.contributions;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.app.LoaderManager;
import android.support.v4.widget.CursorAdapter;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Adapter;
import android.widget.AdapterView;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
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.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyController;
import fr.free.nrw.commons.nearby.NearbyNoificationCardView;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.notification.NotificationController;
import fr.free.nrw.commons.notification.UnreadNotificationsCheckAsync;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.UploadService;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST;
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
public class ContributionsFragment
extends CommonsDaggerSupportFragment
implements LoaderManager.LoaderCallbacks<Cursor>,
AdapterView.OnItemClickListener,
MediaDetailPagerFragment.MediaDetailProvider,
FragmentManager.OnBackStackChangedListener,
ContributionsListFragment.SourceRefresher,
LocationUpdateListener
{
@Inject
@Named("default_preferences")
SharedPreferences prefs;
@Inject
ContributionDao contributionDao;
@Inject
MediaWikiApi mediaWikiApi;
@Inject
NotificationController notificationController;
@Inject
NearbyController nearbyController;
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>();
private Cursor allContributions;
private UploadService uploadService;
private boolean isUploadServiceConnected;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
CountDownLatch waitForContributionsListFragment = new CountDownLatch(1);
private ContributionsListFragment contributionsListFragment;
private MediaDetailPagerFragment mediaDetailPagerFragment;
public static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
public static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
public NearbyNoificationCardView nearbyNoificationCardView;
private Disposable placesDisposable;
private LatLng curLatLng;
private boolean firstLocationUpdate = true;
private LocationServiceManager locationManager;
private boolean isFragmentAttachedBefore = false;
/**
* Since we will need to use parent activity on onAuthCookieAcquired, we have to wait
* fragment to be attached. Latch will be responsible for this sync.
*/
private ServiceConnection uploadServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder binder) {
uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder)
.getService();
isUploadServiceConnected = true;
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
// this should never happen
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
}
};
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_contributions, container, false);
nearbyNoificationCardView = view.findViewById(R.id.card_view_nearby);
if (savedInstanceState != null) {
mediaDetailPagerFragment = (MediaDetailPagerFragment)getChildFragmentManager().findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
contributionsListFragment = (ContributionsListFragment) getChildFragmentManager().findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG);
if (savedInstanceState.getBoolean("mediaDetailsVisible")) {
setMediaDetailPagerFragment();
} else {
setContributionsListFragment();
}
} else {
setContributionsListFragment();
}
if(!BuildConfig.FLAVOR.equalsIgnoreCase("beta")){
setUploadCount();
}
return view;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
/*
- There are some operations we need auth, so we need to make sure isAuthCookieAcquired.
- And since we use same retained fragment doesn't want to make all network operations
all over again on same fragment attached to recreated activity, we do this network
operations on first time fragment atached to an activity. Then they will be retained
until fragment life time ends.
*/
if (((MainActivity)getActivity()).isAuthCookieAcquired && !isFragmentAttachedBefore) {
onAuthCookieAcquired(((MainActivity)getActivity()).uploadServiceIntent);
isFragmentAttachedBefore = true;
new UnreadNotificationsCheckAsync((MainActivity) getActivity(), notificationController).execute();
}
}
/**
* Replace FrameLayout with ContributionsListFragment, user will see contributions list.
* Creates new one if null.
*/
public void setContributionsListFragment() {
// show tabs on contribution list is visible
((MainActivity)getActivity()).showTabs();
// show nearby card view on contributions list is visible
if (nearbyNoificationCardView != null) {
if (prefs.getBoolean("displayNearbyCardView", true)) {
nearbyNoificationCardView.setVisibility(View.VISIBLE);
} else {
nearbyNoificationCardView.setVisibility(View.GONE);
}
}
// Create if null
if (getContributionsListFragment() == null) {
contributionsListFragment = new ContributionsListFragment();
}
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
// When this container fragment is created, we fill it with our ContributionsListFragment
transaction.replace(R.id.root_frame, contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG);
transaction.addToBackStack(CONTRIBUTION_LIST_FRAGMENT_TAG);
transaction.commit();
getChildFragmentManager().executePendingTransactions();
}
/**
* Replace FrameLayout with MediaDetailPagerFragment, user will see details of selected media.
* Creates new one if null.
*/
public void setMediaDetailPagerFragment() {
// hide tabs on media detail view is visible
((MainActivity)getActivity()).hideTabs();
// hide nearby card view on media detail is visible
nearbyNoificationCardView.setVisibility(View.GONE);
// Create if null
if (getMediaDetailPagerFragment() == null) {
mediaDetailPagerFragment = new MediaDetailPagerFragment();
}
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
// When this container fragment is created, we fill it with our MediaDetailPagerFragment
//transaction.addToBackStack(null);
transaction.add(R.id.root_frame, mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
transaction.addToBackStack(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
transaction.commit();
getChildFragmentManager().executePendingTransactions();
}
/**
* Just getter method of ContributionsListFragment child of ContributionsFragment
* @return contributionsListFragment, if any created
*/
public ContributionsListFragment getContributionsListFragment() {
return contributionsListFragment;
}
/**
* Just getter method of MediaDetailPagerFragment child of ContributionsFragment
* @return mediaDetailsFragment, if any created
*/
public MediaDetailPagerFragment getMediaDetailPagerFragment() {
return mediaDetailPagerFragment;
}
@Override
public void onBackStackChanged() {
((MainActivity)getActivity()).initBackButton();
}
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
int uploads = prefs.getInt(UPLOADS_SHOWING, 100);
return new CursorLoader(getActivity(), BASE_URI, //TODO find out the reason we pass activity here
ALL_FIELDS, "", null,
ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads);
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (contributionsListFragment != null) {
contributionsListFragment.changeProgressBarVisibility(false);
if (contributionsListFragment.getAdapter() == null) {
contributionsListFragment.setAdapter(new ContributionsListAdapter(getActivity().getApplicationContext(),
cursor, 0, contributionDao));
} else {
((CursorAdapter) contributionsListFragment.getAdapter()).swapCursor(cursor);
}
contributionsListFragment.clearSyncMessage();
notifyAndMigrateDataSetObservers();
}
}
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
((CursorAdapter) contributionsListFragment.getAdapter()).swapCursor(null);
}
private void notifyAndMigrateDataSetObservers() {
Adapter adapter = contributionsListFragment.getAdapter();
// First, move the observers over to the adapter now that we have it.
for (DataSetObserver observer : observersWaitingForLoad) {
adapter.registerDataSetObserver(observer);
}
observersWaitingForLoad.clear();
// Now fire off a first notification...
for (DataSetObserver observer : observersWaitingForLoad) {
observer.onChanged();
}
}
/**
* Called when onAuthCookieAcquired is called on authenticated parent activity
* @param uploadServiceIntent
*/
public void onAuthCookieAcquired(Intent uploadServiceIntent) {
// Since we call onAuthCookieAcquired method from onAttach, isAdded is still false. So don't use it
if (getActivity() != null) { // If fragment is attached to parent activity
getActivity().bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
isUploadServiceConnected = true;
allContributions = contributionDao.loadAllContributions();
getActivity().getSupportLoaderManager().initLoader(0, null, ContributionsFragment.this);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case LOCATION_REQUEST: {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Timber.d("Location permission granted, refreshing view");
// No need to display permission request button anymore
nearbyNoificationCardView.displayPermissionRequestButton(false);
locationManager.registerLocationManager();
} else {
// Still ask for permission
nearbyNoificationCardView.displayPermissionRequestButton(true);
}
}
break;
default:
// This is needed to allow the request codes from the Fragments to be routed appropriately
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
// show detail at a position
showDetail(i);
}
/**
* Replace whatever is in the current contributionsFragmentContainer view with
* mediaDetailPagerFragment, and preserve previous state in back stack.
* Called when user selects a contribution.
*/
private void showDetail(int i) {
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
mediaDetailPagerFragment = new MediaDetailPagerFragment();
setMediaDetailPagerFragment();
}
mediaDetailPagerFragment.showImage(i);
}
/**
* Retry upload when it is failed
* @param i position of upload which will be retried
*/
public void retryUpload(int i) {
allContributions.moveToPosition(i);
Contribution c = contributionDao.fromCursor(allContributions);
if (c.getState() == STATE_FAILED) {
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c);
Timber.d("Restarting for %s", c.toString());
} else {
Timber.d("Skipping re-upload for non-failed %s", c.toString());
}
}
/**
* Delete a failed upload attempt
* @param i position of upload attempt which will be deteled
*/
public void deleteUpload(int i) {
allContributions.moveToPosition(i);
Contribution c = contributionDao.fromCursor(allContributions);
if (c.getState() == STATE_FAILED) {
Timber.d("Deleting failed contrib %s", c.toString());
contributionDao.delete(c);
} else {
Timber.d("Skipping deletion for non-failed contrib %s", c.toString());
}
}
@Override
public void refreshSource() {
getActivity().getSupportLoaderManager().restartLoader(0, null, this);
}
@Override
public Media getMediaAtPosition(int i) {
if (contributionsListFragment.getAdapter() == null) {
// not yet ready to return data
return null;
} else {
return contributionDao.fromCursor((Cursor) contributionsListFragment.getAdapter().getItem(i));
}
}
@Override
public int getTotalMediaCount() {
if (contributionsListFragment.getAdapter() == null) {
return 0;
}
return contributionsListFragment.getAdapter().getCount();
}
@Override
public void notifyDatasetChanged() {
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
Adapter adapter = contributionsListFragment.getAdapter();
if (adapter == null) {
observersWaitingForLoad.add(observer);
} else {
adapter.registerDataSetObserver(observer);
}
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
Adapter adapter = contributionsListFragment.getAdapter();
if (adapter == null) {
observersWaitingForLoad.remove(observer);
} else {
adapter.unregisterDataSetObserver(observer);
}
}
@SuppressWarnings("ConstantConditions")
private void setUploadCount() {
compositeDisposable.add(mediaWikiApi
.getUploadCount(((MainActivity)getActivity()).sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::displayUploadCount,
t -> Timber.e(t, "Fetching upload count failed")
));
}
private void displayUploadCount(Integer uploadCount) {
if (getActivity().isFinishing()
|| getResources() == null) {
return;
}
((MainActivity)getActivity()).setNumOfUploads(uploadCount);
}
public void betaSetUploadCount(int betaUploadCount) {
displayUploadCount(betaUploadCount);
}
/**
* Updates notification indicator on toolbar to indicate there are unread notifications
* @param isThereUnreadNotifications true if user checked notifications before last notification date
*/
public void updateNotificationsNotification(boolean isThereUnreadNotifications) {
((MainActivity)getActivity()).updateNotificationIcon(isThereUnreadNotifications);
}
@Override
public void onStart() {
super.onStart();
}
@Override
public void onPause() {
super.onPause();
locationManager.removeLocationListener(this);
locationManager.unregisterLocationManager();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
boolean mediaDetailsVisible = mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible();
outState.putBoolean("mediaDetailsVisible", mediaDetailsVisible);
}
@Override
public void onResume() {
super.onResume();
locationManager = new LocationServiceManager(getActivity());
firstLocationUpdate = true;
locationManager.addLocationListener(this);
boolean isSettingsChanged = prefs.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
prefs.edit().putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false).apply();
if (isSettingsChanged) {
refreshSource();
}
if (prefs.getBoolean("displayNearbyCardView", true)) {
nearbyNoificationCardView.cardViewVisibilityState = NearbyNoificationCardView.CardViewVisibilityState.LOADING;
nearbyNoificationCardView.setVisibility(View.VISIBLE);
checkGPS();
} else {
// Hide nearby notification card view if related shared preferences is false
nearbyNoificationCardView.setVisibility(View.GONE);
}
}
/**
* Check GPS to decide displaying request permission button or not.
*/
private void checkGPS() {
if (!locationManager.isProviderEnabled()) {
Timber.d("GPS is not enabled");
nearbyNoificationCardView.permissionType = NearbyNoificationCardView.PermissionType.ENABLE_GPS;
nearbyNoificationCardView.displayPermissionRequestButton(true);
} else {
Timber.d("GPS is enabled");
checkLocationPermission();
}
}
private void checkLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (locationManager.isLocationPermissionGranted()) {
nearbyNoificationCardView.permissionType = NearbyNoificationCardView.PermissionType.NO_PERMISSION_NEEDED;
nearbyNoificationCardView.displayPermissionRequestButton(false);
locationManager.registerLocationManager();
} else {
nearbyNoificationCardView.permissionType = NearbyNoificationCardView.PermissionType.ENABLE_LOCATION_PERMISSON;
nearbyNoificationCardView.displayPermissionRequestButton(true);
}
} else {
// If device is under Marshmallow, we already checked for GPS
nearbyNoificationCardView.permissionType = NearbyNoificationCardView.PermissionType.NO_PERMISSION_NEEDED;
nearbyNoificationCardView.displayPermissionRequestButton(false);
locationManager.registerLocationManager();
}
}
private void updateClosestNearbyCardViewInfo() {
curLatLng = locationManager.getLastLocation();
placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng, true)) // thanks to boolean, it will only return closest result
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::updateNearbyNotification,
throwable -> {
Timber.d(throwable);
updateNearbyNotification(null);
});
}
private void updateNearbyNotification(@Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null && nearbyPlacesInfo.placeList.size() > 0) {
Place closestNearbyPlace = nearbyPlacesInfo.placeList.get(0);
String distance = formatDistanceBetween(curLatLng, closestNearbyPlace.location);
closestNearbyPlace.setDistance(distance);
nearbyNoificationCardView.updateContent (true, closestNearbyPlace);
} else {
// Means that no close nearby place is found
nearbyNoificationCardView.updateContent (false, null);
}
}
@Override
public void onDestroy() {
compositeDisposable.clear();
getChildFragmentManager().removeOnBackStackChangedListener(this);
locationManager.unregisterLocationManager();
locationManager.removeLocationListener(this);
// Try to prevent a possible NPE
locationManager.context = null;
super.onDestroy();
if (isUploadServiceConnected) {
if (getActivity() != null) {
getActivity().unbindService(uploadServiceConnection);
isUploadServiceConnected = false;
}
}
if (placesDisposable != null) {
placesDisposable.dispose();
}
}
@Override
public void onLocationChangedSignificantly(LatLng latLng) {
// Will be called if location changed more than 1000 meter
// Do nothing on slight changes for using network efficiently
firstLocationUpdate = false;
updateClosestNearbyCardViewInfo();
}
@Override
public void onLocationChangedSlightly(LatLng latLng) {
/* Update closest nearby notification card onLocationChangedSlightly
If first time to update location after onResume, then no need to wait for significant
location change. Any closest location is better than no location
*/
if (firstLocationUpdate) {
updateClosestNearbyCardViewInfo();
// Turn it to false, since it is not first location update anymore. To change closest location
// notifiction, we need to wait for a significant location change.
firstLocationUpdate = false;
}
}
@Override
public void onLocationChangedMedium(LatLng latLng) {
// Update closest nearby card view if location changed more than 500 meters
updateClosestNearbyCardViewInfo();
}
}

View file

@ -1,25 +1,31 @@
package fr.free.nrw.commons.contributions;
import android.content.ClipData;
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.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ListAdapter;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Arrays;
import javax.inject.Inject;
@ -30,7 +36,7 @@ import butterknife.ButterKnife;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.utils.PermissionUtils;
import timber.log.Timber;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
@ -38,6 +44,11 @@ import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.app.Activity.RESULT_OK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.view.View.GONE;
import static fr.free.nrw.commons.contributions.ContributionController.SELECT_FROM_GALLERY;
/**
* Created by root on 01.06.2018.
*/
public class ContributionsListFragment extends CommonsDaggerSupportFragment {
@ -47,100 +58,121 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
TextView waitingMessage;
@BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar;
@BindView(R.id.fab_plus)
FloatingActionButton fabPlus;
@BindView(R.id.fab_camera)
FloatingActionButton fabCamera;
@BindView(R.id.fab_galery)
FloatingActionButton fabGalery;
@BindView(R.id.noDataYet)
TextView noDataYet;
@Inject
@Named("prefs")
SharedPreferences prefs;
@Inject
@Named("default_preferences")
SharedPreferences defaultPrefs;
private ContributionController controller;
private Animation fab_close;
private Animation fab_open;
private Animation rotate_forward;
private Animation rotate_backward;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_contributions, container, false);
ButterKnife.bind(this, v);
private boolean isFabOpen = false;
public ContributionController controller;
contributionsList.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
if (savedInstanceState != null) {
Timber.d("Scrolling to %d", savedInstanceState.getInt("grid-position"));
contributionsList.setSelection(savedInstanceState.getInt("grid-position"));
}
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
ButterKnife.bind(this, view);
//TODO: Should this be in onResume?
String lastModified = prefs.getString("lastSyncTimestamp", "");
Timber.d("Last Sync Timestamp: %s", lastModified);
if (lastModified.equals("")) {
waitingMessage.setVisibility(View.VISIBLE);
} else {
waitingMessage.setVisibility(GONE);
}
contributionsList.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment());
changeEmptyScreen(true);
changeProgressBarVisibility(true);
return v;
return view;
}
public ListAdapter getAdapter() {
return contributionsList.getAdapter();
}
public void setAdapter(ListAdapter adapter) {
this.contributionsList.setAdapter(adapter);
if(BuildConfig.FLAVOR.equalsIgnoreCase("beta")){
((ContributionsActivity) getActivity()).betaSetUploadCount(adapter.getCount());
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (controller == null) {
controller = new ContributionController(this);
}
controller.loadState(savedInstanceState);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (controller != null) {
controller.saveState(outState);
} else {
controller = new ContributionController(this);
}
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initializeAnimations();
setListeners();
}
public void changeEmptyScreen(boolean isEmpty){
this.noDataYet.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
}
public void changeProgressBarVisibility(boolean isVisible) {
this.progressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE);
private void initializeAnimations() {
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
}
@Override
public void onSaveInstanceState(Bundle outState) {
if (outState == null) {
outState = new Bundle();
}
super.onSaveInstanceState(outState);
controller.saveState(outState);
outState.putInt("grid-position", contributionsList.getFirstVisiblePosition());
}
private void setListeners() {
@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 == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
if (requestCode == ContributionController.SELECT_FROM_CAMERA) {
// If coming from camera, pass null as uri. Because camera photos get saved to a
// fixed directory
controller.handleImagePicked(requestCode, null, false, null);
} else {
controller.handleImagePicked(requestCode, data.getData(), false, null);
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
fabCamera.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) {
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
if (shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
new AlertDialog.Builder(getParentFragment().getActivity())
.setMessage(getString(R.string.write_storage_permission_rationale))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
getActivity().requestPermissions
(new String[]{WRITE_EXTERNAL_STORAGE}, PermissionUtils.CAMERA_PERMISSION_FROM_CONTRIBUTION_LIST);
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
.create()
.show();
} else {
// No explanation needed, we can request the permission.
requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE},
3);
// MY_PERMISSIONS_WRITE_EXTERNAL_STORAGE is an
// app-defined int constant. The callback method gets the
// result of the request.
}
} else {
controller.startCameraCapture();
}
} else {
controller.startCameraCapture();
}
}
} else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
}
}
});
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_from_gallery:
fabGalery.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//Gallery crashes before reach ShareActivity screen so must implement permissions check here
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -156,10 +188,11 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
new AlertDialog.Builder(getActivity())
new AlertDialog.Builder(getParentFragment().getActivity())
.setMessage(getString(R.string.read_storage_permission_rationale))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, 1);
getActivity().requestPermissions
(new String[]{READ_EXTERNAL_STORAGE}, PermissionUtils.GALLERY_PERMISSION_FROM_CONTRIBUTION_LIST);
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
@ -179,59 +212,64 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
}
} else {
controller.startGalleryPick();
return true;
}
} else {
controller.startGalleryPick();
return true;
}
}
});
}
return true;
case R.id.menu_from_camera:
boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) {
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
if (shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
new AlertDialog.Builder(getActivity())
.setMessage(getString(R.string.write_storage_permission_rationale))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 3);
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
.create()
.show();
} else {
// No explanation needed, we can request the permission.
requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE},
3);
// MY_PERMISSIONS_WRITE_EXTERNAL_STORAGE is an
// app-defined int constant. The callback method gets the
// result of the request.
}
} else {
controller.startCameraCapture();
return true;
}
} else {
controller.startCameraCapture();
return true;
}
return true;
default:
return super.onOptionsItemSelected(item);
private void animateFAB(boolean isFabOpen) {
this.isFabOpen = !isFabOpen;
if (fabPlus.isShown()){
if (isFabOpen) {
fabPlus.startAnimation(rotate_backward);
fabCamera.startAnimation(fab_close);
fabGalery.startAnimation(fab_close);
fabCamera.hide();
fabGalery.hide();
} else {
fabPlus.startAnimation(rotate_forward);
fabCamera.startAnimation(fab_open);
fabGalery.startAnimation(fab_open);
fabCamera.show();
fabGalery.show();
}
this.isFabOpen=!isFabOpen;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
public void onAttach(Context context) {
super.onAttach(context);
ContributionsFragment parentFragment = (ContributionsFragment)getParentFragment();
parentFragment.waitForContributionsListFragment.countDown();
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
if (requestCode == ContributionController.SELECT_FROM_CAMERA) {
// If coming from camera, pass null as uri. Because camera photos get saved to a
// fixed directory
controller.handleImagePicked(requestCode, null, false, null);
} else if (requestCode == ContributionController.PICK_IMAGE_MULTIPLE) {
handleMultipleImages(requestCode, data);
} else if (requestCode == ContributionController.SELECT_FROM_GALLERY){
controller.handleImagePicked(requestCode, data.getData(), false, null);
}
} else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Timber.d("onRequestPermissionsResult: req code = " + " perm = "
+ Arrays.toString(permissions) + " grant =" + Arrays.toString(grantResults));
@ -246,11 +284,12 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
break;
// 2 = Location allowed when 'nearby places' selected
case 2: {
if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) {
// TODO: understand and fix
/*if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) {
Timber.d("Location permission granted");
Intent nearbyIntent = new Intent(getActivity(), NearbyActivity.class);
Intent nearbyIntent = new Intent(getActivity(), MainActivity.class);
startActivity(nearbyIntent);
}
}*/
}
break;
case 3: {
@ -262,42 +301,60 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
menu.clear(); // See http://stackoverflow.com/a/8495697/17865
inflater.inflate(R.menu.fragment_contributions_list, menu);
private void handleMultipleImages(int requestCode, Intent data) {
if (getContext() == null) {
return;
}
if (!deviceHasCamera()) {
menu.findItem(R.id.menu_from_camera).setEnabled(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
&& data.getClipData() != null) {
ClipData mClipData = data.getClipData();
ArrayList<Uri> mArrayUri = new ArrayList<Uri>();
for (int i = 0; i < mClipData.getItemCount(); i++) {
ClipData.Item item = mClipData.getItemAt(i);
Uri uri = item.getUri();
mArrayUri.add(uri);
}
Log.v("LOG_TAG", "Selected Images" + mArrayUri.size());
controller.handleImagesPicked(requestCode, mArrayUri);
} else if(data.getData() != null) {
controller.handleImagePicked(SELECT_FROM_GALLERY, data.getData(), false, null);
}
}
public boolean deviceHasCamera() {
PackageManager pm = getContext().getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) ||
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
controller = new ContributionController(this);
setHasOptionsMenu(true);
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
controller.loadState(savedInstanceState);
/**
* Responsible to set progress bar invisible and visible
* @param isVisible True when contributions list should be hidden.
*/
public void changeProgressBarVisibility(boolean isVisible) {
this.progressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE);
}
/**
* Clears sync message displayed with progress bar before contributions list became visible
*/
protected void clearSyncMessage() {
waitingMessage.setVisibility(GONE);
noDataYet.setVisibility(GONE);
}
public ListAdapter getAdapter() {
return contributionsList.getAdapter();
}
/**
* Sets adapter to contributions list. If beta mode, sets upload count for beta explicitly.
* @param adapter List adapter for uploads of contributor
*/
public void setAdapter(ListAdapter adapter) {
this.contributionsList.setAdapter(adapter);
if(BuildConfig.FLAVOR.equalsIgnoreCase("beta")){
//TODO: add betaSetUploadCount method
((ContributionsFragment) getParentFragment()).betaSetUploadCount(adapter.getCount());
}
}
public interface SourceRefresher {

View file

@ -0,0 +1,550 @@
package fr.free.nrw.commons.contributions;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.nearby.NearbyFragment;
import fr.free.nrw.commons.nearby.NearbyMapFragment;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.upload.UploadService;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
import static android.content.ContentResolver.requestSync;
import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST;
public class MainActivity extends AuthenticatedActivity implements FragmentManager.OnBackStackChangedListener {
@Inject
SessionManager sessionManager;
@BindView(R.id.tab_layout)
TabLayout tabLayout;
@BindView(R.id.pager)
public UnswipableViewPager viewPager;
@Inject
public LocationServiceManager locationManager;
@Inject
@Named("default_preferences")
public SharedPreferences prefs;
public Intent uploadServiceIntent;
public boolean isAuthCookieAcquired = false;
public ContributionsActivityPagerAdapter contributionsActivityPagerAdapter;
public final int CONTRIBUTIONS_TAB_POSITION = 0;
public final int NEARBY_TAB_POSITION = 1;
public boolean isContributionsFragmentVisible = true; // False means nearby fragment is visible
private Menu menu;
private boolean isThereUnreadNotifications = false;
private boolean onOrientationChanged = false;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contributions);
ButterKnife.bind(this);
requestAuthToken();
initDrawer();
setTitle(getString(R.string.navigation_item_home)); // Should I create a new string variable with another name instead?
if (savedInstanceState != null ) {
onOrientationChanged = true; // Will be used in nearby fragment to determine significant update of map
//If nearby map was visible, call on Tab Selected to call all nearby operations
if (savedInstanceState.getInt("viewPagerCurrentItem") == 1) {
((NearbyFragment)contributionsActivityPagerAdapter.getItem(1)).onTabSelected(onOrientationChanged);
}
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("viewPagerCurrentItem", viewPager.getCurrentItem());
}
@Override
protected void onAuthCookieAcquired(String authCookie) {
// Do a sync everytime we get here!
requestSync(sessionManager.getCurrentAccount(), BuildConfig.CONTRIBUTION_AUTHORITY, new Bundle());
uploadServiceIntent = new Intent(this, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
startService(uploadServiceIntent);
addTabsAndFragments();
isAuthCookieAcquired = true;
if (contributionsActivityPagerAdapter.getItem(0) != null) {
((ContributionsFragment)contributionsActivityPagerAdapter.getItem(0)).onAuthCookieAcquired(uploadServiceIntent);
}
}
private void addTabsAndFragments() {
contributionsActivityPagerAdapter = new ContributionsActivityPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(contributionsActivityPagerAdapter);
tabLayout.addTab(tabLayout.newTab().setText(getResources().getString(R.string.contributions_fragment)));
tabLayout.addTab(tabLayout.newTab().setText(getResources().getString(R.string.nearby_fragment)));
// Set custom view to add nearby info icon next to text
View nearbyTabLinearLayout = LayoutInflater.from(this).inflate(R.layout.custom_nearby_tab_layout, null);
View nearbyInfoPopupWindowLayout = LayoutInflater.from(this).inflate(R.layout.nearby_info_popup_layout, null);
ImageView nearbyInfo = nearbyTabLinearLayout.findViewById(R.id.nearby_info_image);
tabLayout.getTabAt(1).setCustomView(nearbyTabLinearLayout);
nearbyInfo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
/*new AlertDialog.Builder(MainActivity.this)
.setTitle(R.string.title_activity_nearby)
.setMessage(R.string.showcase_view_whole_nearby_activity)
.setCancelable(true)
.setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
.create()
.show();*/
String popupText = getResources().getString(R.string.showcase_view_whole_nearby_activity);
ViewUtil.displayPopupWindow(nearbyInfo, MainActivity.this, nearbyInfoPopupWindowLayout, popupText);
}
});
if (uploadServiceIntent != null) {
// If auth cookie already acquired notify contrib fragmnet so that it san operate auth required actions
((ContributionsFragment)contributionsActivityPagerAdapter.getItem(CONTRIBUTIONS_TAB_POSITION)).onAuthCookieAcquired(uploadServiceIntent);
}
setTabAndViewPagerSynchronisation();
}
/**
* Adds number of uploads next to tab text "Contributions" then it will look like
* "Contributions (NUMBER)"
* @param uploadCount
*/
public void setNumOfUploads(int uploadCount) {
tabLayout.getTabAt(0).setText(getResources().getString(R.string.contributions_fragment) +" "+ getResources()
.getQuantityString(R.plurals.contributions_subtitle,
uploadCount, uploadCount));
}
/**
* Normally tab layout and view pager has no relation, which means when you swipe view pager
* tab won't change and vice versa. So we have to notify each of them.
*/
private void setTabAndViewPagerSynchronisation() {
//viewPager.canScrollHorizontally(false);
viewPager.setFocusableInTouchMode(true);
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
switch (position) {
case CONTRIBUTIONS_TAB_POSITION:
Timber.d("Contributions tab selected");
tabLayout.getTabAt(CONTRIBUTIONS_TAB_POSITION).select();
isContributionsFragmentVisible = true;
updateMenuItem();
break;
case NEARBY_TAB_POSITION:
Timber.d("Nearby tab selected");
tabLayout.getTabAt(NEARBY_TAB_POSITION).select();
isContributionsFragmentVisible = false;
updateMenuItem();
// Do all permission and GPS related tasks on tab selected, not on create
((NearbyFragment)contributionsActivityPagerAdapter.getItem(1)).onTabSelected(onOrientationChanged);
break;
default:
tabLayout.getTabAt(CONTRIBUTIONS_TAB_POSITION).select();
break;
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
viewPager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
public void hideTabs() {
changeDrawerIconToBakcButton();
if (tabLayout != null) {
tabLayout.setVisibility(View.GONE);
}
}
public void showTabs() {
changeDrawerIconToDefault();
if (tabLayout != null) {
tabLayout.setVisibility(View.VISIBLE);
}
}
@Override
protected void onAuthFailure() {
}
@Override
public void onBackPressed() {
String contributionsFragmentTag = ((ContributionsActivityPagerAdapter) viewPager.getAdapter()).makeFragmentName(R.id.pager, 0);
String nearbyFragmentTag = ((ContributionsActivityPagerAdapter) viewPager.getAdapter()).makeFragmentName(R.id.pager, 1);
if (getSupportFragmentManager().findFragmentByTag(contributionsFragmentTag) != null && isContributionsFragmentVisible) {
// Meas that contribution fragment is visible (not nearby fragment)
ContributionsFragment contributionsFragment = (ContributionsFragment) getSupportFragmentManager().findFragmentByTag(contributionsFragmentTag);
if (contributionsFragment.getChildFragmentManager().findFragmentByTag(ContributionsFragment.MEDIA_DETAIL_PAGER_FRAGMENT_TAG) != null) {
// Means that media details fragment is visible to uer instead of contributions list fragment (As chils fragment)
// Then we want to go back to contributions list fragment on backbutton pressed from media detail fragment
contributionsFragment.getChildFragmentManager().popBackStack();
// Tabs were invisible when Media Details Fragment is active, make them visible again on Contrib List Fragment active
showTabs();
// Nearby Notification Card View was invisible when Media Details Fragment is active, make it visible again on Contrib List Fragment active, according to preferences
if (prefs.getBoolean("displayNearbyCardView", true)) {
contributionsFragment.nearbyNoificationCardView.setVisibility(View.VISIBLE);
} else {
contributionsFragment.nearbyNoificationCardView.setVisibility(View.GONE);
}
} else {
finish();
}
} else if (getSupportFragmentManager().findFragmentByTag(nearbyFragmentTag) != null && !isContributionsFragmentVisible) {
// Meas that nearby fragment is visible (not contributions fragment)
// Set current item to contributions activity instead of closing the activity
viewPager.setCurrentItem(0);
} else {
super.onBackPressed();
}
}
@Override
public void onBackStackChanged() {
initBackButton();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.contribution_activity_notification_menu, menu);
if (!isThereUnreadNotifications) {
// TODO: used vectors are not compatible with API 19 and below, change them
menu.findItem(R.id.notifications).setIcon(ContextCompat.getDrawable(this, R.drawable.ic_notification_white_clip_art));
} else {
menu.findItem(R.id.notifications).setIcon(ContextCompat.getDrawable(this, R.drawable.ic_notification_white_clip_art_dot));
}
this.menu = menu;
updateMenuItem();
return true;
}
/**
* Responsible with displaying required menu items according to displayed fragment.
* Notifications icon when contributions list is visible, list sheet icon when nearby is visible
*/
private void updateMenuItem() {
if (menu != null) {
if (isContributionsFragmentVisible) {
// Display notifications menu item
menu.findItem(R.id.notifications).setVisible(true);
menu.findItem(R.id.list_sheet).setVisible(false);
Timber.d("Contributions activity notifications menu item is visible");
} else {
// Display bottom list menu item
menu.findItem(R.id.notifications).setVisible(false);
menu.findItem(R.id.list_sheet).setVisible(true);
Timber.d("Contributions activity list sheet menu item is visible");
}
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.notifications:
// Starts notification activity on click to notification icon
NavigationBaseActivity.startActivityWithFlags(this, NotificationActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP);
finish();
return true;
case R.id.list_sheet:
if (contributionsActivityPagerAdapter.getItem(1) != null) {
((NearbyFragment)contributionsActivityPagerAdapter.getItem(1)).listOptionMenuIteClicked();
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private boolean deviceHasCamera() {
PackageManager pm = getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) ||
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
}
/**
* Updte notification icon if there is an unread notification
* @param isThereUnreadNotifications true if user didn't visit notifications activity since
* latest notification came to account
*/
public void updateNotificationIcon(boolean isThereUnreadNotifications) {
if (!isThereUnreadNotifications) {
this.isThereUnreadNotifications = false;
menu.findItem(R.id.notifications).setIcon(ContextCompat.getDrawable(this, R.drawable.ic_notification_white_clip_art));
} else {
this.isThereUnreadNotifications = true;
menu.findItem(R.id.notifications).setIcon(ContextCompat.getDrawable(this, R.drawable.ic_notification_white_clip_art_dot));
}
}
public class ContributionsActivityPagerAdapter extends FragmentPagerAdapter {
FragmentManager fragmentManager;
private boolean isContributionsListFragment = true; // to know what to put in first tab, Contributions of Media Details
public ContributionsActivityPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
this.fragmentManager = fragmentManager;
}
@Override
public int getCount() {
return 2;
}
/*
* Do not use getItem method to access fragments on pager adapter. User reference vairables
* instead.
* */
@Override
public Fragment getItem(int position) {
switch (position){
case 0:
ContributionsFragment retainedContributionsFragment = getContributionsFragment(0);
if (retainedContributionsFragment != null) {
/**
* ContributionsFragment is parent of ContributionsListFragment and
* MediaDetailsFragment. If below decides which child will be visible.
*/
if (isContributionsListFragment) {
retainedContributionsFragment.setContributionsListFragment();
} else {
retainedContributionsFragment.setMediaDetailPagerFragment();
}
return retainedContributionsFragment;
} else {
// If we reach here, retainedContributionsFragment is null
return new ContributionsFragment();
}
case 1:
NearbyFragment retainedNearbyFragment = getNearbyFragment(1);
if (retainedNearbyFragment != null) {
return retainedNearbyFragment;
} else {
// If we reach here, retainedNearbyFragment is null
return new NearbyFragment();
}
default:
return null;
}
}
/**
* Generates fragment tag with makeFragmentName method to get retained contributions fragment
* @param position index of tabs, in our case 0 or 1
* @return
*/
private ContributionsFragment getContributionsFragment(int position) {
String tag = makeFragmentName(R.id.pager, position);
return (ContributionsFragment)fragmentManager.findFragmentByTag(tag);
}
/**
* Generates fragment tag with makeFragmentName method to get retained nearby fragment
* @param position index of tabs, in our case 0 or 1
* @return
*/
private NearbyFragment getNearbyFragment(int position) {
String tag = makeFragmentName(R.id.pager, position);
return (NearbyFragment)fragmentManager.findFragmentByTag(tag);
}
/**
* A simple hack to use retained fragment when getID is called explicitly, if we don't use
* this method, a new fragment will be initialized on each explicit calls of getID
* @param viewId id of view pager
* @param index index of tabs, in our case 0 or 1
* @return
*/
public String makeFragmentName(int viewId, int index) {
return "android:switcher:" + viewId + ":" + index;
}
/**
* In first tab we can have ContributionsFragment or Media details fragment. This method
* is responsible to update related boolean
* @param isContributionsListFragment true when contribution fragment should be visible, false
* means user clicked to MediaDetails
*/
private void updateContributionFragmentTabContent(boolean isContributionsListFragment) {
this.isContributionsListFragment = isContributionsListFragment;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
ContributionsListFragment contributionsListFragment =
(ContributionsListFragment) contributionsActivityPagerAdapter
.getItem(0).getChildFragmentManager()
.findFragmentByTag(ContributionsFragment.CONTRIBUTION_LIST_FRAGMENT_TAG);
contributionsListFragment.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case LOCATION_REQUEST: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Timber.d("Location permission given");
} else {
// If nearby fragment is visible and location permission is not given, send user back to contrib fragment
if (!isContributionsFragmentVisible) {
viewPager.setCurrentItem(CONTRIBUTIONS_TAB_POSITION);
// TODO: If contrib fragment is visible and location permission is not given, display permission request button
} else {
}
}
return;
}
// Storage permission for gallery
case PermissionUtils.GALLERY_PERMISSION_FROM_CONTRIBUTION_LIST: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Storage permission given
ContributionsListFragment contributionsListFragment =
(ContributionsListFragment) contributionsActivityPagerAdapter
.getItem(0).getChildFragmentManager()
.findFragmentByTag(ContributionsFragment.CONTRIBUTION_LIST_FRAGMENT_TAG);
contributionsListFragment.controller.startGalleryPick();
}
return;
}
case PermissionUtils.CAMERA_PERMISSION_FROM_CONTRIBUTION_LIST: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Storage permission given
ContributionsListFragment contributionsListFragment =
(ContributionsListFragment) contributionsActivityPagerAdapter
.getItem(0).getChildFragmentManager()
.findFragmentByTag(ContributionsFragment.CONTRIBUTION_LIST_FRAGMENT_TAG);
contributionsListFragment.controller.startCameraCapture();
}
return;
}
case PermissionUtils.CAMERA_PERMISSION_FROM_NEARBY_MAP: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Storage permission given
NearbyMapFragment nearbyMapFragment =
((NearbyFragment) contributionsActivityPagerAdapter
.getItem(1)).nearbyMapFragment;
nearbyMapFragment.controller.startCameraCapture();
}
return;
}
case PermissionUtils.GALLERY_PERMISSION_FROM_NEARBY_MAP: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Storage permission given
NearbyMapFragment nearbyMapFragment =
((NearbyFragment) contributionsActivityPagerAdapter
.getItem(1)).nearbyMapFragment;
nearbyMapFragment.controller.startGalleryPick();
}
return;
}
default:
return;
}
}
@Override
protected void onDestroy() {
locationManager.unregisterLocationManager();
// Remove ourself from hashmap to prevent memory leaks
locationManager = null;
super.onDestroy();
}
}

View file

@ -0,0 +1,30 @@
package fr.free.nrw.commons.contributions;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class UnswipableViewPager extends ViewPager{
public UnswipableViewPager(@NonNull Context context) {
super(context);
}
public UnswipableViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// Unswipable
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Unswipable
return false;
}
}

View file

@ -9,14 +9,13 @@ import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.bookmarks.BookmarksActivity;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import fr.free.nrw.commons.upload.MultipleShareActivity;
import fr.free.nrw.commons.upload.ShareActivity;
import fr.free.nrw.commons.upload.UploadActivity;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
@ -29,13 +28,7 @@ public abstract class ActivityBuilderModule {
abstract WelcomeActivity bindWelcomeActivity();
@ContributesAndroidInjector
abstract ShareActivity bindShareActivity();
@ContributesAndroidInjector
abstract MultipleShareActivity bindMultipleShareActivity();
@ContributesAndroidInjector
abstract ContributionsActivity bindContributionsActivity();
abstract MainActivity bindContributionsActivity();
@ContributesAndroidInjector
abstract SettingsActivity bindSettingsActivity();
@ -46,15 +39,15 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract SignupActivity bindSignupActivity();
@ContributesAndroidInjector
abstract NearbyActivity bindNearbyActivity();
@ContributesAndroidInjector
abstract NotificationActivity bindNotificationActivity();
@ContributesAndroidInjector
abstract CategoryImagesActivity bindFeaturedImagesActivity();
@ContributesAndroidInjector
abstract UploadActivity bindUploadActivity();
@ContributesAndroidInjector
abstract SearchActivity bindSearchActivity();

View file

@ -1,10 +1,17 @@
package fr.free.nrw.commons.di;
import android.app.Activity;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v4.util.LruCache;
import android.view.inputmethod.InputMethodManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Named;
import javax.inject.Singleton;
@ -12,12 +19,14 @@ import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.UploadController;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
@ -38,6 +47,35 @@ public class CommonsApplicationModule {
return this.applicationContext;
}
@Provides
public InputMethodManager provideInputMethodManager() {
return (InputMethodManager) applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE);
}
@Provides
@Named("licenses")
public List<String> provideLicenses(Context context) {
List<String> licenseItems = new ArrayList<>();
licenseItems.add(context.getString(R.string.license_name_cc0));
licenseItems.add(context.getString(R.string.license_name_cc_by));
licenseItems.add(context.getString(R.string.license_name_cc_by_sa));
licenseItems.add(context.getString(R.string.license_name_cc_by_four));
licenseItems.add(context.getString(R.string.license_name_cc_by_sa_four));
return licenseItems;
}
@Provides
@Named("licenses_by_name")
public Map<String, String> provideLicensesByName(Context context) {
Map<String, String> byName = new HashMap<>();
byName.put(context.getString(R.string.license_name_cc0), Prefs.Licenses.CC0);
byName.put(context.getString(R.string.license_name_cc_by), Prefs.Licenses.CC_BY_3);
byName.put(context.getString(R.string.license_name_cc_by_sa), Prefs.Licenses.CC_BY_SA_3);
byName.put(context.getString(R.string.license_name_cc_by_four), Prefs.Licenses.CC_BY_4);
byName.put(context.getString(R.string.license_name_cc_by_sa_four), Prefs.Licenses.CC_BY_SA_4);
return byName;
}
@Provides
public AccountUtil providesAccountUtil(Context context) {
return new AccountUtil(context);
@ -120,6 +158,17 @@ public class CommonsApplicationModule {
return context.getSharedPreferences("direct_nearby_upload_prefs", MODE_PRIVATE);
}
/**
* Is used to determine when user is viewed notifications activity last
* @param context
* @return date of lastReadNotificationDate
*/
@Provides
@Named("last_read_notification_date")
public SharedPreferences providesLastReadNotificationDatePreferences(Context context) {
return context.getSharedPreferences("last_read_notification_date", MODE_PRIVATE);
}
@Provides
public UploadController providesUploadController(SessionManager sessionManager, @Named("default_preferences") SharedPreferences sharedPreferences, Context context) {
return new UploadController(sessionManager, context, sharedPreferences);

View file

@ -4,29 +4,25 @@ import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.category.CategoryImagesListFragment;
import fr.free.nrw.commons.category.SubCategoryListFragment;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.contributions.ContributionsListFragment;
import fr.free.nrw.commons.explore.categories.SearchCategoryFragment;
import fr.free.nrw.commons.explore.images.SearchImageFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.nearby.NearbyFragment;
import fr.free.nrw.commons.nearby.NearbyListFragment;
import fr.free.nrw.commons.nearby.NearbyMapFragment;
import fr.free.nrw.commons.nearby.NoPermissionsFragment;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.MultipleUploadListFragment;
import fr.free.nrw.commons.upload.SingleUploadFragment;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract CategorizationFragment bindCategorizationFragment();
@ContributesAndroidInjector
abstract ContributionsListFragment bindContributionsListFragment();
@ -48,12 +44,6 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract SettingsFragment bindSettingsFragment();
@ContributesAndroidInjector
abstract MultipleUploadListFragment bindMultipleUploadListFragment();
@ContributesAndroidInjector
abstract SingleUploadFragment bindSingleUploadFragment();
@ContributesAndroidInjector
abstract CategoryImagesListFragment bindFeaturedImagesListFragment();
@ -69,6 +59,12 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract RecentSearchesFragment bindRecentSearchesFragment();
@ContributesAndroidInjector
abstract ContributionsFragment bindContributionsFragment();
@ContributesAndroidInjector
abstract NearbyFragment bindNearbyFragment();
@ContributesAndroidInjector
abstract BookmarkPicturesFragment bindBookmarkPictureListFragment();

View file

@ -95,7 +95,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, rootView);
if(getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
}
else{
@ -124,7 +124,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
public void updateCategoryList(String query) {
this.query = query;
categoriesNotFoundView.setVisibility(GONE);
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
@ -173,7 +173,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
*/
private void handleSuccess(List<String> mediaList) {
queryList = mediaList;
if(mediaList == null || mediaList.isEmpty()) {
if (mediaList == null || mediaList.isEmpty()) {
initErrorView();
}
else {

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.explore.images;
import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle;
@ -97,7 +98,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, rootView);
if(getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
imagesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
}
else{
@ -123,12 +124,13 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
* Checks for internet connection and then initializes the recycler view with 25 images of the searched query
* Clearing imageAdapter every time new keyword is searched so that user can see only new results
*/
@SuppressLint("CheckResult")
public void updateImageList(String query) {
this.query = query;
if(imagesNotFoundView != null) {
if (imagesNotFoundView != null) {
imagesNotFoundView.setVisibility(GONE);
}
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
@ -146,6 +148,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
/**
* Adds more results to existing search results
*/
@SuppressLint("CheckResult")
public void addImagesToList(String query) {
this.query = query;
progressBar.setVisibility(View.VISIBLE);
@ -163,13 +166,11 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
*/
private void handlePaginationSuccess(List<Media> mediaList) {
progressBar.setVisibility(View.GONE);
if (mediaList.size()!=0){
if (!queryList.get(queryList.size()-1).getFilename().equals(mediaList.get(mediaList.size()-1).getFilename())) {
queryList.addAll(mediaList);
imagesAdapter.addAll(mediaList);
imagesAdapter.notifyDataSetChanged();
((SearchActivity)getContext()).viewPagerNotifyDataSetChanged();
}
if (mediaList.size() != 0 || !queryList.get(queryList.size() - 1).getFilename().equals(mediaList.get(mediaList.size() - 1).getFilename())) {
queryList.addAll(mediaList);
imagesAdapter.addAll(mediaList);
imagesAdapter.notifyDataSetChanged();
((SearchActivity) getContext()).viewPagerNotifyDataSetChanged();
}
}
@ -182,7 +183,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
*/
private void handleSuccess(List<Media> mediaList) {
queryList = mediaList;
if(mediaList == null || mediaList.isEmpty()) {
if (mediaList == null || mediaList.isEmpty()) {
initErrorView();
}
else {

View file

@ -26,12 +26,13 @@ public class LocationServiceManager implements LocationListener {
private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 100;
private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10;
private Context context;
public Context context;
private LocationManager locationManager;
private Location lastLocation;
//private Location lastLocationDuplicate; // Will be used for nearby card view on contributions activity
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
private boolean isLocationManagerRegistered = false;
private Set<Activity> locationExplanationDisplayed = new HashSet<>();
public Set<Activity> locationExplanationDisplayed = new HashSet<>();
/**
* Constructs a new instance of LocationServiceManager.
@ -253,15 +254,25 @@ public class LocationServiceManager implements LocationListener {
@Override
public void onLocationChanged(Location location) {
Timber.d("on location changed");
if (isBetterLocation(location, lastLocation)
.equals(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) {
lastLocation = location;
//lastLocationDuplicate = location;
for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChangedSignificantly(LatLng.from(lastLocation));
}
} else if (isBetterLocation(location, lastLocation)
} else if (location.distanceTo(lastLocation) >= 500) {
// Update nearby notification card at every 500 meters.
for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChangedMedium(LatLng.from(lastLocation));
}
}
else if (isBetterLocation(location, lastLocation)
.equals(LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) {
lastLocation = location;
//lastLocationDuplicate = location;
for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChangedSlightly(LatLng.from(lastLocation));
}
@ -286,6 +297,7 @@ public class LocationServiceManager implements LocationListener {
public enum LocationChangeType{
LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers
LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving
LOCATION_MEDIUM_CHANGED, //Between slight and significant changes, will be used for nearby card view updates.
LOCATION_NOT_CHANGED,
PERMISSION_JUST_GRANTED,
MAP_UPDATED

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.location;
public interface LocationUpdateListener {
void onLocationChangedSignificantly(LatLng latLng);
void onLocationChangedSlightly(LatLng latLng);
void onLocationChangedSignificantly(LatLng latLng); // Will be used to update all nearby markers on the map
void onLocationChangedSlightly(LatLng latLng); // Will be used to track users motion
void onLocationChangedMedium(LatLng latLng); // Will be used updating nearby card view notification
}

View file

@ -16,7 +16,7 @@ public final class LogUtils {
* @return
*/
public static String getLogDirectory(boolean isBeta) {
if(isBeta) {
if (isBeta) {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/beta";
} else {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/prod";

View file

@ -49,6 +49,7 @@ import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.delete.DeleteTask;
import fr.free.nrw.commons.delete.ReasonBuilder;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
@ -156,7 +157,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity();
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) (getParentFragment().getParentFragment());
if (savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable");
@ -222,12 +223,14 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
};
view.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener);
locale = getResources().getConfiguration().locale;
return view;
}
@Override
public void onResume() {
super.onResume();
((ContributionsFragment)(getParentFragment().getParentFragment())).nearbyNoificationCardView.setVisibility(View.GONE);
media = detailProvider.getMediaAtPosition(index);
if (media == null) {
// Ask the detail provider to ping us when we're ready
@ -271,7 +274,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@Override
protected Boolean doInBackground(Void... voids) {
// Local files have no filename yet
if(media.getFilename() == null) {
if (media.getFilename() == null) {
return Boolean.FALSE;
}
try {
@ -343,7 +346,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
rebuildCatList();
if(media.getCreator() == null || media.getCreator().equals("")) {
if (media.getCreator() == null || media.getCreator().equals("")) {
authorLayout.setVisibility(GONE);
} else {
author.setText(media.getCreator());
@ -357,7 +360,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (!TextUtils.isEmpty(licenseLink(media))) {
openWebBrowser(licenseLink(media));
} else {
if(isCategoryImage) {
if (isCategoryImage) {
Timber.d("Unable to fetch license URL for %s", media.getLicense());
} else {
Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT);
@ -427,14 +430,14 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@OnClick(R.id.seeMore)
public void onSeeMoreClicked(){
if(nominatedForDeletion.getVisibility()== VISIBLE) {
if (nominatedForDeletion.getVisibility()== VISIBLE) {
openWebBrowser(media.getFilePageTitle().getMobileUri().toString());
}
}
private void enableDeleteButton(boolean visibility) {
delete.setEnabled(visibility);
if(visibility) {
if (visibility) {
delete.setTextColor(getResources().getColor(R.color.primaryTextColor));
} else {
delete.setTextColor(getResources().getColor(R.color.deleteButtonLight));
@ -444,15 +447,26 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private void rebuildCatList() {
categoryContainer.removeAllViews();
// @fixme add the category items
for (String cat : categoryNames) {
View catLabel = buildCatLabel(cat, categoryContainer);
//As per issue #1826(see https://github.com/commons-app/apps-android-commons/issues/1826), some categories come suffixed with strings prefixed with |. As per the discussion
//that was meant for alphabetical sorting of the categories and can be safely removed.
for (int i = 0; i < categoryNames.size(); i++) {
String categoryName = categoryNames.get(i);
//Removed everything after '|'
int indexOfPipe = categoryName.indexOf('|');
if (indexOfPipe != -1) {
categoryName = categoryName.substring(0, indexOfPipe);
//Set the updated category to the list as well
categoryNames.set(i, categoryName);
}
View catLabel = buildCatLabel(categoryName, categoryContainer);
categoryContainer.addView(catLabel);
}
}
private View buildCatLabel(final String catName, ViewGroup categoryContainer) {
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false);
final CompatTextView textView = (CompatTextView) item.findViewById(R.id.mediaDetailCategoryItemText);
final CompatTextView textView = item.findViewById(R.id.mediaDetailCategoryItemText);
textView.setText(catName);
if (categoriesLoaded && categoriesPresent) {

View file

@ -38,7 +38,6 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
@ -99,7 +98,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
pager.setAdapter(adapter);
pager.setCurrentItem(pageNumber, false);
if(getActivity() == null) {
if (getActivity() == null) {
Timber.d("Returning as activity is destroyed!");
return;
}
@ -133,11 +132,11 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if(getActivity() == null) {
if (getActivity() == null) {
Timber.d("Returning as activity is destroyed!");
return true;
}
MediaDetailProvider provider = (MediaDetailProvider) getActivity();
MediaDetailProvider provider = (MediaDetailProvider) getParentFragment();
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
switch (item.getItemId()) {
case R.id.menu_bookmark_current_image:
@ -156,7 +155,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
viewIntent.setAction(ACTION_VIEW);
viewIntent.setData(m.getFilePageTitle().getMobileUri());
//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);
@ -174,12 +173,12 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
return true;
case R.id.menu_retry_current_image:
// Retry
((ContributionsActivity) getActivity()).retryUpload(pager.getCurrentItem());
//((MainActivity) getActivity()).retryUpload(pager.getCurrentItem());
getActivity().getSupportFragmentManager().popBackStack();
return true;
case R.id.menu_cancel_current_image:
// todo: delete image
((ContributionsActivity) getActivity()).deleteUpload(pager.getCurrentItem());
//((MainActivity) getActivity()).deleteUpload(pager.getCurrentItem());
getActivity().getSupportFragmentManager().popBackStack();
return true;
default:
@ -193,7 +192,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
* @param media
*/
private void setWallpaper(Media media) {
if(media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
Timber.d("Media URL not present");
return;
}
@ -254,7 +253,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
menu.clear(); // see http://stackoverflow.com/a/8495697/17865
inflater.inflate(R.menu.fragment_image_detail, menu);
if (pager != null) {
MediaDetailProvider provider = (MediaDetailProvider) getActivity();
MediaDetailProvider provider = (MediaDetailProvider) getParentFragment();
if(provider == null) {
return;
}
@ -326,7 +325,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
@Override
public void onPageScrolled(int i, float v, int i2) {
if(getActivity() == null) {
if(getParentFragment().getActivity() == null) {
Timber.d("Returning as activity is destroyed!");
return;
}
@ -347,7 +346,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
e.printStackTrace();
}
}
getActivity().supportInvalidateOptionsMenu();
getParentFragment().getActivity().supportInvalidateOptionsMenu();
}
@Override
@ -381,22 +380,22 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
public Fragment getItem(int i) {
if (i == 0) {
// See bug https://code.google.com/p/android/issues/detail?id=27526
if(getActivity() == null) {
if(getParentFragment().getActivity() == null) {
Timber.d("Skipping getItem. Returning as activity is destroyed!");
return null;
}
pager.postDelayed(() -> getActivity().supportInvalidateOptionsMenu(), 5);
pager.postDelayed(() -> getParentFragment().getActivity().supportInvalidateOptionsMenu(), 5);
}
return MediaDetailFragment.forMedia(i, editable, isFeaturedImage);
}
@Override
public int getCount() {
if(getActivity() == null) {
if (getActivity() == null) {
Timber.d("Skipping getCount. Returning as activity is destroyed!");
return 0;
}
return ((MediaDetailProvider) getActivity()).getTotalMediaCount();
return ((MediaDetailProvider) getParentFragment()).getTotalMediaCount();
}
}
}

View file

@ -244,9 +244,9 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
Timber.d("Central auth token isn't valid. Trying to fetch a fresh token");
api.removeAllCookies();
String loginResultCode = login(AccountUtil.getUserName(context), AccountUtil.getPassword(context));
if(loginResultCode.equals("PASS")) {
if (loginResultCode.equals("PASS")) {
return getCentralAuthToken();
} else if(loginResultCode.equals("2FA")) {
} else if (loginResultCode.equals("2FA")) {
Timber.e("Cannot refresh session for 2FA enabled user. Login required");
} else {
Timber.e("Error occurred in refreshing session. Error code is %s", loginResultCode);

View file

@ -58,7 +58,7 @@ public class CustomMwApi {
}
public String getAuthCookie() {
if(authCookie == null){
if (authCookie == null){
authCookie = "";
List<Cookie> cookies = client.getCookieStore().getCookies();
for(Cookie cookie: cookies) {
@ -102,14 +102,14 @@ public class CustomMwApi {
}
public String getUserID() throws IOException {
if(this.userID == null || this.userID.equals("0")) {
if (this.userID == null || this.userID.equals("0")) {
this.validateLogin();
}
return userID;
}
public String getUserName() throws IOException {
if(this.userID == null || this.userID.equals("0")) {
if (this.userID == null || this.userID.equals("0")) {
this.validateLogin();
}
return userName;
@ -122,7 +122,7 @@ public class CustomMwApi {
String token = tokenData.getString("/api/login/@token");
CustomApiResult confirmData = this.action("login").param("lgname", username).param("lgpassword", password).param("lgtoken", token).post();
String finalResult = confirmData.getString("/api/login/@result");
if(finalResult.equals("Success")) {
if (finalResult.equals("Success")) {
isLoggedIn = true;
}
return finalResult;
@ -149,7 +149,7 @@ public class CustomMwApi {
.data("comment", comment)
.data("filename", filename)
.sendProgressListener(uploadProgressListener);
if(length != -1) {
if (length != -1) {
builder.file("file", filename, file, length);
} else {
builder.file("file", filename, file);

View file

@ -4,9 +4,11 @@ import android.os.Build;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.utils.PermissionUtils;
import timber.log.Timber;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
@ -24,8 +26,9 @@ class DirectUpload {
}
// These permission requests will be handled by the Fragments.
// Do not use requestCode 1 as it will conflict with NearbyActivity's requestCodes
// Do not use requestCode 1 as it will conflict with NearbyFragment's requestCodes
void initiateGalleryUpload() {
Log.d("deneme7","initiateGalleryUpload");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(fragment.getActivity(), READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
if (fragment.shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE)) {
@ -33,40 +36,42 @@ class DirectUpload {
.setMessage(fragment.getActivity().getString(R.string.read_storage_permission_rationale))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
Timber.d("Requesting permissions for read external storage");
fragment.requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, 4);
fragment.getActivity().requestPermissions
(new String[]{READ_EXTERNAL_STORAGE}, PermissionUtils.GALLERY_PERMISSION_FROM_NEARBY_MAP);
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
.create()
.show();
} else {
fragment.requestPermissions(new String[]{READ_EXTERNAL_STORAGE},
4);
fragment.getActivity().requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, PermissionUtils.GALLERY_PERMISSION_FROM_NEARBY_MAP);
}
} else {
controller.startGalleryPick();
controller.startSingleGalleryPick();
}
}
else {
controller.startGalleryPick();
controller.startSingleGalleryPick();
}
}
void initiateCameraUpload() {
Log.d("deneme7","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(android.R.string.ok, (dialog, which) -> {
fragment.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 5);
fragment.getActivity().requestPermissions
(new String[]{WRITE_EXTERNAL_STORAGE}, PermissionUtils.CAMERA_PERMISSION_FROM_NEARBY_MAP);
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
.create()
.show();
} else {
fragment.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 5);
fragment.getActivity().requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, PermissionUtils.CAMERA_PERMISSION_FROM_NEARBY_MAP);
}
} else {
controller.startCameraCapture();

View file

@ -39,6 +39,7 @@ public class NearbyController {
this.prefs = prefs;
}
/**
* Prepares Place list to make their distance information update later.
*
@ -46,15 +47,15 @@ public class NearbyController {
* @return NearbyPlacesInfo a variable holds Place list without distance information
* and boundary coordinates of current Place List
*/
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) throws IOException {
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng, boolean returnClosestResult) throws IOException {
Timber.d("Loading attractions near %s", curLatLng);
NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo();
if (curLatLng == null) {
Timber.d("Loading attractions neari, but curLatLng is null");
return null;
}
List<Place> places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage());
List<Place> places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage(), returnClosestResult);
if (null != places && places.size() > 0) {
LatLng[] boundaryCoordinates = {places.get(0).location, // south
@ -168,7 +169,7 @@ public class NearbyController {
}
public class NearbyPlacesInfo {
List<Place> placeList; // List of nearby places
LatLng[] boundaryCoordinates; // Corners of nearby area
public List<Place> placeList; // List of nearby places
public LatLng[] boundaryCoordinates; // Corners of nearby area
}
}

View file

@ -5,20 +5,19 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
@ -33,11 +32,11 @@ import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
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;
@ -47,23 +46,18 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import uk.co.deanwild.materialshowcaseview.IShowcaseListener;
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED;
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener,
WikidataEditListener.WikidataP18EditListener {
private static final int LOCATION_REQUEST = 1;
public class NearbyFragment extends CommonsDaggerSupportFragment
implements LocationUpdateListener,
WikidataEditListener.WikidataP18EditListener {
@BindView(R.id.progressBar)
ProgressBar progressBar;
@BindView(R.id.bottom_sheet)
LinearLayout bottomSheet;
@BindView(R.id.bottom_sheet_details)
@ -77,59 +71,127 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
LocationServiceManager locationManager;
@Inject
NearbyController nearbyController;
@Inject WikidataEditListener wikidataEditListener;
@Inject
@Named("application_preferences") SharedPreferences applicationPrefs;
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
WikidataEditListener wikidataEditListener;
@Inject
@Named("application_preferences")
SharedPreferences applicationPrefs;
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 View listButton; // Reference to list button to use in tutorial
private Bundle bundle;
private BottomSheetBehavior bottomSheetBehavior; // Behavior for list bottom sheet
private BottomSheetBehavior bottomSheetBehaviorForDetails; // Behavior for details bottom sheet
private LatLng curLatLng;
private Disposable placesDisposable;
private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed
public View view;
private Snackbar snackbar;
private LatLng lastKnownLocation;
private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
private BroadcastReceiver broadcastReceiver;
private boolean isListShowcaseAdded = false;
private boolean isMapShowCaseAdded = false;
private LatLng lastKnownLocation;
private MaterialShowcaseView secondSingleShowCaseView;
private boolean onOrientationChanged = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nearby);
ButterKnife.bind(this);
resumeFragment();
bundle = new Bundle();
initBottomSheetBehaviour();
initDrawer();
wikidataEditListener.setAuthenticationStateListener(this);
setRetainInstance(true);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_nearby, container, false);
ButterKnife.bind(this, view);
/*// Resume the fragment if exist
resumeFragment();*/
bundle = new Bundle();
initBottomSheetBehaviour();
wikidataEditListener.setAuthenticationStateListener(this);
this.view = view;
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState != null) {
onOrientationChanged = true;
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
}
/**
* Hide or expand bottom sheet according to states of all sheets
*/
public void listOptionMenuIteClicked() {
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);
}
}
/**
* Resume fragments if they exists
*/
private void resumeFragment() {
// Find the retained fragment on activity restarts
nearbyMapFragment = getMapFragment();
nearbyListFragment = getListFragment();
}
/**
* Returns the map fragment added to child fragment manager previously, if exists.
*/
private NearbyMapFragment getMapFragment() {
return (NearbyMapFragment) getChildFragmentManager().findFragmentByTag(TAG_RETAINED_MAP_FRAGMENT);
}
private void removeMapFragment() {
if (nearbyMapFragment != null) {
android.support.v4.app.FragmentManager fm = getFragmentManager();
fm.beginTransaction().remove(nearbyMapFragment).commit();
nearbyMapFragment = null;
}
}
/**
* Returns the list fragment added to child fragment manager previously, if exists.
*/
private NearbyListFragment getListFragment() {
return (NearbyListFragment) getChildFragmentManager().findFragmentByTag(TAG_RETAINED_LIST_FRAGMENT);
}
private void removeListFragment() {
if (nearbyListFragment != null) {
android.support.v4.app.FragmentManager fm = getFragmentManager();
fm.beginTransaction().remove(nearbyListFragment).commit();
nearbyListFragment = null;
}
}
/**
* Initialize bottom sheet behaviour (sheet for map list.) Set height 9/16 of all window.
* Add callback for bottom sheet changes, so that we can sync it with bottom sheet for details
* (sheet for nearby details)
*/
private void initBottomSheetBehaviour() {
transparentView.setAlpha(0);
bottomSheet.getLayoutParams().height = getWindowManager()
bottomSheet.getLayoutParams().height = getActivity().getWindowManager()
.getDefaultDisplay().getHeight() / 16 * 9;
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
// TODO initProperBottomSheetBehavior();
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
@ -148,257 +210,44 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_nearby, menu);
new Handler().post(() -> {
listButton = findViewById(R.id.action_display_list);
secondSingleShowCaseView = new MaterialShowcaseView.Builder(this)
.setTarget(listButton)
.setDismissText(getString(R.string.showcase_view_got_it_button))
.setContentText(getString(R.string.showcase_view_list_icon))
.setDelay(500) // optional but starting animations immediately in onCreate can make them choppy
.singleUse(ViewUtil.SHOWCASE_VIEW_ID_1) // provide a unique ID used to ensure it is only shown once
.setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD))
.setListener(new IShowcaseListener() {
@Override
public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) {
}
// If dismissed, we can inform fragment to start showcase sequence there
@Override
public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) {
nearbyMapFragment.onNearbyMaterialShowcaseDismissed();
}
})
.build();
isListShowcaseAdded = true;
if (isMapShowCaseAdded) { // If map showcase is also ready, start ShowcaseSequence
// Probably this case is not possible. Just added to be careful
setMapViewTutorialShowCase();
}
});
return super.onCreateOptionsMenu(menu);
public void prepareViewsForSheetPosition(int bottomSheetState) {
// TODO
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.action_display_list:
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);
}
}
private void requestLocationPermissions() {
if (!isFinishing()) {
locationManager.requestPermissions(this);
}
public void onLocationChangedSignificantly(LatLng latLng) {
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case LOCATION_REQUEST: {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Timber.d("Location permission granted, refreshing view");
//Still need to check if GPS is enabled
checkGps();
lastKnownLocation = locationManager.getLKL();
refreshView(PERMISSION_JUST_GRANTED);
} else {
//If permission not granted, go to page that says Nearby Places cannot be displayed
hideProgressBar();
showLocationPermissionDeniedErrorDialog();
}
}
break;
default:
// This is needed to allow the request codes from the Fragments to be routed appropriately
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
public void onLocationChangedSlightly(LatLng latLng) {
refreshView(LOCATION_SLIGHTLY_CHANGED);
}
private void showLocationPermissionDeniedErrorDialog() {
new AlertDialog.Builder(this)
.setMessage(R.string.nearby_needs_permissions)
.setCancelable(false)
.setPositiveButton(R.string.give_permission, (dialog, which) -> {
//will ask for the location permission again
checkGps();
})
.setNegativeButton(R.string.cancel, (dialog, which) -> {
//dismiss dialog and finish activity
dialog.cancel();
finish();
})
.create()
.show();
}
private void checkGps() {
if (!locationManager.isProviderEnabled()) {
Timber.d("GPS is not enabled");
new AlertDialog.Builder(this)
.setMessage(R.string.gps_disabled)
.setCancelable(false)
.setPositiveButton(R.string.enable_gps,
(dialog, id) -> {
Intent callGPSSettingIntent = new Intent(
android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
Timber.d("Loaded settings page");
startActivityForResult(callGPSSettingIntent, 1);
})
.setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> {
showLocationPermissionDeniedErrorDialog();
dialog.cancel();
})
.create()
.show();
} else {
Timber.d("GPS is enabled");
checkLocationPermission();
}
}
private void checkLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (locationManager.isLocationPermissionGranted()) {
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
} else {
// Should we show an explanation?
if (locationManager.isPermissionExplanationRequired(this)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
new AlertDialog.Builder(this)
.setMessage(getString(R.string.location_permission_rationale_nearby))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
requestLocationPermissions();
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
showLocationPermissionDeniedErrorDialog();
dialog.cancel();
})
.create()
.show();
} else {
// No explanation needed, we can request the permission.
requestLocationPermissions();
}
}
} else {
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
@Override
public void onLocationChangedMedium(LatLng latLng) {
// For nearby map actions, there are no differences between 500 meter location change (aka medium change) and slight change
refreshView(LOCATION_SLIGHTLY_CHANGED);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1) {
Timber.d("User is back from Settings page");
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
public void onWikidataEditSuccessful() {
refreshView(MAP_UPDATED);
}
@Override
protected void onStart() {
super.onStart();
locationManager.addLocationListener(this);
registerLocationUpdates();
}
@Override
protected void onStop() {
super.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (placesDisposable != null) {
placesDisposable.dispose();
}
}
@Override
protected void onResume() {
super.onResume();
lockNearbyView = false;
checkGps();
addNetworkBroadcastReceiver();
}
@Override
public void onPause() {
super.onPause();
// this means that this activity will not be recreated now, user is leaving it
// or the activity is otherwise finishing
if(isFinishing()) {
// we will not need this fragment anymore, this may also be a good place to signal
// 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(LOCATION_SIGNIFICANTLY_CHANGED);
} else {
ViewUtil.showLongToast(NearbyActivity.this, getString(R.string.no_internet));
}
}
};
this.registerReceiver(broadcastReceiver, intentFilter);
}
/**
* This method should be the single point to load/refresh nearby places
*
* @param locationChangeType defines if location shanged significantly or slightly
*/
private void refreshView(LocationChangeType locationChangeType) {
private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) {
Timber.d("Refreshing nearby places");
if (lockNearbyView) {
return;
}
if (!NetworkUtils.isInternetConnectionEstablished(this)) {
if (!NetworkUtils.isInternetConnectionEstablished(getActivity())) {
hideProgressBar();
return;
}
@ -408,7 +257,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
if (curLatLng != null && curLatLng.equals(lastLocation)
&& !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed
return;
if (!onOrientationChanged) {
return;
}
}
curLatLng = lastLocation;
@ -421,9 +272,15 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
return;
}
/*
onOrientation changed is true whenever activities orientation changes. After orientation
change we want to refresh map significantly, doesn't matter if location changed significantly
or not. Thus, we included onOrientatinChanged boolean to if clause
*/
if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)
|| locationChangeType.equals(PERMISSION_JUST_GRANTED)
|| locationChangeType.equals(MAP_UPDATED)) {
|| locationChangeType.equals(MAP_UPDATED)
|| onOrientationChanged) {
progressBar.setVisibility(View.VISIBLE);
//TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found
@ -435,7 +292,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
bundle.putString("CurLatLng", gsonCurLatLng);
placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng))
.loadAttractionsFromLocation(curLatLng, false))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces,
@ -455,40 +312,8 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
}
}
/**
* This method first checks if the location permissions has been granted and then register the location manager for updates.
*/
private void registerLocationUpdates() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (locationManager.isLocationPermissionGranted()) {
locationManager.registerLocationManager();
} else {
// Should we show an explanation?
if (locationManager.isPermissionExplanationRequired(this)) {
new AlertDialog.Builder(this)
.setMessage(getString(R.string.location_permission_rationale_nearby))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
requestLocationPermissions();
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
showLocationPermissionDeniedErrorDialog();
dialog.cancel();
})
.create()
.show();
} else {
// No explanation needed, we can request the permission.
requestLocationPermissions();
}
}
} else {
locationManager.registerLocationManager();
}
}
private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
Timber.d("Populating nearby places");
List<Place> placeList = nearbyPlacesInfo.placeList;
LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates;
Gson gson = new GsonBuilder()
@ -499,7 +324,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates);
if (placeList.size() == 0) {
ViewUtil.showSnackbar(findViewById(R.id.container), R.string.no_nearby);
ViewUtil.showSnackbar(view.findViewById(R.id.container), R.string.no_nearby);
}
bundle.putString("PlaceList", gsonPlaceList);
@ -520,47 +345,13 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
updateMapFragment(false);
updateListFragment();
}
isMapShowCaseAdded = true;
}
public void setMapViewTutorialShowCase() {
/*
*This showcase view will be the first step of our nearbyMaterialShowcaseSequence. The reason we use a
* single item instead of adding another step to nearbyMaterialShowcaseSequence is that we are not able to
* call withoutShape() method on steps. For mapView we need an showcase view without
* any circle on it, it should cover the whole page.
* */
MaterialShowcaseView firstSingleShowCaseView = new MaterialShowcaseView.Builder(this)
.setTarget(nearbyMapFragment.mapView)
.setDismissText(getString(R.string.showcase_view_got_it_button))
.setContentText(getString(R.string.showcase_view_whole_nearby_activity))
.setDelay(500) // optional but starting animations immediately in onCreate can make them choppy
.singleUse(ViewUtil.SHOWCASE_VIEW_ID_2) // provide a unique ID used to ensure it is only shown once
.withoutShape() // no shape on map view since there are no view to focus on
.setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD))
.setListener(new IShowcaseListener() {
@Override
public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) {
}
@Override
public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) {
/* Add other nearbyMaterialShowcaseSequence here, it will make the user feel as they are a
* nearbyMaterialShowcaseSequence whole together.
* */
secondSingleShowCaseView.show(NearbyActivity.this);
}
})
.build();
if (applicationPrefs.getBoolean("firstRunNearby", true)) {
applicationPrefs.edit().putBoolean("firstRunNearby", false).apply();
firstSingleShowCaseView.show(this);
}
}
/**
* Lock nearby view updates while updating map or list. Because we don't want new update calls
* when we already updating for old location update.
* @param lock
*/
private void lockNearbyView(boolean lock) {
if (lock) {
lockNearbyView = true;
@ -573,53 +364,22 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
}
}
private void hideProgressBar() {
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
}
}
private NearbyMapFragment getMapFragment() {
return (NearbyMapFragment) getSupportFragmentManager().findFragmentByTag(TAG_RETAINED_MAP_FRAGMENT);
}
private void removeMapFragment() {
if (nearbyMapFragment != null) {
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
fm.beginTransaction().remove(nearbyMapFragment).commit();
nearbyMapFragment = null;
}
}
private NearbyListFragment getListFragment() {
return (NearbyListFragment) getSupportFragmentManager().findFragmentByTag(TAG_RETAINED_LIST_FRAGMENT);
}
private void removeListFragment() {
if (nearbyListFragment != null) {
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
fm.beginTransaction().remove(nearbyListFragment).commit();
nearbyListFragment = null;
}
}
private void updateMapFragment(boolean isSlightUpdate) {
/*
* Significant update means updating nearby place markers. Slightly update means only
* updating current location marker and camera target.
* We update our map Significantly on each 1000 meter change, but we can't never know
* the frequency of nearby places. Thus we check if we are close to the boundaries of
* our nearby markers, we update our map Significantly.
* */
Significant update means updating nearby place markers. Slightly update means only
updating current location marker and camera target.
We update our map Significantly on each 1000 meter change, but we can't never know
the frequency of nearby places. Thus we check if we are close to the boundaries of
our nearby markers, we update our map Significantly.
*/
NearbyMapFragment nearbyMapFragment = getMapFragment();
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 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
&& (curLatLng.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude()
|| curLatLng.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude()
@ -627,7 +387,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|| curLatLng.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) {
// populate places
placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng))
.loadAttractionsFromLocation(curLatLng, false))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces,
@ -642,6 +402,15 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
return;
}
/*
If this is the map update just after orientation change, then it is not a slight update
anymore. We want to significantly update map after each orientation change
*/
if (onOrientationChanged) {
isSlightUpdate = false;
onOrientationChanged = false;
}
if (isSlightUpdate) {
nearbyMapFragment.setBundleForUpdtes(bundle);
nearbyMapFragment.updateMapSlightly();
@ -668,7 +437,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
* Calls fragment for map view.
*/
private void setMapFragment() {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction();
nearbyMapFragment = new NearbyMapFragment();
nearbyMapFragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container, nearbyMapFragment, TAG_RETAINED_MAP_FRAGMENT);
@ -679,7 +448,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
* Calls fragment for list view.
*/
private void setListFragment() {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction();
nearbyListFragment = new NearbyListFragment();
nearbyListFragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container_sheet, nearbyListFragment, TAG_RETAINED_LIST_FRAGMENT);
@ -688,26 +457,235 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
fragmentTransaction.commitAllowingStateLoss();
}
@Override
public void onLocationChangedSignificantly(LatLng latLng) {
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
private void hideProgressBar() {
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
}
}
@Override
public void onLocationChangedSlightly(LatLng latLng) {
refreshView(LOCATION_SLIGHTLY_CHANGED);
/**
* This method first checks if the location permissions has been granted and then register the location manager for updates.
*/
private void registerLocationUpdates() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (locationManager.isLocationPermissionGranted()) {
locationManager.registerLocationManager();
} else {
// Should we show an explanation?
if (locationManager.isPermissionExplanationRequired(getActivity())) {
new AlertDialog.Builder(getActivity())
.setMessage(getString(R.string.location_permission_rationale_nearby))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
requestLocationPermissions();
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
showLocationPermissionDeniedErrorDialog();
dialog.cancel();
})
.create()
.show();
} else {
// No explanation needed, we can request the permission.
requestLocationPermissions();
}
}
} else {
locationManager.registerLocationManager();
}
}
public void prepareViewsForSheetPosition(int bottomSheetState) {
// TODO
private void requestLocationPermissions() {
if (!getActivity().isFinishing()) {
locationManager.requestPermissions(getActivity());
}
}
private void showLocationPermissionDeniedErrorDialog() {
new AlertDialog.Builder(getActivity())
.setMessage(R.string.nearby_needs_permissions)
.setCancelable(false)
.setPositiveButton(R.string.give_permission, (dialog, which) -> {
//will ask for the location permission again
checkGps();
})
.setNegativeButton(R.string.cancel, (dialog, which) -> {
//dismiss dialog and send user to contributions tab instead
dialog.cancel();
((MainActivity)getActivity()).viewPager.setCurrentItem(((MainActivity)getActivity()).CONTRIBUTIONS_TAB_POSITION);
})
.create()
.show();
}
/**
* Checks device GPS permission first for all API levels
*/
private void checkGps() {
Timber.d("checking GPS");
if (!locationManager.isProviderEnabled()) {
Timber.d("GPS is not enabled");
new AlertDialog.Builder(getActivity())
.setMessage(R.string.gps_disabled)
.setCancelable(false)
.setPositiveButton(R.string.enable_gps,
(dialog, id) -> {
Intent callGPSSettingIntent = new Intent(
android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
Timber.d("Loaded settings page");
startActivityForResult(callGPSSettingIntent, 1);
})
.setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> {
showLocationPermissionDeniedErrorDialog();
dialog.cancel();
})
.create()
.show();
} else {
Timber.d("GPS is enabled");
checkLocationPermission();
}
}
/**
* This method ideally should be called from inside of CheckGPS method. If device GPS is enabled
* then we need to control app specific permissions for >=M devices. For other devices, enabled
* GPS is enough for nearby, so directly call refresh view.
*/
private void checkLocationPermission() {
Timber.d("Checking location permission");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (locationManager.isLocationPermissionGranted()) {
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
} else {
// Should we show an explanation?
if (locationManager.isPermissionExplanationRequired(getActivity())) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
new AlertDialog.Builder(getActivity())
.setMessage(getString(R.string.location_permission_rationale_nearby))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
requestLocationPermissions();
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
showLocationPermissionDeniedErrorDialog();
dialog.cancel();
})
.create()
.show();
} else {
// No explanation needed, we can request the permission.
requestLocationPermissions();
}
}
} else {
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
}
private void showErrorMessage(String message) {
ViewUtil.showLongToast(NearbyActivity.this, message);
ViewUtil.showLongToast(getActivity(), message);
}
private void addNetworkBroadcastReceiver() {
IntentFilter intentFilter = new IntentFilter(NETWORK_INTENT_ACTION);
snackbar = Snackbar.make(transparentView , R.string.no_internet, Snackbar.LENGTH_INDEFINITE);
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (snackbar != null) {
if (NetworkUtils.isInternetConnectionEstablished(getActivity())) {
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
snackbar.dismiss();
} else {
snackbar.show();
}
}
}
};
getActivity().registerReceiver(broadcastReceiver, intentFilter);
}
@Override
public void onWikidataEditSuccessful() {
refreshView(MAP_UPDATED);
public void onResume() {
super.onResume();
// Resume the fragment if exist
resumeFragment();
}
public void onTabSelected(boolean onOrientationChanged) {
Timber.d("On nearby tab selected");
this.onOrientationChanged = onOrientationChanged;
performNearbyOperations();
}
/**
* Calls nearby operations in required order.
*/
private void performNearbyOperations() {
locationManager.addLocationListener(this);
registerLocationUpdates();
lockNearbyView = false;
checkGps();
addNetworkBroadcastReceiver();
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
}
@Override
public void onDestroy() {
super.onDestroy();
if (placesDisposable != null) {
placesDisposable.dispose();
}
}
@Override
public void onDetach() {
super.onDetach();
snackbar = null;
broadcastReceiver = null;
}
@Override
public void onStart() {
super.onStart();
}
@Override
public void onPause() {
super.onPause();
// this means that this activity will not be recreated now, user is leaving it
// or the activity is otherwise finishing
if(getActivity().isFinishing()) {
// we will not need this fragment anymore, this may also be a good place to signal
// to the retained fragment object to perform its own cleanup.
//removeMapFragment();
removeListFragment();
}
if (broadcastReceiver != null) {
getActivity().unregisterReceiver(broadcastReceiver);
broadcastReceiver = null;
}
if (locationManager != null) {
locationManager.removeLocationListener(this);
locationManager.unregisterLocationManager();
}
}
}

View file

@ -73,7 +73,7 @@ public class NearbyListFragment extends DaggerFragment {
ViewGroup container,
Bundle savedInstanceState) {
Timber.d("NearbyListFragment created");
View view = inflater.inflate(R.layout.fragment_nearby, container, false);
View view = inflater.inflate(R.layout.fragment_nearby_list, container, false);
recyclerView = view.findViewById(R.id.listView);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));

View file

@ -7,7 +7,6 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
@ -16,6 +15,7 @@ import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@ -109,7 +109,7 @@ public class NearbyMapFragment extends DaggerFragment {
private Animation fab_close;
private Animation fab_open;
private Animation rotate_forward;
private ContributionController controller;
public ContributionController controller;
private DirectUpload directUpload;
private Place place;
@ -122,9 +122,7 @@ public class NearbyMapFragment extends DaggerFragment {
private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.06;
private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04;
private boolean isSecondMaterialShowcaseDismissed;
private boolean isMapReady;
private MaterialShowcaseView thirdSingleShowCaseView;
private Bundle bundleForUpdtes;// Carry information from activity about changed nearby places and current location
@ -177,6 +175,13 @@ public class NearbyMapFragment extends DaggerFragment {
setRetainInstance(true);
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@ -214,7 +219,12 @@ public class NearbyMapFragment extends DaggerFragment {
});
}
/**
* Updates map slightly means it doesn't updates all nearby markers around. It just updates
* location tracker marker of user.
*/
public void updateMapSlightly() {
Timber.d("updateMapSlightly called, bundle is:"+bundleForUpdtes);
if (mapboxMap != null) {
Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriDeserializer())
@ -229,7 +239,13 @@ public class NearbyMapFragment extends DaggerFragment {
}
/**
* Updates map significantly means it updates nearby markers and location tracker marker. It is
* called when user is out of boundaries (south, north, east or west) of markers drawn by
* previous nearby call.
*/
public void updateMapSignificantly() {
Timber.d("updateMapSignificantly called, bundle is:"+bundleForUpdtes);
if (mapboxMap != null) {
if (bundleForUpdtes != null) {
Gson gson = new GsonBuilder()
@ -281,7 +297,7 @@ public class NearbyMapFragment extends DaggerFragment {
// Make camera to follow user on location change
CameraPosition position ;
if(ViewUtil.isPortrait(getActivity())){
if (ViewUtil.isPortrait(getActivity())){
position = new CameraPosition.Builder()
.target(isBottomListSheetExpanded ?
new LatLng(curMapBoxLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT,
@ -309,13 +325,19 @@ public class NearbyMapFragment extends DaggerFragment {
}
}
/**
* Updates camera position according to list sheet status. If list sheet is collapsed, camera
* focus should be in the center. If list sheet is expanded, camera focus should be visible
* on the gap between list sheet and tab layout.
* @param isBottomListSheetExpanded
*/
private void updateMapCameraAccordingToBottomSheet(boolean isBottomListSheetExpanded) {
CameraPosition position;
this.isBottomListSheetExpanded = isBottomListSheetExpanded;
if (mapboxMap != null && curLatLng != null) {
if (isBottomListSheetExpanded) {
// Make camera to follow user on location change
if(ViewUtil.isPortrait(getActivity())) {
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
@ -345,39 +367,40 @@ public class NearbyMapFragment extends DaggerFragment {
}
private void initViews() {
bottomSheetList = getActivity().findViewById(R.id.bottom_sheet);
Timber.d("initViews called");
bottomSheetList = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.bottom_sheet);
bottomSheetListBehavior = BottomSheetBehavior.from(bottomSheetList);
bottomSheetDetails = getActivity().findViewById(R.id.bottom_sheet_details);
bottomSheetDetails = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.bottom_sheet_details);
bottomSheetDetailsBehavior = BottomSheetBehavior.from(bottomSheetDetails);
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetDetails.setVisibility(View.VISIBLE);
fabPlus = getActivity().findViewById(R.id.fab_plus);
fabCamera = getActivity().findViewById(R.id.fab_camera);
fabGallery = getActivity().findViewById(R.id.fab_galery);
fabRecenter = getActivity().findViewById(R.id.fab_recenter);
fabPlus = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_plus);
fabCamera = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_camera);
fabGallery = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_galery);
fabRecenter = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_recenter);
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
fab_open = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.rotate_backward);
transparentView = getActivity().findViewById(R.id.transparentView);
transparentView = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.transparentView);
description = getActivity().findViewById(R.id.description);
title = getActivity().findViewById(R.id.title);
distance = getActivity().findViewById(R.id.category);
icon = getActivity().findViewById(R.id.icon);
description = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.description);
title = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.title);
distance = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.category);
icon = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.icon);
wikidataButton = getActivity().findViewById(R.id.wikidataButton);
wikipediaButton = getActivity().findViewById(R.id.wikipediaButton);
directionsButton = getActivity().findViewById(R.id.directionsButton);
commonsButton = getActivity().findViewById(R.id.commonsButton);
wikidataButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikidataButton);
wikipediaButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikipediaButton);
directionsButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.directionsButton);
commonsButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.commonsButton);
wikidataButtonText = getActivity().findViewById(R.id.wikidataButtonText);
wikipediaButtonText = getActivity().findViewById(R.id.wikipediaButtonText);
directionsButtonText = getActivity().findViewById(R.id.directionsButtonText);
commonsButtonText = getActivity().findViewById(R.id.commonsButtonText);
wikidataButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikidataButtonText);
wikipediaButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikipediaButtonText);
directionsButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.directionsButtonText);
commonsButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.commonsButtonText);
bookmarkButton = getActivity().findViewById(R.id.bookmarkButton);
bookmarkButtonImage = getActivity().findViewById(R.id.bookmarkButtonImage);
@ -416,7 +439,7 @@ public class NearbyMapFragment extends DaggerFragment {
mapView.getMapAsync(mapboxMap -> {
CameraPosition position;
if(ViewUtil.isPortrait(getActivity())){
if (ViewUtil.isPortrait(getActivity())){
position = new CameraPosition.Builder()
.target(isBottomListSheetExpanded ?
new LatLng(curLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT,
@ -494,6 +517,7 @@ public class NearbyMapFragment extends DaggerFragment {
}
private void setupMapView(Bundle savedInstanceState) {
Timber.d("setupMapView called");
MapboxMapOptions options = new MapboxMapOptions()
.compassGravity(Gravity.BOTTOM | Gravity.LEFT)
.compassMargins(new int[]{12, 0, 0, 24})
@ -505,15 +529,19 @@ public class NearbyMapFragment extends DaggerFragment {
.zoom(11)
.build());
// create map
mapView = new MapView(getActivity(), options);
mapView.onCreate(savedInstanceState);
mapView.getMapAsync(mapboxMap -> {
((NearbyActivity)getActivity()).setMapViewTutorialShowCase();
NearbyMapFragment.this.mapboxMap = mapboxMap;
updateMapSignificantly();
});
mapView.setStyleUrl("asset://mapstyle.json");
if (!getParentFragment().getActivity().isFinishing()) {
mapView = new MapView(getParentFragment().getActivity(), options);
// create map
mapView.onCreate(savedInstanceState);
mapView.getMapAsync(new OnMapReadyCallback() {
@Override
public void onMapReady(MapboxMap mapboxMap) {
NearbyMapFragment.this.mapboxMap = mapboxMap;
updateMapSignificantly();
}
});
mapView.setStyleUrl("asset://mapstyle.json");
}
}
/**
@ -542,6 +570,7 @@ public class NearbyMapFragment extends DaggerFragment {
* move.
*/
private void addCurrentLocationMarker(MapboxMap mapboxMap) {
Timber.d("addCurrentLocationMarker is called");
if (currentLocationMarker != null) {
currentLocationMarker.remove(); // Remove previous marker, we are not Hansel and Gretel
}
@ -564,8 +593,11 @@ public class NearbyMapFragment extends DaggerFragment {
mapboxMap.addPolygon(currentLocationPolygonOptions);
}
/**
* Adds markers for nearby places to mapbox map
*/
private void addNearbyMarkerstoMapBoxMap() {
Timber.d("addNearbyMarkerstoMapBoxMap is called");
mapboxMap.addMarkers(baseMarkerOptions);
mapboxMap.setOnInfoWindowCloseListener(marker -> {
@ -624,6 +656,12 @@ public class NearbyMapFragment extends DaggerFragment {
return circle;
}
/**
* If nearby details bottom sheet state is collapsed: show fab plus
* If nearby details bottom sheet state is expanded: show fab plus
* If nearby details bottom sheet state is hidden: hide all fabs
* @param bottomSheetState
*/
public void prepareViewsForSheetPosition(int bottomSheetState) {
switch (bottomSheetState) {
@ -648,6 +686,9 @@ public class NearbyMapFragment extends DaggerFragment {
}
}
/**
* Hides all fabs
*/
private void hideFAB() {
removeAnchorFromFABs(fabPlus);
@ -679,25 +720,14 @@ public class NearbyMapFragment extends DaggerFragment {
private void showFAB() {
addAnchorToBigFABs(fabPlus, getActivity().findViewById(R.id.bottom_sheet_details).getId());
addAnchorToBigFABs(fabPlus, ((NearbyFragment)getParentFragment()).view.findViewById(R.id.bottom_sheet_details).getId());
fabPlus.show();
addAnchorToSmallFABs(fabGallery, getActivity().findViewById(R.id.empty_view).getId());
addAnchorToSmallFABs(fabGallery, ((NearbyFragment)getParentFragment()).view.findViewById(R.id.empty_view).getId());
addAnchorToSmallFABs(fabCamera, getActivity().findViewById(R.id.empty_view1).getId());
thirdSingleShowCaseView = new MaterialShowcaseView.Builder(this.getActivity())
.setTarget(fabPlus)
.setDismissText(getString(R.string.showcase_view_got_it_button))
.setContentText(getString(R.string.showcase_view_plus_fab))
.setDelay(500) // optional but starting animations immediately in onCreate can make them choppy
.singleUse(ViewUtil.SHOWCASE_VIEW_ID_3) // provide a unique ID used to ensure it is only shown once
.setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD))
.build();
addAnchorToSmallFABs(fabCamera, ((NearbyFragment)getParentFragment()).view.findViewById(R.id.empty_view1).getId());
isMapReady = true;
if (isSecondMaterialShowcaseDismissed) {
thirdSingleShowCaseView.show(getActivity());
}
}
@ -724,6 +754,11 @@ public class NearbyMapFragment extends DaggerFragment {
floatingActionButton.setLayoutParams(params);
}
/**
* Same botom sheet carries information for all nearby places, so we need to pass information
* (title, description, distance and links) to view on nearby marker click
* @param place Place of clicked nearby marker
*/
private void passInfoToSheet(Place place) {
this.place = place;
@ -795,7 +830,7 @@ 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
// Do not use requestCode 1 as it will conflict with NearbyFragment's requestCodes
switch (requestCode) {
// 4 = "Read external storage" allowed when gallery selected
case 4: {
@ -873,18 +908,13 @@ public class NearbyMapFragment extends DaggerFragment {
}
}
/**
* This bundle is sent whenever and updte for nearby map comes, not for recreation, for updates
*/
public void setBundleForUpdtes(Bundle bundleForUpdtes) {
this.bundleForUpdtes = bundleForUpdtes;
}
public void onNearbyMaterialShowcaseDismissed() {
isSecondMaterialShowcaseDismissed = true;
if (isMapReady) {
thirdSingleShowCaseView.show(getActivity());
}
}
@Override
public void onStart() {
if (mapView != null) {

View file

@ -0,0 +1,283 @@
package fr.free.nrw.commons.nearby;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.SwipeDismissBehavior;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.CardView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
/**
* Custom card view for nearby notification card view on main screen, above contributions list
*/
public class NearbyNoificationCardView extends CardView{
private static final float MINIMUM_THRESHOLD_FOR_SWIPE = 100;
private Context context;
private Button permissionRequestButton;
private RelativeLayout contentLayout;
private TextView notificationTitle;
private TextView notificationDistance;
private ImageView notificationIcon;
private ProgressBar progressBar;
public CardViewVisibilityState cardViewVisibilityState;
public PermissionType permissionType;
float x1,x2;
public NearbyNoificationCardView(@NonNull Context context) {
super(context);
this.context = context;
cardViewVisibilityState = CardViewVisibilityState.INVISIBLE;
init();
}
public NearbyNoificationCardView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.context = context;
cardViewVisibilityState = CardViewVisibilityState.INVISIBLE;
init();
}
public NearbyNoificationCardView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
cardViewVisibilityState = CardViewVisibilityState.INVISIBLE;
init();
}
private void init() {
View rootView = inflate(context, R.layout.nearby_card_view, this);
permissionRequestButton = rootView.findViewById(R.id.permission_request_button);
contentLayout = rootView.findViewById(R.id.content_layout);
notificationTitle = rootView.findViewById(R.id.nearby_title);
notificationDistance = rootView.findViewById(R.id.nearby_distance);
notificationIcon = rootView.findViewById(R.id.nearby_icon);
progressBar = rootView.findViewById(R.id.progressBar);
setActionListeners();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// If you don't setVisibility after getting layout params, then you will se an empty space in place of nerabyNotificationCardView
if (((MainActivity)context).prefs.getBoolean("displayNearbyCardView", true)) {
this.setVisibility(VISIBLE);
} else {
this.setVisibility(GONE);
}
}
private void setActionListeners() {
this.setOnClickListener(view -> ((MainActivity)context).viewPager.setCurrentItem(1));
this.setOnTouchListener(
(v, event) -> {
boolean isSwipe = false;
float deltaX=0.0f;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x1 = event.getX();
break;
case MotionEvent.ACTION_UP:
x2 = event.getX();
deltaX = x2 - x1;
if (deltaX < 0) {
//Right to left swipe
isSwipe = true;
} else if (deltaX > 0) {
//Left to right swipe
isSwipe = true;
}
break;
}
if (isSwipe && (pixelToDp(Math.abs(deltaX)) > MINIMUM_THRESHOLD_FOR_SWIPE)) {
v.setVisibility(GONE);
// Save shared preference for nearby card view accordingly
((MainActivity) context).prefs.edit()
.putBoolean("displayNearbyCardView", false).apply();
ViewUtil.showLongToast(context, getResources().getString(R.string.nearby_notification_dismiss_message));
return true;
}
return false;
});
}
private float pixelToDp(float pixels) {
return (pixels / Resources.getSystem().getDisplayMetrics().density);
}
/**
* Sets permission request button visible and content layout invisible, then adds correct
* permission request actions to permission request button according to PermissionType enum
* @param isPermissionRequestButtonNeeded true if permissions missing
*/
public void displayPermissionRequestButton(boolean isPermissionRequestButtonNeeded) {
if (isPermissionRequestButtonNeeded) {
cardViewVisibilityState = CardViewVisibilityState.ASK_PERMISSION;
contentLayout.setVisibility(GONE);
permissionRequestButton.setVisibility(VISIBLE);
if (permissionType == PermissionType.ENABLE_LOCATION_PERMISSON) {
permissionRequestButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
if (!((MainActivity)context).isFinishing()) {
((MainActivity) context).locationManager.requestPermissions((MainActivity) context);
}
}
});
} else if (permissionType == PermissionType.ENABLE_GPS) {
permissionRequestButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
new AlertDialog.Builder(context)
.setMessage(R.string.gps_disabled)
.setCancelable(false)
.setPositiveButton(R.string.enable_gps,
(dialog, id) -> {
Intent callGPSSettingIntent = new Intent(
android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
Timber.d("Loaded settings page");
((MainActivity) context).startActivityForResult(callGPSSettingIntent, 1);
})
.setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> {
dialog.cancel();
displayPermissionRequestButton(true);
})
.create()
.show();
}
});
}
} else {
cardViewVisibilityState = CardViewVisibilityState.LOADING;
permissionRequestButton.setVisibility(GONE);
contentLayout.setVisibility(VISIBLE);
// Set visibility of elements in content layout once it become visible
progressBar.setVisibility(VISIBLE);
notificationTitle.setVisibility(GONE);
notificationDistance.setVisibility(GONE);
notificationIcon.setVisibility(GONE);
permissionRequestButton.setVisibility(GONE);
}
}
/**
* Pass place information to views.
* @param isClosestNearbyPlaceFound false if there are no close place
* @param place Closes place where we will get information from
*/
public void updateContent(boolean isClosestNearbyPlaceFound, Place place) {
Timber.d("Update nearby card notification content");
cardViewVisibilityState = CardViewVisibilityState.READY;
permissionRequestButton.setVisibility(GONE);
contentLayout.setVisibility(VISIBLE);
// Make progress bar invisible once data is ready
progressBar.setVisibility(GONE);
// And content views visible since they are ready
notificationTitle.setVisibility(VISIBLE);
notificationDistance.setVisibility(VISIBLE);
notificationIcon.setVisibility(VISIBLE);
if (isClosestNearbyPlaceFound) {
notificationTitle.setText(place.name);
notificationDistance.setText(place.distance);
} else {
notificationDistance.setText("");
notificationTitle.setText(R.string.no_close_nearby);
}
}
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (visibility == VISIBLE) {
/**
* Sometimes we need to preserve previous state of notification card view without getting
* any data from user. Ie. wen user came back from Media Details fragment to Contrib List
* fragment, we need to know what was the state of card view, and set it to exact same state.
*/
switch (cardViewVisibilityState) {
case READY:
permissionRequestButton.setVisibility(GONE);
contentLayout.setVisibility(VISIBLE);
// Make progress bar invisible once data is ready
progressBar.setVisibility(GONE);
// And content views visible since they are ready
notificationTitle.setVisibility(VISIBLE);
notificationDistance.setVisibility(VISIBLE);
notificationIcon.setVisibility(VISIBLE);
break;
case LOADING:
permissionRequestButton.setVisibility(GONE);
contentLayout.setVisibility(VISIBLE);
// Set visibility of elements in content layout once it become visible
progressBar.setVisibility(VISIBLE);
notificationTitle.setVisibility(GONE);
notificationDistance.setVisibility(GONE);
notificationIcon.setVisibility(GONE);
permissionRequestButton.setVisibility(GONE);
break;
case ASK_PERMISSION:
contentLayout.setVisibility(GONE);
permissionRequestButton.setVisibility(VISIBLE);
break;
default:
break;
}
}
}
/**
* This states will help us to preserve progress bar and content layout states
*/
public enum CardViewVisibilityState {
LOADING,
READY,
INVISIBLE,
ASK_PERMISSION,
}
/**
* We need to know which kind of permission we need to request, then update permission request
* button action accordingly
*/
public enum PermissionType {
ENABLE_GPS,
ENABLE_LOCATION_PERMISSON, // For only after Marsmallow
NO_PERMISSION_NEEDED
}
}

View file

@ -23,9 +23,9 @@ import timber.log.Timber;
public class NearbyPlaces {
private static final int MIN_RESULTS = 40;
private static int MIN_RESULTS = 40;
private static final double INITIAL_RADIUS = 1.0; // in kilometers
private static final double MAX_RADIUS = 300.0; // in kilometers
private static double MAX_RADIUS = 300.0; // in kilometers
private static final double RADIUS_MULTIPLIER = 1.618;
private static final Uri WIKIDATA_QUERY_URL = Uri.parse("https://query.wikidata.org/sparql");
private static final Uri WIKIDATA_QUERY_UI_URL = Uri.parse("https://query.wikidata.org/");
@ -41,9 +41,22 @@ public class NearbyPlaces {
}
}
List<Place> getFromWikidataQuery(LatLng curLatLng, String lang) throws IOException {
List<Place> getFromWikidataQuery(LatLng curLatLng, String lang, boolean returnClosestResult) throws IOException {
List<Place> places = Collections.emptyList();
/**
* If returnClosestResult is true, then this means that we are trying to get closest point
* to use in cardView above contributions list
*/
if (returnClosestResult) {
MIN_RESULTS = 1; // Return closest nearby place
MAX_RADIUS = 5; // Return places only in 5 km area
radius = INITIAL_RADIUS; // refresh radius again, otherwise increased radius is grater than MAX_RADIUS, thus returns null
} else {
MIN_RESULTS = 40;
MAX_RADIUS = 300.0; // in kilometers
}
// increase the radius gradually to find a satisfactory number of nearby places
while (radius <= MAX_RADIUS) {
try {
@ -103,12 +116,19 @@ public class NearbyPlaces {
String point = fields[0];
String wikiDataLink = Utils.stripLocalizedString(fields[1]);
String name = Utils.stripLocalizedString(fields[2]);
//getting icon link here
String identifier = Utils.stripLocalizedString(fields[3]);
//getting the ID which is at the end of link
identifier = identifier.split("/")[Utils.stripLocalizedString(fields[3]).split("/").length-1];
//replaced the extra > char from fields
identifier = identifier.replace(">","");
String type = Utils.stripLocalizedString(fields[4]);
String icon = fields[5];
String wikipediaSitelink = Utils.stripLocalizedString(fields[7]);
String commonsSitelink = Utils.stripLocalizedString(fields[8]);
String category = Utils.stripLocalizedString(fields[9]);
Timber.v("Name: " + name + ", type: " + type + ", category: " + category + ", wikipediaSitelink: " + wikipediaSitelink + ", commonsSitelink: " + commonsSitelink);
double latitude;
@ -127,7 +147,7 @@ public class NearbyPlaces {
places.add(new Place(
name,
Place.Label.fromText(type), // list
Place.Label.fromText(identifier), // list
type, // details
Uri.parse(icon),
new LatLng(latitude, longitude, 0),
@ -143,4 +163,5 @@ public class NearbyPlaces {
return places;
}
}

View file

@ -121,27 +121,30 @@ public class Place {
*/
public enum Label {
BUILDING("building", R.drawable.round_icon_generic_building),
HOUSE("house", R.drawable.round_icon_house),
COTTAGE("cottage", R.drawable.round_icon_house),
FARMHOUSE("farmhouse", R.drawable.round_icon_house),
CHURCH("church", R.drawable.round_icon_church),
RAILWAY_STATION("railway station", R.drawable.round_icon_railway_station),
GATEHOUSE("gatehouse", R.drawable.round_icon_gatehouse),
MILESTONE("milestone", R.drawable.round_icon_milestone),
INN("inn", R.drawable.round_icon_house),
CITY("city", R.drawable.round_icon_city),
SECONDARY_SCHOOL("secondary school", R.drawable.round_icon_school),
EDU("edu", R.drawable.round_icon_school),
ISLE("isle", R.drawable.round_icon_island),
MOUNTAIN("mountain", R.drawable.round_icon_mountain),
AIRPORT("airport", R.drawable.round_icon_airport),
BRIDGE("bridge", R.drawable.round_icon_bridge),
ROAD("road", R.drawable.round_icon_road),
FOREST("forest", R.drawable.round_icon_forest),
PARK("park", R.drawable.round_icon_park),
RIVER("river", R.drawable.round_icon_river),
WATERFALL("waterfall", R.drawable.round_icon_waterfall),
BUILDING("Q41176", R.drawable.round_icon_generic_building),
HOUSE("Q3947", R.drawable.round_icon_house),
COTTAGE("Q5783996", R.drawable.round_icon_house),
FARMHOUSE("Q489357", R.drawable.round_icon_house),
CHURCH("Q16970", R.drawable.round_icon_church), //changed from church to church building
RAILWAY_STATION("Q55488", R.drawable.round_icon_railway_station),
GATEHOUSE("Q277760", R.drawable.round_icon_gatehouse),
MILESTONE("Q10145", R.drawable.round_icon_milestone),
INN("Q256020", R.drawable.round_icon_house), //Q27686
HOTEL("Q27686", R.drawable.round_icon_house),
CITY("Q515", R.drawable.round_icon_city),
UNIVERSITY("Q3918",R.drawable.round_icon_school), //added university
SCHOOL("Q3914", R.drawable.round_icon_school), //changed from "secondary school" to school
EDUCATION("Q8434", R.drawable.round_icon_school), //changed from edu to education, there is no id for "edu"
ISLE("Q23442", R.drawable.round_icon_island),
MOUNTAIN("Q8502", R.drawable.round_icon_mountain),
AIRPORT("Q1248784", R.drawable.round_icon_airport),
BRIDGE("Q12280", R.drawable.round_icon_bridge),
ROAD("Q34442", R.drawable.round_icon_road),
FOREST("Q4421", R.drawable.round_icon_forest),
PARK("Q22698", R.drawable.round_icon_park),
RIVER("Q4022", R.drawable.round_icon_river),
WATERFALL("Q34038", R.drawable.round_icon_waterfall),
TEMPLE("Q44539",R.drawable.round_icon_church),
UNKNOWN("?", R.drawable.round_icon_unknown);
private static final Map<String, Label> TEXT_TO_DESCRIPTION

View file

@ -11,13 +11,15 @@ public class Notification {
public String description;
public String link;
public String iconUrl;
public String dateWithYear;
public Notification(NotificationType notificationType, String notificationText, String date, String description, String link, String iconUrl) {
public Notification(NotificationType notificationType, String notificationText, String date, String description, String link, String iconUrl, String dateWithYear) {
this.notificationType = notificationType;
this.notificationText = notificationText;
this.date = date;
this.description = description;
this.link = link;
this.iconUrl = iconUrl;
this.dateWithYear = dateWithYear;
}
}

View file

@ -17,6 +17,7 @@ import android.widget.RelativeLayout;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
@ -25,6 +26,7 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.NetworkUtils;
@ -85,6 +87,11 @@ public class NotificationActivity extends NavigationBaseActivity {
private void addNotifications() {
Timber.d("Add notifications");
// Store when add notification is called last
long currentDate = new Date(System.currentTimeMillis()).getTime();
getSharedPreferences("prefs", MODE_PRIVATE).edit().putLong("last_read_notification_date", currentDate).apply();
Timber.d("Set last notification read date to current date:"+ currentDate);
if(mNotificationWorkerFragment == null){
Observable.fromCallable(() -> {
progressBar.setVisibility(View.VISIBLE);
@ -115,7 +122,7 @@ public class NotificationActivity extends NavigationBaseActivity {
}
private void setAdapter(List<Notification> notificationList) {
if(notificationList == null || notificationList.isEmpty()) {
if (notificationList == null || notificationList.isEmpty()) {
ViewUtil.showSnackbar(relativeLayout, R.string.no_notifications);
return;
}
@ -140,4 +147,10 @@ public class NotificationActivity extends NavigationBaseActivity {
.commit();
mNotificationWorkerFragment.setNotificationList(notificationList);
}
@Override
public void onBackPressed() {
startActivityWithFlags(
this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP); }
}

View file

@ -1,6 +1,11 @@
package fr.free.nrw.commons.notification;
import android.graphics.Color;
import android.text.Html;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -60,7 +65,25 @@ public class NotificationRenderer extends Renderer<Notification> {
notificationText = notificationText.trim().replaceAll("(^\\s*)|(\\s*$)", "");
notificationText = Html.fromHtml(notificationText).toString();
notificationText = notificationText.concat(" ");
title.setText(notificationText);
SpannableString ss = new SpannableString(notificationText);
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(View view) {
listener.notificationClicked(getContent());
}
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
ds.setColor(Color.BLACK);
}
};
// Attach a ClickableSpan to the range (start:0, end:notificationText.length()) of the String
ss.setSpan(clickableSpan, 0, notificationText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
title.setText(ss, TextView.BufferType.SPANNABLE);
}
public interface NotificationClicked{

View file

@ -144,7 +144,7 @@ public class NotificationUtils {
notificationText = getWelcomeMessage(context, document);
break;
}
return new Notification(type, notificationText, getTimestamp(document), description, link, iconUrl);
return new Notification(type, notificationText, getTimestamp(document), description, link, iconUrl, getTimestampWithYear(document));
}
private static String getNotificationText(Node document) {
@ -193,7 +193,7 @@ public class NotificationUtils {
private static String getNotificationIconUrl(Node document) {
String format = "%s%s";
Node iconUrl = getNode(getModel(document), "iconUrl");
if(iconUrl == null) {
if (iconUrl == null) {
return null;
} else {
String url = iconUrl.getTextContent();
@ -247,6 +247,14 @@ public class NotificationUtils {
return "";
}
private static String getTimestampWithYear(Node document) {
Element timestampElement = (Element) getNode(document, "timestamp");
if (timestampElement != null) {
return timestampElement.getAttribute("utcunix");
}
return "";
}
private static String getNotificationDescription(Node document) {
Element titleElement = (Element) getNode(document, "title");
if (titleElement != null) {

View file

@ -0,0 +1,81 @@
package fr.free.nrw.commons.notification;
import android.os.AsyncTask;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Date;
import java.util.List;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import timber.log.Timber;
/**
* This asynctask will check unread notifications after a date (date user check notifications last)
*/
public class UnreadNotificationsCheckAsync extends AsyncTask<Void, Void, Notification> {
WeakReference<MainActivity> context;
NotificationController notificationController;
public UnreadNotificationsCheckAsync(MainActivity context, NotificationController notificationController) {
this.context = new WeakReference<>(context);
this.notificationController = notificationController;
}
@Override
protected Notification doInBackground(Void... voids) {
Notification lastNotification = null;
try {
lastNotification = findLastNotification(notificationController.getNotifications());
} catch (IOException e) {
e.printStackTrace();
}
return lastNotification;
}
@Override
protected void onPostExecute(Notification lastNotification) {
super.onPostExecute(lastNotification);
if (lastNotification == null) {
return;
}
Date lastNotificationCheckDate = new Date(context.get()
.getSharedPreferences("prefs",0)
.getLong("last_read_notification_date", 0));
Timber.d("You may have unread notifications since"+lastNotificationCheckDate);
boolean isThereUnreadNotifications;
Date lastReadNotificationDate = new java.util.Date(Long.parseLong(lastNotification.dateWithYear)*1000);
if (lastNotificationCheckDate.before(lastReadNotificationDate)) {
isThereUnreadNotifications = true;
} else {
isThereUnreadNotifications = false;
}
// Check if activity is still running
if (context.get().getWindow().getDecorView().isShown() && !context.get().isFinishing()) {
// Check if fragment is not null and visible
if (context.get().isContributionsFragmentVisible && context.get().contributionsActivityPagerAdapter.getItem(0) != null) {
((ContributionsFragment)(context.get().contributionsActivityPagerAdapter.getItem(0))).updateNotificationsNotification(isThereUnreadNotifications);
}
}
}
private Notification findLastNotification(List<Notification> allNotifications) {
if (allNotifications.size() > 0) {
return allNotifications.get(allNotifications.size()-1);
} else {
return null;
}
}
}

View file

@ -51,7 +51,7 @@ public class QuizActivity extends AppCompatActivity {
*/
@OnClick(R.id.next_button)
public void setNextQuestion(){
if( questionIndex <= quiz.size() && (positiveAnswer.isChecked() || negativeAnswer.isChecked())) {
if ( questionIndex <= quiz.size() && (positiveAnswer.isChecked() || negativeAnswer.isChecked())) {
evaluateScore();
} else if ( !positiveAnswer.isChecked() && !negativeAnswer.isChecked()){
AlertDialog.Builder alert = new AlertDialog.Builder(this);
@ -107,11 +107,11 @@ public class QuizActivity extends AppCompatActivity {
* to evaluate score and check whether answer is correct or wrong
*/
public void evaluateScore() {
if((quiz.get(questionIndex).isAnswer() && positiveAnswer.isChecked()) ||
if ((quiz.get(questionIndex).isAnswer() && positiveAnswer.isChecked()) ||
(!quiz.get(questionIndex).isAnswer() && negativeAnswer.isChecked()) ){
customAlert(getResources().getString(R.string.correct),quiz.get(questionIndex).getAnswerMessage() );
score++;
} else{
} else {
customAlert(getResources().getString(R.string.wrong), quiz.get(questionIndex).getAnswerMessage());
}
}
@ -127,12 +127,12 @@ public class QuizActivity extends AppCompatActivity {
alert.setMessage(Message);
alert.setPositiveButton(R.string.continue_message, (dialog, which) -> {
questionIndex++;
if(questionIndex == quiz.size()){
if (questionIndex == quiz.size()) {
Intent i = new Intent(QuizActivity.this, QuizResultActivity.class);
dialog.dismiss();
i.putExtra("QuizResult",score);
startActivity(i);
}else {
} else {
displayQuestion();
}
});

View file

@ -73,7 +73,7 @@ public class QuizChecker {
*/
private void setTotalUploadCount(int uploadCount) {
totalUploadCount = uploadCount - countPref.getInt(UPLOAD_SHARED_PREFERENCE,0);
if( totalUploadCount < 0){
if ( totalUploadCount < 0){
totalUploadCount = 0;
countPref.edit().putInt(UPLOAD_SHARED_PREFERENCE,0).apply();
}
@ -104,7 +104,7 @@ public class QuizChecker {
*/
private void setRevertParameter(int revertCountFetched) {
revertCount = revertCountFetched - revertPref.getInt(REVERT_SHARED_PREFERENCE,0);
if(revertCount < 0){
if (revertCount < 0){
revertCount = 0;
revertPref.edit().putInt(REVERT_SHARED_PREFERENCE, 0).apply();
}
@ -116,7 +116,7 @@ public class QuizChecker {
* to check whether the criterion to call quiz is satisfied
*/
private void calculateRevertParameter() {
if( revertCount < 0 || totalUploadCount < 0){
if ( revertCount < 0 || totalUploadCount < 0){
revertPref.edit().putInt(REVERT_SHARED_PREFERENCE, 0).apply();
countPref.edit().putInt(UPLOAD_SHARED_PREFERENCE,0).apply();
return;

View file

@ -8,7 +8,6 @@ import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import com.dinuscxj.progressbar.CircleProgressBar;
@ -16,7 +15,7 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.contributions.MainActivity;
import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater;
@ -26,16 +25,9 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.dinuscxj.progressbar.CircleProgressBar;
import java.io.File;
import java.io.FileOutputStream;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsActivity;
/**
* Displays the final score of quiz and congratulates the user
@ -57,13 +49,13 @@ public class QuizResultActivity extends AppCompatActivity {
ButterKnife.bind(this);
setSupportActionBar(toolbar);
if( getIntent() != null) {
if ( getIntent() != null) {
Bundle extras = getIntent().getExtras();
int score = extras.getInt("QuizResult");
setScore(score);
}else{
startActivityWithFlags(
this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP);
super.onBackPressed();
}
@ -87,14 +79,14 @@ public class QuizResultActivity extends AppCompatActivity {
@OnClick(R.id.quiz_result_next)
public void launchContributionActivity(){
startActivityWithFlags(
this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
@Override
public void onBackPressed() {
startActivityWithFlags(
this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP);
super.onBackPressed();
}
@ -186,9 +178,9 @@ public class QuizResultActivity extends AppCompatActivity {
AlertDialog.Builder alertadd = new AlertDialog.Builder(QuizResultActivity.this);
LayoutInflater factory = LayoutInflater.from(QuizResultActivity.this);
final View view = factory.inflate(R.layout.image_alert_layout, null);
ImageView screenShotImage = (ImageView) view.findViewById(R.id.alert_image);
ImageView screenShotImage = view.findViewById(R.id.alert_image);
screenShotImage.setImageBitmap(screenshot);
TextView shareMessage = (TextView) view.findViewById(R.id.alert_text);
TextView shareMessage = view.findViewById(R.id.alert_text);
shareMessage.setText(R.string.quiz_result_share_message);
alertadd.setView(view);
alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot));

View file

@ -44,7 +44,7 @@ public class RadioGroupHelper {
public RadioGroupHelper(View rootView, int... radiosIDs) {
super();
for (int radioButtonID : radiosIDs) {
add((RadioButton)rootView.findViewById(radioButtonID));
add(rootView.findViewById(radioButtonID));
}
}
@ -58,7 +58,7 @@ public class RadioGroupHelper {
*/
View.OnClickListener onClickListener = v -> {
for (CompoundButton rb : radioButtons) {
if(rb != v) rb.setChecked(false);
if (rb != v) rb.setChecked(false);
}
};
}

View file

@ -8,7 +8,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity;
public abstract class BaseActivity extends CommonsDaggerAppCompatActivity {
boolean currentTheme;
protected boolean currentTheme;
@Override
protected void onCreate(Bundle savedInstanceState) {

View file

@ -13,6 +13,7 @@ import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -32,11 +33,9 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.achievements.AchievementsActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.bookmarks.BookmarksActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.bookmarks.BookmarksActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import timber.log.Timber;
@ -91,6 +90,23 @@ public abstract class NavigationBaseActivity extends BaseActivity
}
}
public void changeDrawerIconToBakcButton() {
toggle.setDrawerIndicatorEnabled(false);
toggle.setHomeAsUpIndicator(R.drawable.ic_arrow_back_white);
toggle.setToolbarNavigationClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onBackPressed();
}
});
}
public void changeDrawerIconToDefault() {
if (toggle != null) {
toggle.setDrawerIndicatorEnabled(true);
}
}
/**
* Set the username in navigationHeader.
*/
@ -156,13 +172,9 @@ public abstract class NavigationBaseActivity extends BaseActivity
case R.id.action_home:
drawerLayout.closeDrawer(navigationView);
startActivityWithFlags(
this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP);
return true;
case R.id.action_nearby:
drawerLayout.closeDrawer(navigationView);
startActivityWithFlags(this, NearbyActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
return true;
case R.id.action_about:
drawerLayout.closeDrawer(navigationView);
startActivityWithFlags(this, AboutActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);

View file

@ -1,56 +1,72 @@
package fr.free.nrw.commons.upload;
import android.text.TextUtils;
import java.util.List;
/**
* Holds a description of an item being uploaded by {@link UploadActivity}
*/
class Description {
private String languageId;
private String languageDisplayText;
private String languageCode;
private String descriptionText;
private boolean set;
private int selectedLanguageIndex = -1;
public String getLanguageId() {
return languageId;
/**
* @return The language code ie. "en" or "fr"
*/
String getLanguageCode() {
return languageCode;
}
public void setLanguageId(String languageId) {
this.languageId = languageId;
/**
* @param languageCode The language code ie. "en" or "fr"
*/
void setLanguageCode(String languageCode) {
this.languageCode = languageCode;
}
public String getLanguageDisplayText() {
return languageDisplayText;
}
public void setLanguageDisplayText(String languageDisplayText) {
this.languageDisplayText = languageDisplayText;
}
public String getDescriptionText() {
String getDescriptionText() {
return descriptionText;
}
public void setDescriptionText(String descriptionText) {
void setDescriptionText(String descriptionText) {
this.descriptionText = descriptionText;
if (!TextUtils.isEmpty(descriptionText)) {
set = true;
}
}
public boolean isSet() {
return set;
}
public void setSet(boolean set) {
this.set = set;
}
public int getSelectedLanguageIndex() {
/**
* @return the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter}
*/
int getSelectedLanguageIndex() {
return selectedLanguageIndex;
}
public void setSelectedLanguageIndex(int selectedLanguageIndex) {
/**
* @param selectedLanguageIndex the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter}
*/
void setSelectedLanguageIndex(int selectedLanguageIndex) {
this.selectedLanguageIndex = selectedLanguageIndex;
}
/**
* Formats the list of descriptions into the format Commons requires for uploads.
*
* @param descriptions the list of descriptions, description is ignored if text is null.
* @return a string with the pattern of {{en|1=descriptionText}}
*/
static String formatList(List<Description> descriptions) {
StringBuilder descListString = new StringBuilder();
for (Description description : descriptions) {
if (!description.isEmpty()) {
String individualDescription = String.format("{{%s|1=%s}}", description.getLanguageCode(),
description.getDescriptionText());
descListString.append(individualDescription);
}
}
return descListString.toString();
}
public boolean isEmpty() {
return descriptionText == null || descriptionText.isEmpty();
}
}

View file

@ -2,12 +2,12 @@ package fr.free.nrw.commons.upload;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.AppCompatSpinner;
import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@ -17,182 +17,241 @@ import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.EditText;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnTouch;
import butterknife.Optional;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.AbstractTextWatcher;
import fr.free.nrw.commons.utils.BiMap;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.subjects.BehaviorSubject;
import io.reactivex.subjects.Subject;
import timber.log.Timber;
import static android.view.MotionEvent.ACTION_UP;
class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> {
List<Description> descriptions;
List<Language> languages;
private Title title;
private List<Description> descriptions;
private Context context;
private Callback callback;
private Subject<String> titleChangedSubject;
public DescriptionsAdapter() {
private BiMap<AdapterView, String> selectedLanguages;
private UploadView uploadView;
DescriptionsAdapter(UploadView uploadView) {
title = new Title();
descriptions = new ArrayList<>();
descriptions.add(new Description());
languages = new ArrayList<>();
titleChangedSubject = BehaviorSubject.create();
selectedLanguages = new BiMap<>();
this.uploadView = uploadView;
}
public void setCallback(Callback callback) {
void setCallback(Callback callback) {
this.callback = callback;
}
public void setDescriptions(List<Description> descriptions) {
void setItems(Title title, List<Description> descriptions) {
this.descriptions = descriptions;
this.title = title;
selectedLanguages = new BiMap<>();
notifyDataSetChanged();
}
public void setLanguages(List<Language> languages) {
this.languages = languages;
@Override
public int getItemViewType(int position) {
if (position == 0) return 1;
else return 2;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.row_item_description, parent, false);
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view;
if (viewType == 1) {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.row_item_title, parent, false);
} else {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.row_item_description, parent, false);
}
context = parent.getContext();
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.init(position);
}
@Override
public int getItemCount() {
return descriptions.size();
return descriptions.size() + 1;
}
public List<Description> getDescriptions() {
List<Description> getDescriptions() {
return descriptions;
}
public void addDescription(Description description) {
void addDescription(Description description) {
this.descriptions.add(description);
notifyItemInserted(descriptions.size() - 1);
notifyItemInserted(descriptions.size() + 1);
}
public Title getTitle() {
return title;
}
public void setTitle(Title title) {
this.title = title;
notifyItemInserted(0);
}
public class ViewHolder extends RecyclerView.ViewHolder {
@Nullable
@BindView(R.id.spinner_description_languages)
AppCompatSpinner spinnerDescriptionLanguages;
@BindView(R.id.et_description_text)
EditText etDescriptionText;
private View view;
@BindView(R.id.description_item_edit_text)
EditText descItemEditText;
private View view;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
this.view = itemView;
Timber.i("descItemEditText:" + descItemEditText);
}
public void init(int position) {
Description description = descriptions.get(position);
if (!TextUtils.isEmpty(description.getDescriptionText())) {
etDescriptionText.setText(description.getDescriptionText());
} else {
etDescriptionText.setText("");
}
Drawable drawableRight = context.getResources()
.getDrawable(R.drawable.mapbox_info_icon_default);
if (position != 0) {
etDescriptionText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
} else {
etDescriptionText.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableRight, null);
}
etDescriptionText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
description.setDescriptionText(editable.toString());
}
});
etDescriptionText.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
ViewUtil.hideKeyboard(v);
}
});
SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(context,
R.layout.row_item_languages_spinner);
Collections.sort(languages, (language, t1) -> language.getLocale().getDisplayLanguage()
.compareTo(t1.getLocale().getDisplayLanguage().toString()));
languagesAdapter.setLanguages(languages);
languagesAdapter.notifyDataSetChanged();
spinnerDescriptionLanguages.setAdapter(languagesAdapter);
if (description.getSelectedLanguageIndex() == -1) {
if (position == 0) {
int defaultLocaleIndex = getIndexOfUserDefaultLocale();
spinnerDescriptionLanguages.setSelection(defaultLocaleIndex);
if (position == 0) {
Timber.d("Title is " + title);
if (!title.isEmpty()) {
descItemEditText.setText(title.toString());
} else {
spinnerDescriptionLanguages.setSelection(0);
descItemEditText.setText("");
}
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), null);
descItemEditText.addTextChangedListener(new AbstractTextWatcher(titleText ->{
title.setTitleText(titleText);
titleChangedSubject.onNext(titleText);
}));
descItemEditText.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
ViewUtil.hideKeyboard(v);
} else {
uploadView.setTopCardState(false);
}
});
} else {
spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex());
Description description = descriptions.get(position - 1);
Timber.d("Description is " + description);
if (!TextUtils.isEmpty(description.getDescriptionText())) {
descItemEditText.setText(description.getDescriptionText());
} else {
descItemEditText.setText("");
}
if (position == 1) {
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), null);
} else {
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
}
descItemEditText.addTextChangedListener(new AbstractTextWatcher(descriptionText -> {
description.setDescriptionText(descriptionText);
}));
descItemEditText.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
ViewUtil.hideKeyboard(v);
} else {
uploadView.setTopCardState(false);
}
});
SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(context,
R.layout.row_item_languages_spinner, selectedLanguages);
languagesAdapter.notifyDataSetChanged();
spinnerDescriptionLanguages.setAdapter(languagesAdapter);
if (description.getSelectedLanguageIndex() == -1) {
if (position == 1) {
int defaultLocaleIndex = languagesAdapter.getIndexOfUserDefaultLocale(context);
spinnerDescriptionLanguages.setSelection(defaultLocaleIndex);
} else {
spinnerDescriptionLanguages.setSelection(0);
}
} else {
spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex());
selectedLanguages.put(spinnerDescriptionLanguages, description.getLanguageCode());
}
//TODO do it the butterknife way
spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position,
long l) {
description.setSelectedLanguageIndex(position);
String languageCode = ((SpinnerLanguagesAdapter) adapterView.getAdapter()).getLanguageCode(position);
description.setLanguageCode(languageCode);
selectedLanguages.remove(adapterView);
selectedLanguages.put(adapterView, languageCode);
((SpinnerLanguagesAdapter) adapterView.getAdapter()).selectedLangCode = languageCode;
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
}
languages.get(spinnerDescriptionLanguages.getSelectedItemPosition()).setSet(true);
//TODO do it the butterknife way
spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position,
long l) {
//TODO handle case when user tries to select an already selected language
updateDescriptionBasedOnSelectedLanguageIndex(description, position);
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
}
@OnTouch(R.id.et_description_text)
@Optional
@OnTouch(R.id.description_item_edit_text)
boolean descriptionInfo(View view, MotionEvent motionEvent) {
//Title info is visible only for the title
if (getAdapterPosition() == 0) {
//Description info is visible only for the first item
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
final int value = view.getRight() - descItemEditText
.getCompoundDrawables()[2]
.getBounds().width();
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
callback.showAlert(R.string.media_detail_title, R.string.title_info);
return true;
}
} else {
final int value = descItemEditText.getLeft() + descItemEditText
.getCompoundDrawables()[0]
.getBounds().width();
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
callback.showAlert(R.string.media_detail_title, R.string.title_info);
return true;
}
}
//Description info is visible only for the first description
} else if (getAdapterPosition() == 1) {
final int value;
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
value = etDescriptionText.getRight() - etDescriptionText
.getCompoundDrawables()[2]
.getBounds().width() - etDescriptionText.getPaddingRight();
if (motionEvent.getAction() == ACTION_UP && motionEvent.getX() >= value) {
value = view.getRight() - descItemEditText.getCompoundDrawables()[2].getBounds().width();
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
callback.showAlert(R.string.media_detail_description,
R.string.description_info);
return true;
}
} else {
value = etDescriptionText.getLeft() + etDescriptionText
value = descItemEditText.getLeft() + descItemEditText
.getCompoundDrawables()[0]
.getBounds().width();
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
@ -206,27 +265,12 @@ class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewH
}
}
private int getIndexOfUserDefaultLocale() {
for (int i = 0; i < languages.size(); i++) {
if (languages.get(i).getLocale()
.equals(context.getResources().getConfiguration().locale)) {
return i;
}
}
return 0;
}
private void updateDescriptionBasedOnSelectedLanguageIndex(Description description,
int position) {
Language language = languages.get(position);
Locale locale = language.getLocale();
description.setSelectedLanguageIndex(position);
description.setLanguageDisplayText(locale.getDisplayName());
description.setLanguageId(locale.getLanguage());
private Drawable getInfoIcon() {
return context.getResources()
.getDrawable(R.drawable.mapbox_info_icon_default);
}
public interface Callback {
void showAlert(int mediaDetailDescription, int descriptionInfo);
}
}

View file

@ -1,82 +0,0 @@
package fr.free.nrw.commons.upload;
import android.app.Activity;
import android.content.Intent;
import android.graphics.BitmapRegionDecoder;
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;
/**
* Created by bluesir9 on 16/9/17.
*
* <p>Responsible for checking if the picture that the user is trying to upload is useful or not. Will attempt to filter
* away completely black,fuzzy/blurry pictures(for now).
*
* <p>todo: Detect selfies?
*/
public class DetectUnwantedPicturesAsync extends AsyncTask<Void, Void, ImageUtils.Result> {
private final String imageMediaFilePath;
public final WeakReference<Activity> activityWeakReference;
DetectUnwantedPicturesAsync(WeakReference<Activity> activityWeakReference, String imageMediaFilePath) {
//this.callback = callback;
this.imageMediaFilePath = imageMediaFilePath;
this.activityWeakReference = activityWeakReference;
}
@Override
protected ImageUtils.Result doInBackground(Void... voids) {
try {
Timber.d("FilePath: " + imageMediaFilePath);
if (imageMediaFilePath == null) {
return ImageUtils.Result.IMAGE_OK;
}
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(imageMediaFilePath,false);
return ImageUtils.checkIfImageIsTooDark(decoder);
} catch (IOException ioe) {
Timber.e(ioe, "IO Exception");
return ImageUtils.Result.IMAGE_OK;
}
}
@Override
protected void onPostExecute(ImageUtils.Result result) {
super.onPostExecute(result);
Activity activity = activityWeakReference.get();
if (result != ImageUtils.Result.IMAGE_OK) {
//show appropriate error message
String errorMessage = result == ImageUtils.Result.IMAGE_DARK ? activity.getString(R.string.upload_image_too_dark) : activity.getString(R.string.upload_image_blurry);
AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(activity);
errorDialogBuilder.setMessage(errorMessage);
errorDialogBuilder.setTitle(activity.getString(R.string.warning));
errorDialogBuilder.setPositiveButton(activity.getString(R.string.no), (dialogInterface, i) -> {
//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();
}
}
}
}

View file

@ -0,0 +1,153 @@
package fr.free.nrw.commons.upload;
import android.app.Activity;
import com.karumi.dexter.Dexter;
import com.karumi.dexter.DexterBuilder;
import com.karumi.dexter.listener.PermissionDeniedResponse;
import com.karumi.dexter.listener.PermissionGrantedResponse;
import com.karumi.dexter.listener.single.BasePermissionListener;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ExternalStorageUtils;
import fr.free.nrw.commons.utils.PermissionUtils;
import io.reactivex.Completable;
import io.reactivex.subjects.CompletableSubject;
import timber.log.Timber;
public class DexterPermissionObtainer {
private final String requestedPermission;
private android.app.AlertDialog storagePermissionInfoDialog;
private DexterBuilder dexterStoragePermissionBuilder;
private PermissionDeniedResponse permissionDeniedResponse;
private boolean storagePromptInProgress;
private final String rationaleTitle;
private final String rationaleText;
private Activity activity;
private CompletableSubject storagePromptObservable;
/**
* @param activity The activity that is requesting the permission
* @param requestedPermission The permission being requested in the form of Manifest.permission.*
* @param rationaleTitle The title of the rationale dialog
* @param rationaleText The text inside the rationale dialog
*/
DexterPermissionObtainer(Activity activity, String requestedPermission, String rationaleTitle, String rationaleText) {
this.activity = activity;
this.rationaleTitle = rationaleTitle;
this.rationaleText = rationaleText;
this.requestedPermission = requestedPermission;
this.storagePromptObservable = CompletableSubject.create();
initPermissionsRationaleDialog();
}
/**
* Checks if storage permissions are obtained, prompts the users to grant storage permissions if necessary.
* When storage permission is present, onPermissionObtained is called.
*/
Completable confirmStoragePermissions() {
if (ExternalStorageUtils.isStoragePermissionGranted(activity)) {
Timber.i("Storage permissions already granted.");
storagePromptObservable.onComplete();
} else if (!storagePromptInProgress) {
if (storagePromptObservable.hasComplete()) {
storagePromptObservable = CompletableSubject.create();
}
//If permission is not there, ask for it
storagePromptInProgress = true;
askDexterToHandleExternalStoragePermission();
}
return storagePromptObservable;
}
/**
* To be called when the user returns to the original activity after manually enabling storage permissions.
*/
void onManualPermissionReturned() {
//OnActivity result, no matter what the result is, our function can handle that.
askDexterToHandleExternalStoragePermission();
}
/**
* This method initialised the Dexter's permission builder (if not already initialised). Also makes sure that the builder is initialised
* only once, otherwise we would'nt know on which instance of it, the user is working on. And after the builder is initialised, it checks
* for the required permission and then handles the permission status, thanks to Dexter's appropriate callbacks.
*/
private void askDexterToHandleExternalStoragePermission() {
Timber.d("External storage permission is being requested");
if (null == dexterStoragePermissionBuilder) {
dexterStoragePermissionBuilder = Dexter.withActivity(activity)
.withPermission(requestedPermission)
.withListener(new BasePermissionListener() {
@Override
public void onPermissionGranted(PermissionGrantedResponse response) {
Timber.d("User has granted us the permission for writing the external storage");
//If permission is granted, well and good
storagePromptInProgress = false;
storagePromptObservable.onComplete();
//onPermissionObtained.run();
}
@Override
public void onPermissionDenied(PermissionDeniedResponse response) {
Timber.d("User has granted us the permission for writing the external storage");
//If permission is not granted in whatsoever scenario, we show him a dialog stating why we need the permission
permissionDeniedResponse = response;
if (null != storagePermissionInfoDialog && !storagePermissionInfoDialog
.isShowing()) {
storagePermissionInfoDialog.show();
}
}
});
}
dexterStoragePermissionBuilder.check();
}
/**
* We have agreed to show a dialog showing why we need a particular permission.
* This method is used to initialise the dialog which is going to show the permission's rationale.
* The dialog is initialised along with a callback for positive and negative user actions.
*/
private void initPermissionsRationaleDialog() {
if (storagePermissionInfoDialog == null) {
storagePermissionInfoDialog = DialogUtil
.getAlertDialogWithPositiveAndNegativeCallbacks(
activity,
rationaleTitle, rationaleText,
R.drawable.ic_launcher, new DialogUtil.Callback() {
@Override
public void onPositiveButtonClicked() {
//If the user is willing to give us the permission
//But had somehow previously choose never ask again, we take him to app settings to manually enable permission
if (null == permissionDeniedResponse) {
//Dexter returned null, lets see if this ever happens
Timber.w("Dexter returned null as permissionDeniedResponse");
} else if (permissionDeniedResponse.isPermanentlyDenied()) {
PermissionUtils.askUserToManuallyEnablePermissionFromSettings(activity);
Timber.i("Permission permanently denied.");
} else {
//or if we still have chance to show runtime permission dialog, we show him that.
askDexterToHandleExternalStoragePermission();
Timber.d("Asking via Dexter for permission.");
}
}
@Override
public void onNegativeButtonClicked() {
//This was the behaviour as of now, I was planning to maybe snack him with some message
//and then call finish after some time, or may be it could be associated with some action
// on the snack. If the user does not want us to give the permission, even after showing
// rationale dialog, lets not trouble him any more.
activity.finish();
}
});
}
}
}

View file

@ -1,95 +0,0 @@
package fr.free.nrw.commons.upload;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
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.mwapi.MediaWikiApi;
import timber.log.Timber;
/**
* Sends asynchronous queries to the Commons MediaWiki API to check that file doesn't already exist
* Displays a warning to the user if the file already exists on Commons
*/
public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
interface Callback {
void onResult(Result result);
}
public enum Result {
NO_DUPLICATE,
DUPLICATE_PROCEED,
DUPLICATE_CANCELLED
}
private final WeakReference<Activity> activity;
private final MediaWikiApi api;
private final String fileSha1;
private final WeakReference<Context> context;
private final Callback callback;
public ExistingFileAsync(WeakReference<Activity> activity, String fileSha1, WeakReference<Context> context, Callback callback, MediaWikiApi mwApi) {
this.activity = activity;
this.fileSha1 = fileSha1;
this.context = context;
this.callback = callback;
this.api = mwApi;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected Boolean doInBackground(Void... voids) {
// https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba
boolean fileExists;
try {
String fileSha1 = this.fileSha1;
fileExists = api.existingFile(fileSha1);
} catch (IOException e) {
Timber.e(e, "IO Exception: ");
return false;
}
Timber.d("File already exists in Commons: %s", fileExists);
return fileExists;
}
@Override
protected void onPostExecute(Boolean fileExists) {
super.onPostExecute(fileExists);
// If file exists, display warning to user.
// Use soft warning for now (user able to choose to proceed) until have determined that implementation works without bugs
if (fileExists) {
AlertDialog.Builder builder = new AlertDialog.Builder(context.get());
builder.setMessage(R.string.file_exists)
.setTitle(R.string.warning);
builder.setPositiveButton(R.string.no, (dialog, id) -> {
//Go back to ContributionsActivity
Intent intent = new Intent(context.get(), ContributionsActivity.class);
context.get().startActivity(intent);
callback.onResult(Result.DUPLICATE_CANCELLED);
});
builder.setNegativeButton(R.string.yes, (dialog, id) -> callback.onResult(Result.DUPLICATE_PROCEED));
AlertDialog dialog = builder.create();
if (!activity.get().isFinishing()) {
dialog.show();
}
} else {
callback.onResult(Result.NO_DUPLICATE);
}
}
}

View file

@ -1,21 +1,21 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Date;
import java.util.List;
@ -44,89 +44,44 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
@Inject
@Named("default_preferences")
SharedPreferences prefs;
private Uri mediaUri;
private String filePath;
private ContentResolver contentResolver;
private GPSExtractor imageObj;
private Context context;
private String decimalCoords;
private boolean haveCheckedForOtherImages = false;
private String filePath;
private ExifInterface exifInterface;
private boolean useExtStorage;
private boolean cacheFound;
private boolean haveCheckedForOtherImages = false;
private GPSExtractor tempImageObj;
FileProcessor(Uri mediaUri, ContentResolver contentResolver, Context context) {
this.mediaUri = mediaUri;
FileProcessor(@NonNull String filePath, ContentResolver contentResolver, Context context) {
this.filePath = filePath;
this.contentResolver = contentResolver;
this.context = context;
ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this);
try {
exifInterface=new ExifInterface(filePath);
} catch (IOException e) {
Timber.e(e);
}
useExtStorage = prefs.getBoolean("useExternalStorage", true);
}
/**
* Gets file path from media URI.
* In older devices getPath() may fail depending on the source URI, creating and using a copy of the file seems to work instead.
*
* @return file path of media
*/
@Nullable
private String getPathOfMediaOrCopy() {
filePath = FileUtils.getPath(context, mediaUri);
Timber.d("Filepath: " + filePath);
if (filePath == null) {
String copyPath = null;
try {
ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r");
if (descriptor != null) {
if (useExtStorage) {
copyPath = FileUtils.createCopyPath(descriptor);
return copyPath;
}
copyPath = getApplicationContext().getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + ".jpg";
FileUtils.copy(descriptor.getFileDescriptor(), copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
} catch (IOException e) {
Timber.w(e, "Error in file " + copyPath);
return null;
}
}
return filePath;
}
/**
* Processes file coordinates, either from EXIF data or user location
*
* @param gpsEnabled if true use GPS
*/
GPSExtractor processFileCoordinates(boolean gpsEnabled) {
GPSExtractor processFileCoordinates(SimilarImageInterface similarImageInterface) {
Timber.d("Calling GPSExtractor");
try {
ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (descriptor != null) {
imageObj = new GPSExtractor(descriptor.getFileDescriptor());
}
} else {
String filePath = getPathOfMediaOrCopy();
if (filePath != null) {
imageObj = new GPSExtractor(filePath);
}
}
decimalCoords = imageObj.getCoords();
if (decimalCoords == null || !imageObj.imageCoordsExists) {
//Find other photos taken around the same time which has gps coordinates
if (!haveCheckedForOtherImages)
findOtherImages();// Do not do repeat the process
} else {
useImageCoords();
}
} catch (FileNotFoundException e) {
Timber.w("File not found: " + mediaUri, e);
imageObj = new GPSExtractor(exifInterface);
decimalCoords = imageObj.getCoords();
if (decimalCoords == null || !imageObj.imageCoordsExists) {
//Find other photos taken around the same time which has gps coordinates
if (!haveCheckedForOtherImages)
findOtherImages(similarImageInterface);// Do not do repeat the process
} else {
useImageCoords();
}
return imageObj;
}
@ -136,10 +91,10 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
/**
* Find other images around the same location that were taken within the last 20 sec
*
* @param similarImageInterface
*/
private void findOtherImages() {
Timber.d("filePath" + getPathOfMediaOrCopy());
private void findOtherImages(SimilarImageInterface similarImageInterface) {
Timber.d("filePath" + filePath);
long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created
File folder = new File(filePath.substring(0, filePath.lastIndexOf('/')));
@ -154,7 +109,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos
ParcelFileDescriptor descriptor = null;
try {
descriptor = contentResolver.openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r");
descriptor = contentResolver.openFileDescriptor(Uri.fromFile(file), "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
@ -173,12 +128,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
if (tempImageObj.getCoords() != null && tempImageObj.imageCoordsExists) {
// Current image has gps coordinates and it's not current gps locaiton
Timber.d("This file has image coords:" + file.getAbsolutePath());
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
Bundle args = new Bundle();
args.putString("originalImagePath", filePath);
args.putString("possibleImagePath", file.getAbsolutePath());
newFragment.setArguments(args);
newFragment.show(((AppCompatActivity) context).getSupportFragmentManager(), "dialog");
similarImageInterface.showSimilarImageFragment(filePath, file.getAbsolutePath());
break;
}
}
@ -210,7 +160,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
// If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories
if (catListEmpty) {
cacheFound = false;
apiCall.request(decimalCoords)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
@ -223,7 +172,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
);
Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList);
} else {
cacheFound = true;
Timber.d("Cache found, setting categoryList in model to %s", displayCatList);
gpsCategoryModel.setCategoryList(displayCatList);
}
@ -232,20 +180,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
}
}
boolean isCacheFound() {
return cacheFound;
}
/**
* Calls the async task that detects if image is fuzzy, too dark, etc
*/
void detectUnwantedPictures() {
String imageMediaFilePath = FileUtils.getPath(context, mediaUri);
DetectUnwantedPicturesAsync detectUnwantedPicturesAsync
= new DetectUnwantedPicturesAsync(new WeakReference<Activity>((Activity) context), imageMediaFilePath);
detectUnwantedPicturesAsync.execute();
}
@Override
public void onPositiveResponse() {
imageObj = tempImageObj;

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.SharedPreferences;
@ -12,6 +13,7 @@ import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -33,6 +35,8 @@ import java.util.Date;
import timber.log.Timber;
import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext;
public class FileUtils {
/**
@ -76,21 +80,32 @@ public class FileUtils {
/**
* In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
*
* @return path of copy
*/
@Nullable
static String createCopyPath(ParcelFileDescriptor descriptor) {
try {
String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + ".jpg";
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
newFile.mkdir();
FileUtils.copy(descriptor.getFileDescriptor(), copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
} catch (IOException e) {
Timber.e(e);
return null;
}
@NonNull
static String createExternalCopyPathAndCopy(Uri uri, ContentResolver contentResolver) throws IOException {
FileDescriptor fileDescriptor = contentResolver.openFileDescriptor(uri, "r").getFileDescriptor();
String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + "." + getFileExt(uri, contentResolver);
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
newFile.mkdir();
FileUtils.copy(fileDescriptor, copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
/**
* In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
*
* @return path of copy
*/
@NonNull
static String createCopyPathAndCopy(Uri uri, Context context) throws IOException {
FileDescriptor fileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r").getFileDescriptor();
String copyPath = context.getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + "." + getFileExt(uri, context.getContentResolver());
FileUtils.copy(fileDescriptor, copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
/**
@ -121,13 +136,13 @@ public class FileUtils {
if ("primary".equalsIgnoreCase(type)) {
returnPath = Environment.getExternalStorageDirectory() + "/" + split[1];
}
} else if (isDownloadsDocument(uri)) { // DownloadsProvider
} else if (isDownloadsDocument(uri)) { // DownloadsProvider
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/document"), Long.valueOf(id));
returnPath = getDataColumn(context, contentUri, null, null);
returnPath = getDataColumn(context, contentUri, null, null);
} else if (isMediaDocument(uri)) { // MediaProvider
final String docId = DocumentsContract.getDocumentId(uri);
@ -166,7 +181,7 @@ public class FileUtils {
returnPath = uri.getPath();
}
if(returnPath == null) {
if (returnPath == null) {
//fetching path may fail depending on the source URI and all hope is lost
//so we will create and use a copy of the file, which seems to work
String copyPath = null;
@ -304,6 +319,7 @@ public class FileUtils {
/**
* Read and return the content of a resource file as string.
*
* @param fileName asset file's path (e.g. "/queries/nearby_query.rq")
* @return the content of the file
*/
@ -330,6 +346,7 @@ public class FileUtils {
/**
* Deletes files.
*
* @param file context
*/
public static boolean deleteFile(File file) {
@ -355,7 +372,7 @@ public class FileUtils {
commonsAppDirectory.mkdir();
}
File logsFile = new File(commonsAppDirectory,"logs.txt");
File logsFile = new File(commonsAppDirectory, "logs.txt");
if (logsFile.exists()) {
//old logs file is useless
logsFile.delete();
@ -377,4 +394,39 @@ public class FileUtils {
}
}
public static String getFilename(Uri uri, ContentResolver contentResolver) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN)
return "";
String result = null;
if (uri.getScheme().equals("content")) {
try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
}
}
if (result == null) {
result = uri.getPath();
int cut = result.lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
}
}
return result;
}
public static String getFileExt(String fileName){
//Default file extension
String extension=".jpg";
int i = fileName.lastIndexOf('.');
if (i > 0) {
extension = fileName.substring(i+1);
}
return extension;
}
public static String getFileExt(Uri uri, ContentResolver contentResolver) {
return getFileExt(getFilename(uri, contentResolver));
}
}

View file

@ -16,11 +16,22 @@ import timber.log.Timber;
*/
public class GPSExtractor {
private ExifInterface exif;
public static final GPSExtractor DUMMY= new GPSExtractor();
private double decLatitude;
private double decLongitude;
public boolean imageCoordsExists;
private String latitude;
private String longitude;
private String latitudeRef;
private String longitudeRef;
private String decimalCoords;
/**
* Dummy constructor.
*/
private GPSExtractor(){
}
/**
* Construct from the file descriptor of the image (only for API 24 or newer).
* @param fileDescriptor the file descriptor of the image
@ -28,7 +39,8 @@ public class GPSExtractor {
@RequiresApi(24)
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
try {
exif = new ExifInterface(fileDescriptor);
ExifInterface exif = new ExifInterface(fileDescriptor);
processCoords(exif);
} catch (IOException | IllegalArgumentException e) {
Timber.w(e);
}
@ -41,47 +53,53 @@ public class GPSExtractor {
*/
public GPSExtractor(@NonNull String path) {
try {
exif = new ExifInterface(path);
ExifInterface exif = new ExifInterface(path);
processCoords(exif);
} catch (IOException | IllegalArgumentException e) {
Timber.w(e);
}
}
/**
* Construct from the file path of the image.
* @param exif exif interface of the image
*
*/
public GPSExtractor(@NonNull ExifInterface exif){
processCoords(exif);
}
private void processCoords(ExifInterface exif){
//If image has no EXIF data and user has enabled GPS setting, get user's location
//Always return null as a temporary fix for #1599
if (exif != null && exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) != null) {
//If image has EXIF data, extract image coords
imageCoordsExists = true;
Timber.d("EXIF data has location info");
latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
}
}
/**
* Extracts geolocation (either of image from EXIF data, or of user)
* @return coordinates as string (needs to be passed as a String in API query)
*/
@Nullable
public String getCoords() {
String latitude;
String longitude;
String latitudeRef;
String longitudeRef;
String decimalCoords;
if(decimalCoords!=null){
return decimalCoords;
}else if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
Timber.d("Latitude: %s %s", latitude, latitudeRef);
Timber.d("Longitude: %s %s", longitude, longitudeRef);
//If image has no EXIF data and user has enabled GPS setting, get user's location
//TODO: Always return null as a temporary fix for #1599
if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) {
return null;
decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef);
return decimalCoords;
} else {
//If image has EXIF data, extract image coords
imageCoordsExists = true;
Timber.d("EXIF data has location info");
latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
Timber.d("Latitude: %s %s", latitude, latitudeRef);
Timber.d("Longitude: %s %s", longitude, longitudeRef);
decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef);
return decimalCoords;
} else {
return null;
}
return null;
}
}

View file

@ -0,0 +1,52 @@
package fr.free.nrw.commons.upload;
import android.app.Activity;
import android.content.Context;
import android.graphics.Point;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Display;
/**
* Created by Ilgaz Er on 8/7/2018.
*/
public class HeightLimitedRecyclerView extends RecyclerView {
int height;
public HeightLimitedRecyclerView(Context context) {
super(context);
DisplayMetrics displayMetrics = new DisplayMetrics();
((Activity) getContext()).getWindowManager()
.getDefaultDisplay()
.getMetrics(displayMetrics);
height=displayMetrics.heightPixels;
}
public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
DisplayMetrics displayMetrics = new DisplayMetrics();
((Activity) getContext()).getWindowManager()
.getDefaultDisplay()
.getMetrics(displayMetrics);
height=displayMetrics.heightPixels;
}
public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
DisplayMetrics displayMetrics = new DisplayMetrics();
((Activity) getContext()).getWindowManager()
.getDefaultDisplay()
.getMetrics(displayMetrics);
height=displayMetrics.heightPixels;
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
heightSpec = MeasureSpec.makeMeasureSpec((int) (height*0.3), MeasureSpec.AT_MOST);
super.onMeasure(widthSpec, heightSpec);
}
}

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.upload;
import java.util.Locale;
class Language {
private Locale locale;
private boolean isSet = false;

View file

@ -1,493 +0,0 @@
package fr.free.nrw.commons.upload;
import android.Manifest;
import android.Manifest.permission;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.Toast;
import com.karumi.dexter.Dexter;
import com.karumi.dexter.DexterBuilder;
import com.karumi.dexter.listener.PermissionDeniedResponse;
import com.karumi.dexter.listener.PermissionGrantedResponse;
import com.karumi.dexter.listener.single.BasePermissionListener;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.ButterKnife;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.modifications.CategoryModifier;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ContributionUtils;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.DialogUtil.Callback;
import fr.free.nrw.commons.utils.ExternalStorageUtils;
import fr.free.nrw.commons.utils.PermissionUtils;
import timber.log.Timber;
//TODO: We should use this class to see how multiple uploads are handled, and then REMOVE it.
public class MultipleShareActivity extends AuthenticatedActivity
implements MediaDetailPagerFragment.MediaDetailProvider,
AdapterView.OnItemClickListener,
FragmentManager.OnBackStackChangedListener,
MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
OnCategoriesSaveHandler,
ActivityCompat.OnRequestPermissionsResultCallback{
@Inject
MediaWikiApi mwApi;
@Inject
SessionManager sessionManager;
@Inject
UploadController uploadController;
@Inject
ModifierSequenceDao modifierSequenceDao;
@Inject
@Named("default_preferences")
SharedPreferences prefs;
private ArrayList<Contribution> photosList = null;
private MultipleUploadListFragment uploadsList;
private MediaDetailPagerFragment mediaDetails;
private CategorizationFragment categorizationFragment;
private boolean locationPermitted = false;
private boolean isMultipleUploadsPrepared = false;
private boolean isMultipleUploadsFinalised = false; // Checks is user clicked to upload button or regret before this phase
private final String TAG="#MultipleShareActivity#";
private AlertDialog storagePermissionInfoDialog;
private DexterBuilder dexterStoragePermissionBuilder;
private PermissionDeniedResponse permissionDeniedResponse;
@Override
public Media getMediaAtPosition(int i) {
return photosList.get(i);
}
@Override
public int getTotalMediaCount() {
if (photosList == null) {
return 0;
}
return photosList.size();
}
@Override
public void notifyDatasetChanged() {
if (uploadsList != null) {
uploadsList.notifyDatasetChanged();
}
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
// fixme implement me if needed
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
// fixme implement me if needed
}
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int index, long item) {
showDetail(index);
}
@Override
public void OnMultipleUploadInitiated() {
// No need to request external permission here, because if user can reach this point, then she permission granted
Timber.d("OnMultipleUploadInitiated");
multipleUploadBegins();
}
private void multipleUploadBegins() {
Timber.d("Multiple upload begins");
final ProgressDialog dialog = new ProgressDialog(this);
dialog.setIndeterminate(false);
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setMax(photosList.size());
dialog.setTitle(getResources().getQuantityString(R.plurals.starting_multiple_uploads, photosList.size(), photosList.size()));
dialog.show();
for (int i = 0; i < photosList.size(); i++) {
Contribution up = photosList.get(i);
final int uploadCount = i + 1; // Goddamn Java
uploadController.startUpload(up, contribution -> {
dialog.setProgress(uploadCount);
if (uploadCount == photosList.size()) {
dialog.dismiss();
Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
startingToast.show();
}
});
}
uploadsList.setImageOnlyMode(true);
categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization");
if (categorizationFragment == null) {
categorizationFragment = new CategorizationFragment();
}
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
View target = getCurrentFocus();
if (target != null) {
InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null)
imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
}
getSupportFragmentManager().beginTransaction()
.add(R.id.uploadsFragmentContainer, categorizationFragment, "categorization")
.commitAllowingStateLoss();
isMultipleUploadsFinalised = true;
//See http://stackoverflow.com/questions/7469082/getting-exception-illegalstateexception-can-not-perform-this-action-after-onsa
}
@Override
public void onCategoriesSave(List<String> categories) {
if (categories.size() > 0) {
for (Contribution contribution : photosList) {
ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
modifierSequenceDao.save(categoriesSequence);
}
}
// FIXME: Make sure that the content provider is up
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default!
finish();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
if (mediaDetails.isVisible()) {
getSupportFragmentManager().popBackStack();
}
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_multiple_uploads);
ButterKnife.bind(this);
initDrawer();
initPermissionsRationaleDialog();
if (savedInstanceState != null) {
photosList = savedInstanceState.getParcelableArrayList("uploadsList");
}
getSupportFragmentManager().addOnBackStackChangedListener(this);
requestAuthToken();
//TODO: 15/10/17 should location permission be explicitly requested if not provided?
//check if location permission is enabled
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
{
locationPermitted = true;
}
}
}
/**
* We have agreed to show a dialog showing why we need a particular permission.
* This method is used to initialise the dialog which is going to show the permission's rationale.
* The dialog is initialised along with a callback for positive and negative user actions.
*/
private void initPermissionsRationaleDialog() {
if (storagePermissionInfoDialog == null) {
storagePermissionInfoDialog = DialogUtil
.getAlertDialogWithPositiveAndNegativeCallbacks(
MultipleShareActivity.this,
getString(R.string.storage_permission), getString(
R.string.write_storage_permission_rationale_for_image_share),
R.drawable.ic_launcher, new Callback() {
@Override
public void onPositiveButtonClicked() {
//If the user is willing to give us the permission
//But had somehow previously choose never ask again, we take him to app settings to manually enable permission
if(null== permissionDeniedResponse){
//Dexter returned null, lets see if this ever happens
return;
}
else if (permissionDeniedResponse.isPermanentlyDenied()) {
PermissionUtils.askUserToManuallyEnablePermissionFromSettings(MultipleShareActivity.this);
} else {
//or if we still have chance to show runtime permission dialog, we show him that.
askDexterToHandleExternalStoragePermission();
}
}
@Override
public void onNegativeButtonClicked() {
//This was the behaviour as of now, I was planning to maybe snack him with some message
//and then call finish after some time, or may be it could be associated with some action on the snack
//If the user does not want us to give the permission, even after showing rationale dialog, lets not trouble him anymore
finish();
}
});
}
}
@Override
protected void onDestroy() {
super.onDestroy();
getSupportFragmentManager().removeOnBackStackChangedListener(this);
uploadController.cleanup();
}
private void showDetail(int i) {
if (mediaDetails == null || !mediaDetails.isVisible()) {
mediaDetails = new MediaDetailPagerFragment(true, false);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.uploadsFragmentContainer, mediaDetails)
.addToBackStack(null)
.commit();
getSupportFragmentManager().executePendingTransactions();
}
mediaDetails.showImage(i);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
/* This will be true if permission request is granted before we request. Otherwise we will
* explicitly call operations under this method again.
*/
if (isMultipleUploadsPrepared) {
super.onSaveInstanceState(outState);
Timber.d("onSaveInstanceState multiple uploads is prepared, permission granted");
outState.putParcelableArrayList("uploadsList", photosList);
} else {
Timber.d("onSaveInstanceState multiple uploads is not prepared, permission not granted");
return;
}
}
@Override
protected void onAuthCookieAcquired(String authCookie) {
// Multiple uploads prepared boolean is used to decide when to call multipleUploadsBegin()
isMultipleUploadsFinalised = false;
isMultipleUploadsPrepared = false;
mwApi.setAuthCookie(authCookie);
if (!ExternalStorageUtils.isStoragePermissionGranted(this)) {
//If permission is not there, handle the negative cases
askDexterToHandleExternalStoragePermission();
isMultipleUploadsPrepared = false;
return; // Postpone operation to do after gettion permission
} else {
isMultipleUploadsPrepared = true;
prepareMultipleUploadList();
}
}
/**
* This method initialised the Dexter's permission builder (if not already initialised). Also makes sure that the builder is initialised
* only once, otherwise we would'nt know on which instance of it, the user is working on. And after the builder is initialised, it checks for the required
* permission and then handles the permission status, thanks to Dexter's appropriate callbacks.
*/
private void askDexterToHandleExternalStoragePermission() {
Timber.d(TAG, "External storage permission is being requested");
if (null == dexterStoragePermissionBuilder) {
dexterStoragePermissionBuilder = Dexter.withActivity(this)
.withPermission(permission.WRITE_EXTERNAL_STORAGE)
.withListener(new BasePermissionListener() {
@Override
public void onPermissionGranted(PermissionGrantedResponse response) {
Timber.d(TAG,"User has granted us the permission for writing the external storage");
//If permission is granted, well and good
prepareMultipleUploadList();
}
@Override
public void onPermissionDenied(PermissionDeniedResponse response) {
Timber.d(TAG,"User has granted us the permission for writing the external storage");
//If permission is not granted in whatsoever scenario, we show him a dialog stating why we need the permission
permissionDeniedResponse=response;
if (null != storagePermissionInfoDialog && !storagePermissionInfoDialog
.isShowing()) {
storagePermissionInfoDialog.show();
}
}
});
}
dexterStoragePermissionBuilder.check();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) {
//OnActivity result, no matter what the result is, our function can handle that.
askDexterToHandleExternalStoragePermission();
}
}
/**
* Prepares a list from files will be uploaded. Saves these files temporarily to external
* storage. Adds them to uploads list
*/
private void prepareMultipleUploadList() {
Intent intent = getIntent();
if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
if (photosList == null) {
photosList = new ArrayList<>();
ArrayList<Uri> urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
for (int i = 0; i < urisList.size(); i++) {
Contribution up = new Contribution();
Uri uri = urisList.get(i);
// Use temporarily saved file Uri instead
uri = ContributionUtils.saveFileBeingUploadedTemporarily(this, uri);
up.setLocalUri(uri);
up.setTag("mimeType", intent.getType());
up.setTag("sequence", i);
up.setSource(Contribution.SOURCE_EXTERNAL);
up.setMultiple(true);
String imageGpsCoordinates = extractImageGpsData(uri);
if (imageGpsCoordinates != null) {
Timber.d("GPS data for image found!");
up.setDecimalCoords(imageGpsCoordinates);
}
photosList.add(up);
}
}
uploadsList = (MultipleUploadListFragment) getSupportFragmentManager().findFragmentByTag("uploadsList");
if (uploadsList == null) {
uploadsList = new MultipleUploadListFragment();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.uploadsFragmentContainer, uploadsList, "uploadsList")
.commit();
}
setTitle(getResources().getQuantityString(R.plurals.multiple_uploads_title, photosList.size(), photosList.size()));
uploadController.prepareService();
}
}
@Override
protected void onAuthFailure() {
Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG);
failureToast.show();
finish();
}
@Override
public void onBackStackChanged() {
getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible());
}
/**
* Will attempt to extract the gps coordinates using exif data or by using the current
* location if available for the image who's imageUri has been provided.
* @param imageUri The uri of the image who's GPS coordinates data we wish to extract
* @return GPS coordinates as a String as is returned by {@link GPSExtractor}
*/
@Nullable
private String extractImageGpsData(Uri imageUri) {
Timber.d("Entering extractImagesGpsData");
if (imageUri == null) {
//now why would you do that???
return null;
}
GPSExtractor gpsExtractor = null;
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(imageUri,"r");
if (fd != null) {
gpsExtractor = new GPSExtractor(fd.getFileDescriptor());
}
} else {
String filePath = FileUtils.getPath(this,imageUri);
if (filePath != null) {
gpsExtractor = new GPSExtractor(filePath);
}
}
if (gpsExtractor != null) {
//get image coordinates from exif data or user location
return gpsExtractor.getCoords();
}
} catch (FileNotFoundException fnfe) {
Timber.w(fnfe);
return null;
}
return null;
}
// If on back pressed before sharing
@Override
public void onBackPressed() {
super.onBackPressed();
}
@Override
protected void onStop() {
// Remove saved files if activity is stopped before upload operation, ie user changed mind
if (!isMultipleUploadsFinalised) {
if (photosList != null) {
for (Contribution contribution : photosList) {
Timber.d("User changed mind, didn't click to upload button, deleted file: "+contribution.getLocalUri());
ContributionUtils.removeTemporaryFile(contribution.getLocalUri());
}
}
}
super.onStop();
}
}

View file

@ -1,254 +0,0 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import android.graphics.Point;
import android.net.Uri;
import android.os.Bundle;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.GridView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.view.SimpleDraweeView;
import butterknife.BindView;
import butterknife.ButterKnife;
import dagger.android.support.AndroidSupportInjection;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.utils.ViewUtil;
public class MultipleUploadListFragment extends Fragment {
public interface OnMultipleUploadInitiatedHandler {
void OnMultipleUploadInitiated();
}
@BindView(R.id.multipleShareBackground)
GridView photosGrid;
@BindView(R.id.multipleBaseTitle)
EditText baseTitle;
private PhotoDisplayAdapter photosAdapter;
private TitleTextWatcher textWatcher = new TitleTextWatcher();
private Point photoSize;
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
private OnMultipleUploadInitiatedHandler multipleUploadInitiatedHandler;
private boolean imageOnlyMode;
private static class UploadHolderView {
private Uri imageUri;
private SimpleDraweeView image;
private TextView title;
private RelativeLayout overlay;
}
@Override
public void onAttach(Context context) {
AndroidSupportInjection.inject(this);
super.onAttach(context);
}
private class PhotoDisplayAdapter extends BaseAdapter {
@Override
public int getCount() {
return detailProvider.getTotalMediaCount();
}
@Override
public Object getItem(int i) {
return detailProvider.getMediaAtPosition(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
UploadHolderView holder;
if (view == null) {
view = LayoutInflater.from(getContext()).inflate(R.layout.layout_upload_item, viewGroup, false);
holder = new UploadHolderView();
holder.image = view.findViewById(R.id.uploadImage);
holder.title = view.findViewById(R.id.uploadTitle);
holder.overlay = view.findViewById(R.id.uploadOverlay);
holder.image.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, photoSize.y));
holder.image.setHierarchy(GenericDraweeHierarchyBuilder
.newInstance(getResources())
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
R.drawable.ic_image_black_24dp, getContext().getTheme()))
.setFailureImage(VectorDrawableCompat.create(getResources(),
R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
.build());
view.setTag(holder);
} else {
holder = (UploadHolderView) view.getTag();
}
Contribution up = (Contribution) this.getItem(i);
if (holder.imageUri == null || !holder.imageUri.equals(up.getLocalUri())) {
holder.image.setImageURI(up.getLocalUri().toString());
holder.imageUri = up.getLocalUri();
}
if (!imageOnlyMode) {
holder.overlay.setVisibility(View.VISIBLE);
holder.title.setText(up.getFilename());
} else {
holder.overlay.setVisibility(View.GONE);
}
return view;
}
}
@Override
public void onStop() {
super.onStop();
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
View target = getActivity().getCurrentFocus();
ViewUtil.hideKeyboard(target);
}
// FIXME: Wrong result type
private Point calculatePicDimension(int count) {
DisplayMetrics screenMetrics = getResources().getDisplayMetrics();
int screenWidth = screenMetrics.widthPixels;
int screenHeight = screenMetrics.heightPixels;
int picWidth = Math.min((int) Math.sqrt(screenWidth * screenHeight / count), screenWidth);
picWidth = Math.min((int) (192 * screenMetrics.density), Math.max((int) (120 * screenMetrics.density), picWidth / 48 * 48));
int picHeight = Math.min(picWidth, (int) (192 * screenMetrics.density)); // Max Height is same as Contributions list
return new Point(picWidth, picHeight);
}
public void notifyDatasetChanged() {
if (photosAdapter != null) {
photosAdapter.notifyDataSetChanged();
}
}
public void setImageOnlyMode(boolean mode) {
imageOnlyMode = mode;
if (imageOnlyMode) {
baseTitle.setVisibility(View.GONE);
} else {
baseTitle.setVisibility(View.VISIBLE);
}
photosAdapter.notifyDataSetChanged();
photosGrid.setEnabled(!mode);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_multiple_uploads_list, container, false);
ButterKnife.bind(this,view);
photosAdapter = new PhotoDisplayAdapter();
photosGrid.setAdapter(photosAdapter);
photosGrid.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
photoSize = calculatePicDimension(detailProvider.getTotalMediaCount());
photosGrid.setColumnWidth(photoSize.x);
baseTitle.addTextChangedListener(textWatcher);
baseTitle.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
ViewUtil.hideKeyboard(v);
}
});
return view;
}
@Override
public void onDestroyView() {
baseTitle.removeTextChangedListener(textWatcher);
super.onDestroyView();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
menu.clear();
inflater.inflate(R.menu.fragment_multiple_upload_list, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_upload_multiple:
if (baseTitle.getText().toString().trim().isEmpty()) {
Toast.makeText(getContext(), R.string.add_set_name_toast, Toast.LENGTH_LONG).show();
return false;
}
multipleUploadInitiatedHandler.OnMultipleUploadInitiated();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity();
multipleUploadInitiatedHandler = (OnMultipleUploadInitiatedHandler) getActivity();
setHasOptionsMenu(true);
}
private class TitleTextWatcher implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence charSequence, int i1, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i1, int i2, int i3) {
for (int i = 0; i < detailProvider.getTotalMediaCount(); i++) {
Contribution up = (Contribution) detailProvider.getMediaAtPosition(i);
Boolean isDirty = (Boolean) up.getTag("isDirty");
if (isDirty == null || !isDirty) {
if (!TextUtils.isEmpty(charSequence)) {
up.setFilename(charSequence.toString() + " - " + ((Integer) up.getTag("sequence") + 1));
} else {
up.setFilename("");
}
}
}
detailProvider.notifyDatasetChanged();
}
@Override
public void afterTextChanged(Editable editable) {
}
}
}

View file

@ -1,674 +0,0 @@
package fr.free.nrw.commons.upload;
import android.Manifest;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.support.design.widget.FloatingActionButton;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.Toast;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.view.SimpleDraweeView;
import com.github.chrisbanes.photoview.PhotoView;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.modifications.CategoryModifier;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.CategoryApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ContributionUtils;
import fr.free.nrw.commons.utils.ExternalStorageUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED;
import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE;
import static fr.free.nrw.commons.upload.FileUtils.getSHA1;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
/**
* Activity for the title/desc screen after image is selected. Also starts processing image
* GPS coordinates or user location (if enabled in Settings) for category suggestions.
*/
public class ShareActivity
extends AuthenticatedActivity
implements SingleUploadFragment.OnUploadActionInitiated,
OnCategoriesSaveHandler,
ActivityCompat.OnRequestPermissionsResultCallback {
private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4;
//Had to make them class variables, to extract out the click listeners, also I see no harm in this
final Rect startBounds = new Rect();
final Rect finalBounds = new Rect();
final Point globalOffset = new Point();
@Inject
MediaWikiApi mwApi;
@Inject
CacheController cacheController;
@Inject
SessionManager sessionManager;
@Inject
UploadController uploadController;
@Inject
ModifierSequenceDao modifierSequenceDao;
@Inject
CategoryApi apiCall;
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
@Inject
@Named("default_preferences")
SharedPreferences prefs;
@Inject
GpsCategoryModel gpsCategoryModel;
@BindView(R.id.container)
FrameLayout flContainer;
@BindView(R.id.backgroundImage)
SimpleDraweeView backgroundImageView;
@BindView(R.id.media_map)
FloatingActionButton mapButton;
@BindView(R.id.media_upload_zoom_in)
FloatingActionButton zoomInButton;
@BindView(R.id.media_upload_zoom_out)
FloatingActionButton zoomOutButton;
@BindView(R.id.main_fab)
FloatingActionButton mainFab;
@BindView(R.id.expanded_image)
PhotoView expandedImageView;
private String source;
private String mimeType;
private CategorizationFragment categorizationFragment;
private Uri mediaUri;
private Uri contentProviderUri;
private Contribution contribution;
private GPSExtractor gpsObj;
private String decimalCoords;
private FileProcessor fileObj;
private boolean useNewPermissions = false;
private boolean storagePermitted = false;
private boolean locationPermitted = false;
private String title;
private String description;
private String wikiDataEntityId;
private boolean duplicateCheckPassed = false;
private boolean isNearbyUpload = false;
private Animator CurrentAnimator;
private long ShortAnimationDuration;
private boolean isFABOpen = false;
private float startScaleFinal;
private Bundle savedInstanceState;
private boolean isUploadFinalised = false; // Checks is user clicked to upload button or regret before this phase
private boolean isZoom = false;
/**
* Called when user taps the submit button.
* Requests Storage permission, if needed.
*/
@Override
public void uploadActionInitiated(String title, String description) {
this.title = title;
this.description = description;
if (sessionManager.getCurrentAccount() != null) {
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 (needsToRequestStoragePermission()) {
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_PERM_ON_SUBMIT_STORAGE);
} else {
uploadBegins();
}
} else {
uploadBegins();
}
}
else //Send user to login activity
{
Toast.makeText(this, "You need to login first!", Toast.LENGTH_SHORT).show();
Intent loginIntent = new Intent(ShareActivity.this, LoginActivity.class);
startActivity(loginIntent);
}
}
/**
* Checks whether storage permissions need to be requested.
* Permissions are needed if the file is not owned by this application, (e.g. shared from the Gallery)
*
* @return true if file is not owned by this application and permission hasn't been granted beforehand
*/
@RequiresApi(16)
private boolean needsToRequestStoragePermission() {
return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri)
&& (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED);
//return false;
}
/**
* Called after permission checks are done.
* Gets file metadata for category suggestions, displays toast, caches categories found, calls uploadController
*/
private void uploadBegins() {
fileObj.processFileCoordinates(locationPermitted);
Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
startingToast.show();
if (!fileObj.isCacheFound()) {
//Has to be called after apiCall.request()
cacheController.cacheCategory();
Timber.d("Cache the categories found");
}
uploadController.startUpload(title, contentProviderUri, mediaUri, description, mimeType, source, decimalCoords, wikiDataEntityId, c -> {
ShareActivity.this.contribution = c;
showPostUpload();
});
isUploadFinalised = true;
}
/**
* Starts CategorizationFragment after uploadBegins.
*/
private void showPostUpload() {
if (categorizationFragment == null) {
categorizationFragment = new CategorizationFragment();
}
getSupportFragmentManager().beginTransaction()
.replace(R.id.single_upload_fragment_container, categorizationFragment, "categorization")
.commit();
}
/**
* Send categories to modifications queue after they are selected
*
* @param categories categories selected
*/
@Override
public void onCategoriesSave(List<String> categories) {
if (categories.size() > 0) {
ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
modifierSequenceDao.save(categoriesSequence);
}
// FIXME: Make sure that the content provider is up
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default!
finish();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (contribution != null) {
outState.putParcelable("contribution", contribution);
}
}
@Override
protected void onAuthCookieAcquired(String authCookie) {
mwApi.setAuthCookie(authCookie);
}
@Override
protected void onAuthFailure() {
Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG);
failureToast.show();
finish();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
isUploadFinalised = false;
setContentView(R.layout.activity_share);
ButterKnife.bind(this);
initBack();
backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder
.newInstance(getResources())
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
R.drawable.ic_image_black_24dp, getTheme()))
.setFailureImage(VectorDrawableCompat.create(getResources(),
R.drawable.ic_error_outline_black_24dp, getTheme()))
.build());
if (!ExternalStorageUtils.isStoragePermissionGranted(this)) {
this.savedInstanceState = savedInstanceState;
ExternalStorageUtils.requestExternalStoragePermission(this);
return; // Postpone operation to do after getting permission
} else {
receiveImageIntent();
createContributionWithReceivedIntent(savedInstanceState);
}
}
@Override
protected void onStop() {
// If upload is not finalised with failure or success, but contribution is created,
// we have to remove temp file, to prevent using unnecessary memory
if (!isUploadFinalised) {
if (mediaUri != null) {
ContributionUtils.removeTemporaryFile(mediaUri);
}
}
super.onStop();
}
private void createContributionWithReceivedIntent(Bundle savedInstanceState) {
if (savedInstanceState != null) {
contribution = savedInstanceState.getParcelable("contribution");
}
requestAuthToken();
Timber.d("Uri: %s", mediaUri.toString());
Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory());
SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization");
if (shareView == null && categorizationFragment == null) {
shareView = new SingleUploadFragment();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.single_upload_fragment_container, shareView, "shareView")
.commitAllowingStateLoss();
}
uploadController.prepareService();
ContentResolver contentResolver = this.getContentResolver();
fileObj = new FileProcessor(mediaUri, contentResolver, this);
checkIfFileExists();
gpsObj = fileObj.processFileCoordinates(locationPermitted);
decimalCoords = fileObj.getDecimalCoords();
if (sessionManager.getCurrentAccount() == null) {
Toast.makeText(this, getString(R.string.login_alert_message), Toast.LENGTH_SHORT).show();
applicationPrefs.edit().putBoolean("login_skipped", false).apply();
Intent loginIntent = new Intent(ShareActivity.this, LoginActivity.class);
startActivity(loginIntent);
}
}
/**
* Receive intent from ContributionController.java when user selects picture to upload
*/
private void receiveImageIntent() {
Intent intent = getIntent();
if (Intent.ACTION_SEND.equals(intent.getAction())) {
mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
contentProviderUri = mediaUri;
mediaUri = ContributionUtils.saveFileBeingUploadedTemporarily(this, mediaUri);
if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
} else {
source = Contribution.SOURCE_EXTERNAL;
}
boolean isDirectUpload = intent.getBooleanExtra("isDirectUpload", false);
if (isDirectUpload) {
Timber.d("This was initiated by a direct upload from Nearby");
isNearbyUpload = true;
wikiDataEntityId = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF);
Timber.d("Received wikiDataEntityId from contribution controller %s", wikiDataEntityId);
}
mimeType = intent.getType();
}
if (mediaUri != null) {
backgroundImageView.setImageURI(mediaUri);
}
}
/**
* Function to display the zoom and map FAB
*/
private void showFABMenu() {
isFABOpen = true;
if (gpsObj != null && gpsObj.imageCoordsExists)
mapButton.setVisibility(View.VISIBLE);
zoomInButton.setVisibility(View.VISIBLE);
mainFab.animate().rotationBy(180);
mapButton.animate().translationY(-getResources().getDimension(R.dimen.second_fab));
zoomInButton.animate().translationY(-getResources().getDimension(R.dimen.first_fab));
}
/**
* Function to close the zoom and map FAB
*/
private void closeFABMenu() {
isFABOpen = false;
mainFab.animate().rotationBy(-180);
mapButton.animate().translationY(0);
zoomInButton.animate().translationY(0).setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
if (!isFABOpen) {
mapButton.setVisibility(View.GONE);
zoomInButton.setVisibility(View.GONE);
}
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
}
/**
* Checks if upload was initiated via Nearby
*
* @return true if upload was initiated via Nearby
*/
protected boolean isNearbyUpload() {
return isNearbyUpload;
}
/**
* Handles submit button permission request (for storage)
*
* @param requestCode type of request
* @param permissions permissions requested
* @param grantResults grant results
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Timber.d("onRequestPermissionsResult external storage permission granted");
// You can receive image intent and save image to a temp file only if ext storage permission is granted
receiveImageIntent();
createContributionWithReceivedIntent(savedInstanceState);
if (requestCode == REQUEST_PERM_ON_SUBMIT_STORAGE) {
checkIfFileExists();
//Uploading only begins if storage permission granted from arrow icon
uploadBegins();
}
} else {
finish();
}
}
/**
* Check if file user wants to upload already exists on Commons
*/
private void checkIfFileExists() {
if (!useNewPermissions || storagePermitted) {
if (!duplicateCheckPassed) {
//Test SHA1 of image to see if it matches SHA1 of a file on Commons
try {
InputStream inputStream = getContentResolver().openInputStream(mediaUri);
String fileSHA1 = getSHA1(inputStream);
Timber.d("Input stream created from %s", mediaUri.toString());
Timber.d("File SHA1 is: %s", fileSHA1);
ExistingFileAsync fileAsyncTask =
new ExistingFileAsync(new WeakReference<Activity>(this), fileSHA1, new WeakReference<Context>(this), result -> {
Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
duplicateCheckPassed = (result == DUPLICATE_PROCEED || result == NO_DUPLICATE);
if (duplicateCheckPassed) {
//image is not a duplicate, so now check if its a unwanted picture or not
fileObj.detectUnwantedPictures();
}
}, mwApi);
fileAsyncTask.execute();
} catch (IOException e) {
Timber.e(e, "IO Exception: ");
}
}
} else {
Timber.w("not ready for preprocessing: useNewPermissions=%s storage=%s location=%s",
useNewPermissions, storagePermitted, locationPermitted);
}
}
@Override
public void onPause() {
super.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
uploadController.cleanup();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
if (categorizationFragment != null && categorizationFragment.isVisible()) {
categorizationFragment.showBackButtonDialog();
} else {
onBackPressed();
}
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* Allows zooming in to the image about to be uploaded. Called when zoom FAB is tapped
*/
private void zoomImageFromThumb(final View thumbView, Uri imageuri) {
// If there's an animation in progress, cancel it immediately and proceed with this one.
if (CurrentAnimator != null) {
CurrentAnimator.cancel();
}
isZoom = true;
ViewUtil.hideKeyboard(ShareActivity.this.findViewById(R.id.titleEdit));
closeFABMenu();
mainFab.setVisibility(View.GONE);
InputStream input = null;
try {
input = this.getContentResolver().openInputStream(imageuri);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Zoom zoomObj = new Zoom(thumbView, flContainer, this.getContentResolver());
Bitmap scaledImage = zoomObj.createScaledImage(input, imageuri);
// Load the high-resolution "zoomed-in" image.
expandedImageView.setImageBitmap(scaledImage);
float startScale = zoomObj.adjustStartEndBounds(startBounds, finalBounds, globalOffset);
// Hide the thumbnail and show the zoomed-in view. When the animation
// begins, it will position the zoomed-in view in the place of the
// thumbnail.
thumbView.setAlpha(0f);
expandedImageView.setVisibility(View.VISIBLE);
zoomOutButton.setVisibility(View.VISIBLE);
zoomInButton.setVisibility(View.GONE);
// Set the pivot point for SCALE_X and SCALE_Y transformations
// to the top-left corner of the zoomed-in view (the default
// is the center of the view).
expandedImageView.setPivotX(0f);
expandedImageView.setPivotY(0f);
// Construct and run the parallel animation of the four translation and
// scale properties (X, Y, SCALE_X, and SCALE_Y).
AnimatorSet set = new AnimatorSet();
set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left, finalBounds.left))
.with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds.top))
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f))
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f));
set.setDuration(ShortAnimationDuration);
set.setInterpolator(new DecelerateInterpolator());
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
CurrentAnimator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
CurrentAnimator = null;
}
});
set.start();
CurrentAnimator = set;
// Upon clicking the zoomed-in image, it should zoom back down
// to the original bounds and show the thumbnail instead of
// the expanded image.
startScaleFinal = startScale;
}
/**
* Called when user taps the ^ FAB button, expands to show Zoom and Map
*/
@OnClick(R.id.main_fab)
public void onMainFabClicked() {
if (!isFABOpen) {
showFABMenu();
} else {
closeFABMenu();
}
}
@OnClick(R.id.media_upload_zoom_in)
public void onZoomInFabClicked() {
try {
zoomImageFromThumb(backgroundImageView, mediaUri);
} catch (Exception e) {
Timber.e(e);
}
}
@OnClick(R.id.media_upload_zoom_out)
public void onZoomOutFabClicked() {
if (CurrentAnimator != null) {
CurrentAnimator.cancel();
}
isZoom = false;
zoomOutButton.setVisibility(View.GONE);
mainFab.setVisibility(View.VISIBLE);
// Animate the four positioning/sizing properties in parallel,
// back to their original values.
AnimatorSet set = new AnimatorSet();
set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left))
.with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top))
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScaleFinal))
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScaleFinal));
set.setDuration(ShortAnimationDuration);
set.setInterpolator(new DecelerateInterpolator());
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//background image view is thumbView
backgroundImageView.setAlpha(1f);
expandedImageView.setVisibility(View.GONE);
CurrentAnimator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
//background image view is thumbView
backgroundImageView.setAlpha(1f);
expandedImageView.setVisibility(View.GONE);
CurrentAnimator = null;
}
});
set.start();
CurrentAnimator = set;
}
@OnClick(R.id.media_map)
public void onFabShowMapsClicked() {
if (gpsObj != null && gpsObj.imageCoordsExists) {
Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude());
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
mapIntent.setPackage("com.google.android.apps.maps");
startActivity(mapIntent);
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
if(isZoom) {
onZoomOutFabClicked();
return true;
}
}
return super.onKeyDown(keyCode,event);
}
}

View file

@ -0,0 +1,5 @@
package fr.free.nrw.commons.upload;
public interface SimilarImageInterface {
void showSimilarImageFragment(String originalFilePath, String possibleFilePath);
}

View file

@ -1,389 +0,0 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.Html;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.OnItemSelected;
import butterknife.OnTouch;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
import static android.view.MotionEvent.ACTION_UP;
public class SingleUploadFragment extends CommonsDaggerSupportFragment {
@BindView(R.id.titleEdit) EditText titleEdit;
@BindView(R.id.rv_descriptions) RecyclerView rvDescriptions;
@BindView(R.id.titleDescButton) Button titleDescButton;
@BindView(R.id.share_license_summary) TextView licenseSummaryView;
@BindView(R.id.licenseSpinner) Spinner licenseSpinner;
@Inject @Named("default_preferences") SharedPreferences prefs;
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
private String license;
private OnUploadActionInitiated uploadActionInitiatedHandler;
private TitleTextWatcher textWatcher = new TitleTextWatcher();
private DescriptionsAdapter descriptionsAdapter;
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.activity_share, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
//What happens when the 'submit' icon is tapped
case R.id.menu_upload_single:
if (titleEdit.getText().toString().trim().isEmpty()) {
Toast.makeText(getContext(), R.string.add_title_toast, Toast.LENGTH_LONG).show();
return false;
}
String title = titleEdit.getText().toString();
String descriptionsInVariousLanguages = getDescriptionsInAppropriateFormat();
//Save the title/desc in short-lived cache so next time this fragment is loaded, we can access these
prefs.edit()
.putString("Title", title)
.putString("Desc", new Gson().toJson(descriptionsAdapter
.getDescriptions()))//Description, now is not just a string, its a list of description objects
.apply();
uploadActionInitiatedHandler
.uploadActionInitiated(title, descriptionsInVariousLanguages);
return true;
}
return super.onOptionsItemSelected(item);
}
private String getDescriptionsInAppropriateFormat() {
List<Description> descriptions = descriptionsAdapter.getDescriptions();
StringBuilder descriptionsInAppropriateFormat = new StringBuilder();
for (Description description : descriptions) {
String individualDescription = String.format("{{%s|1=%s}}", description.getLanguageId(),
description.getDescriptionText());
descriptionsInAppropriateFormat.append(individualDescription);
}
return descriptionsInAppropriateFormat.toString();
}
private List<Description> getDescriptions() {
List<Description> descriptions = descriptionsAdapter.getDescriptions();
return descriptions;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false);
ButterKnife.bind(this, rootView);
initRecyclerView();
Intent activityIntent = getActivity().getIntent();
if (activityIntent.hasExtra("title")) {
titleEdit.setText(activityIntent.getStringExtra("title"));
}
if (activityIntent.hasExtra("description") && descriptionsAdapter.getDescriptions() != null
&& descriptionsAdapter.getDescriptions().size() > 0) {
descriptionsAdapter.getDescriptions().get(0)
.setDescriptionText(activityIntent.getStringExtra("description"));
descriptionsAdapter.notifyItemChanged(0);
}
ArrayList<String> licenseItems = new ArrayList<>();
licenseItems.add(getString(R.string.license_name_cc0));
licenseItems.add(getString(R.string.license_name_cc_by));
licenseItems.add(getString(R.string.license_name_cc_by_sa));
licenseItems.add(getString(R.string.license_name_cc_by_four));
licenseItems.add(getString(R.string.license_name_cc_by_sa_four));
license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
// If this is a direct upload from Nearby, autofill title and desc fields with the Place's values
boolean isNearbyUpload = ((ShareActivity) getActivity()).isNearbyUpload();
if (isNearbyUpload) {
String imageTitle = directPrefs.getString("Title", "");
String imageDesc = directPrefs.getString("Desc", "");
String imageCats = directPrefs.getString("Category", "");
Timber.d("Image title: " + imageTitle + ", image desc: " + imageDesc + ", image categories: " + imageCats);
titleEdit.setText(imageTitle);
if (descriptionsAdapter.getDescriptions() != null
&& descriptionsAdapter.getDescriptions().size() > 0) {
descriptionsAdapter.getDescriptions().get(0).setDescriptionText(imageDesc);
descriptionsAdapter.notifyItemChanged(0);
}
}
// check if this is the first time we have uploaded
if (prefs.getString("Title", "").trim().length() == 0
&& prefs.getString("Desc", "").trim().length() == 0) {
titleDescButton.setVisibility(View.GONE);
}
Timber.d(license);
ArrayAdapter<String> adapter;
if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme", false)) {
// dark theme
adapter = new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_dropdown_item, licenseItems);
} else {
// light theme
adapter = new ArrayAdapter<>(getActivity(), R.layout.light_simple_spinner_dropdown_item, licenseItems);
}
licenseSpinner.setAdapter(adapter);
int position = licenseItems.indexOf(getString(Utils.licenseNameFor(license)));
// Check position is valid
if (position < 0) {
Timber.d("Invalid position: %d. Using default license", position);
position = 4;
}
Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license)));
licenseSpinner.setSelection(position);
titleEdit.addTextChangedListener(textWatcher);
titleEdit.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
ViewUtil.hideKeyboard(v);
}
});
setLicenseSummary(license);
return rootView;
}
private void initRecyclerView() {
descriptionsAdapter = new DescriptionsAdapter();
descriptionsAdapter.setCallback(this::showInfoAlert);
descriptionsAdapter.setLanguages(getLocaleSupportedByDevice());
rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
rvDescriptions.setAdapter(descriptionsAdapter);
}
private List<Language> getLocaleSupportedByDevice() {
List<Language> languages = new ArrayList<>();
Locale[] localesArray = Locale.getAvailableLocales();
List<Locale> locales = Arrays.asList(localesArray);
for (Locale locale : locales) {
languages.add(new Language(locale));
}
return languages;
}
@Override
public void onDestroyView() {
titleEdit.removeTextChangedListener(textWatcher);
super.onDestroyView();
}
@OnItemSelected(R.id.licenseSpinner)
void onLicenseSelected(AdapterView<?> parent, View view, int position, long id) {
String licenseName = parent.getItemAtPosition(position).toString();
// Set selected color to white because it should be readable on random images.
TextView selectedText = (TextView) licenseSpinner.getChildAt(0);
if (selectedText != null) {
selectedText.setTextColor(Color.WHITE);
selectedText.setBackgroundColor(Color.TRANSPARENT);
}
String license;
if (getString(R.string.license_name_cc0).equals(licenseName)) {
license = Prefs.Licenses.CC0;
} else if (getString(R.string.license_name_cc_by).equals(licenseName)) {
license = Prefs.Licenses.CC_BY_3;
} else if (getString(R.string.license_name_cc_by_sa).equals(licenseName)) {
license = Prefs.Licenses.CC_BY_SA_3;
} else if (getString(R.string.license_name_cc_by_four).equals(licenseName)) {
license = Prefs.Licenses.CC_BY_4;
} else if (getString(R.string.license_name_cc_by_sa_four).equals(licenseName)) {
license = Prefs.Licenses.CC_BY_SA_4;
} else {
throw new IllegalStateException("Unknown licenseName: " + licenseName);
}
setLicenseSummary(license);
prefs.edit()
.putString(Prefs.DEFAULT_LICENSE, license)
.apply();
}
@OnClick(R.id.titleDescButton)
void setTitleDescButton() {
//Retrieve last title and desc entered
String title = prefs.getString("Title", "");
String descriptionJson = prefs.getString("Desc", "");
Timber.d("Title: %s, Desc: %s", title, descriptionJson);
titleEdit.setText(title);
Type typeOfDest = new TypeToken<List<Description>>() {
}.getType();
List<Description> descriptions = new Gson().fromJson(descriptionJson, typeOfDest);
descriptionsAdapter.setDescriptions(descriptions);
}
/**
* Copied from https://stackoverflow.com/a/26269435/8065933
*/
@OnTouch(R.id.titleEdit)
boolean titleInfo(View view, MotionEvent motionEvent) {
final int value;
if (ViewCompat.getLayoutDirection(getView()) == ViewCompat.LAYOUT_DIRECTION_LTR) {
value = titleEdit.getRight() - titleEdit.getCompoundDrawables()[2].getBounds().width();
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
showInfoAlert(R.string.media_detail_title, R.string.title_info);
return true;
}
}
else {
value = titleEdit.getLeft() + titleEdit.getCompoundDrawables()[0].getBounds().width();
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
showInfoAlert(R.string.media_detail_title, R.string.title_info);
return true;
}
}
return false;
}
@SuppressLint("StringFormatInvalid")
private void setLicenseSummary(String license) {
String licenseHyperLink = "<a href='" + licenseUrlFor(license)+"'>"+ getString(Utils.licenseNameFor(license)) + "</a><br>";
licenseSummaryView.setMovementMethod(LinkMovementMethod.getInstance());
licenseSummaryView.setText(Html.fromHtml(getString(R.string.share_license_summary, licenseHyperLink)));
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
uploadActionInitiatedHandler = (OnUploadActionInitiated) getActivity();
}
@Override
public void onStop() {
super.onStop();
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
View target = getActivity().getCurrentFocus();
ViewUtil.hideKeyboard(target);
}
@NonNull
private String licenseUrlFor(String license) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
return "https://creativecommons.org/licenses/by/3.0/";
case Prefs.Licenses.CC_BY_4:
return "https://creativecommons.org/licenses/by/4.0/";
case Prefs.Licenses.CC_BY_SA_3:
return "https://creativecommons.org/licenses/by-sa/3.0/";
case Prefs.Licenses.CC_BY_SA_4:
return "https://creativecommons.org/licenses/by-sa/4.0/";
case Prefs.Licenses.CC0:
return "https://creativecommons.org/publicdomain/zero/1.0/";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
public interface OnUploadActionInitiated {
void uploadActionInitiated(String title, String description);
}
private class TitleTextWatcher implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void afterTextChanged(Editable editable) {
if (getActivity() != null) {
getActivity().invalidateOptionsMenu();
}
}
}
private void showInfoAlert (int titleStringID, int messageStringID){
new AlertDialog.Builder(getContext())
.setTitle(titleStringID)
.setMessage(messageStringID)
.setCancelable(true)
.setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
.create()
.show();
}
@OnClick(R.id.ll_add_description)
public void onLLAddDescriptionClicked() {
descriptionsAdapter.addDescription(new Description());
rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1);
}
}

View file

@ -1,48 +1,88 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.BiMap;
public class SpinnerLanguagesAdapter extends ArrayAdapter {
private final int resource;
private final LayoutInflater layoutInflater;
List<Language> languages;
private List<String> languageNamesList;
private List<String> languageCodesList;
private final BiMap<AdapterView, String> selectedLanguages;
public String selectedLangCode="";
public SpinnerLanguagesAdapter(@NonNull Context context,
int resource) {
int resource, BiMap<AdapterView, String> selectedLanguages) {
super(context, resource);
this.resource = resource;
this.layoutInflater = LayoutInflater.from(context);
languages = new ArrayList<>();
languageNamesList = new ArrayList<>();
languageCodesList = new ArrayList<>();
prepareLanguages();
this.selectedLanguages = selectedLanguages;
}
public void setLanguages(List<Language> languages) {
this.languages = languages;
private void prepareLanguages() {
List<Language> languages = getLocaleSupportedByDevice();
for(Language language: languages) {
if(!languageCodesList.contains(language.getLocale().getLanguage())) {
languageNamesList.add(language.getLocale().getDisplayName());
languageCodesList.add(language.getLocale().getLanguage());
}
}
}
private List<Language> getLocaleSupportedByDevice() {
List<Language> languages = new ArrayList<>();
Locale[] localesArray = Locale.getAvailableLocales();
for (Locale locale : localesArray) {
languages.add(new Language(locale));
}
Collections.sort(languages, (language, t1) -> language.getLocale().getDisplayName()
.compareTo(t1.getLocale().getDisplayName()));
return languages;
}
@Override
public boolean isEnabled(int position) {
return !languageCodesList.get(position).isEmpty()&&
(!selectedLanguages.containsKey(languageCodesList.get(position)) ||
languageCodesList.get(position).equals(selectedLangCode));
}
@Override
public int getCount() {
return languages.size();
return languageNamesList.size();
}
@Override
public View getDropDownView(int position, @Nullable View convertView,
@NonNull ViewGroup parent) {
@NonNull ViewGroup parent) {
View view = layoutInflater.inflate(resource, parent, false);
ViewHolder holder = new ViewHolder(view);
holder.init(position, true);
@ -75,19 +115,40 @@ public class SpinnerLanguagesAdapter extends ArrayAdapter {
}
public void init(int position, boolean isDropDownView) {
Language language = languages.get(position);
if (!isDropDownView) {
view.setVisibility(View.GONE);
tvLanguage.setText(
language.getLocale().getLanguage());
if(languageCodesList.get(position).length()>2)
tvLanguage.setText(languageCodesList.get(position).subSequence(0,2));
else
tvLanguage.setText(languageCodesList.get(position));
} else {
view.setVisibility(View.VISIBLE);
tvLanguage.setText(
String.format("%s [%s]", language.getLocale().getDisplayName(),
language.getLocale().getLanguage()));
if (languageCodesList.get(position).isEmpty()) {
tvLanguage.setText(languageNamesList.get(position));
tvLanguage.setTextColor(Color.GRAY);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
tvLanguage.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
}
} else {
tvLanguage.setText(
String.format("%s [%s]", languageNamesList.get(position), languageCodesList.get(position)));
if(selectedLanguages.containsKey(languageCodesList.get(position))&&
!languageCodesList.get(position).equals(selectedLangCode))
tvLanguage.setTextColor(Color.GRAY);
else
tvLanguage.setTextColor(Color.BLACK);
}
}
}
}
String getLanguageCode(int position) {
return languageCodesList.get(position);
}
int getIndexOfUserDefaultLocale(Context context) {
return languageCodesList.indexOf(context.getResources().getConfiguration().locale.getLanguage());
}
}

View file

@ -0,0 +1,5 @@
package fr.free.nrw.commons.upload;
public interface ThumbnailClickedListener {
void thumbnailClicked(UploadModel.UploadItem content);
}

View file

@ -0,0 +1,37 @@
package fr.free.nrw.commons.upload;
import android.text.TextUtils;
import io.reactivex.subjects.BehaviorSubject;
import timber.log.Timber;
class Title{
private String titleText;
private boolean set;
@Override
public String toString() {
return titleText;
}
public void setTitleText(String titleText) {
this.titleText = titleText;
if (!TextUtils.isEmpty(titleText)) {
set = true;
}
}
public boolean isSet() {
return set;
}
public void setSet(boolean set) {
this.set = set;
}
public boolean isEmpty() {
return titleText==null || titleText.isEmpty();
}
}

View file

@ -0,0 +1,607 @@
package fr.free.nrw.commons.upload;
import android.Manifest;
import android.animation.LayoutTransition;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.constraint.ConstraintLayout;
import android.support.design.widget.TextInputLayout;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.CardView;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Html;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewFlipper;
import com.github.chrisbanes.photoview.PhotoView;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxTextView;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
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.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.category.CategoriesModel;
import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.StringUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static fr.free.nrw.commons.utils.ImageUtils.Result;
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
public class UploadActivity extends AuthenticatedActivity implements UploadView, SimilarImageInterface {
@Inject InputMethodManager inputMethodManager;
@Inject MediaWikiApi mwApi;
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
@Inject UploadPresenter presenter;
@Inject CategoriesModel categoriesModel;
// Main GUI
@BindView(R.id.backgroundImage) PhotoView background;
@BindView(R.id.activity_upload_cards) ConstraintLayout cardLayout;
@BindView(R.id.view_flipper) ViewFlipper viewFlipper;
// Top Card
@BindView(R.id.top_card) CardView topCard;
@BindView(R.id.top_card_expand_button) ImageView topCardExpandButton;
@BindView(R.id.top_card_title) TextView topCardTitle;
@BindView(R.id.top_card_thumbnails) RecyclerView topCardThumbnails;
// Bottom Card
@BindView(R.id.bottom_card) CardView bottomCard;
@BindView(R.id.bottom_card_expand_button) ImageView bottomCardExpandButton;
@BindView(R.id.bottom_card_title) TextView bottomCardTitle;
@BindView(R.id.bottom_card_subtitle) TextView bottomCardSubtitle;
@BindView(R.id.bottom_card_next) Button next;
@BindView(R.id.bottom_card_previous) Button previous;
@BindView(R.id.bottom_card_add_desc) Button bottomCardAddDescription;
//Right Card
@BindView(R.id.right_card) CardView rightCard;
@BindView(R.id.right_card_expand_button) ImageView rightCardExpandButton;
@BindView(R.id.right_card_map_button) View rightCardMapButton;
// Category Search
@BindView(R.id.categories_title) TextView categoryTitle;
@BindView(R.id.category_next) Button categoryNext;
@BindView(R.id.category_previous) Button categoryPrevious;
@BindView(R.id.categoriesSearchInProgress) ProgressBar categoriesSearchInProgress;
@BindView(R.id.category_search) EditText categoriesSearch;
@BindView(R.id.category_search_container) TextInputLayout categoriesSearchContainer;
@BindView(R.id.categories) RecyclerView categoriesList;
// Final Submission
@BindView(R.id.license_title) TextView licenseTitle;
@BindView(R.id.share_license_summary) TextView licenseSummary;
@BindView(R.id.media_upload_policy) TextView licensePolicy;
@BindView(R.id.license_list) Spinner licenseSpinner;
@BindView(R.id.submit) Button submit;
@BindView(R.id.license_previous) Button licensePrevious;
@BindView(R.id.rv_descriptions) RecyclerView rvDescriptions;
private DescriptionsAdapter descriptionsAdapter;
private RVRendererAdapter<CategoryItem> categoriesAdapter;
private CompositeDisposable compositeDisposable;
DexterPermissionObtainer dexterPermissionObtainer;
@SuppressLint("CheckResult")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_upload);
ButterKnife.bind(this);
compositeDisposable = new CompositeDisposable();
configureLayout();
configureTopCard();
configureBottomCard();
initRecyclerView();
configureRightCard();
configureNavigationButtons();
configureCategories();
configureLicenses();
presenter.init();
dexterPermissionObtainer = new DexterPermissionObtainer(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
getString(R.string.storage_permission),
getString(R.string.write_storage_permission_rationale_for_image_share));
dexterPermissionObtainer.confirmStoragePermissions().subscribe(this::receiveSharedItems);
}
@Override
public boolean checkIfLoggedIn() {
if (!sessionManager.isUserLoggedIn()) {
Timber.d("Current account is null");
ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in));
Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class);
startActivity(loginIntent);
return false;
}
return true;
}
@Override
protected void onDestroy() {
presenter.cleanup();
super.onDestroy();
}
@Override
protected void onResume() {
super.onResume();
checkIfLoggedIn();
compositeDisposable.add(
dexterPermissionObtainer.confirmStoragePermissions()
.subscribe(() -> presenter.addView(this)));
compositeDisposable.add(
RxTextView.textChanges(categoriesSearch)
.doOnEach(v -> categoriesSearchContainer.setError(null))
.takeUntil(RxView.detaches(categoriesSearch))
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(filter -> updateCategoryList(filter.toString()), Timber::e)
);
}
@Override
protected void onPause() {
presenter.removeView();
compositeDisposable.dispose();
compositeDisposable = new CompositeDisposable();
super.onPause();
}
@Override
public void updateThumbnails(List<UploadModel.UploadItem> uploads) {
int uploadCount = uploads.size();
topCardThumbnails.setAdapter(new UploadThumbnailsAdapterFactory(presenter::thumbnailClicked).create(uploads));
topCardTitle.setText(getResources().getQuantityString(R.plurals.upload_count_title, uploadCount, uploadCount));
}
@Override
public void updateRightCardContent(boolean gpsPresent) {
if(gpsPresent){
rightCardMapButton.setVisibility(View.VISIBLE);
}else{
rightCardMapButton.setVisibility(View.GONE);
}
//The card should be disabled if it has no buttons.
setRightCardVisibility(gpsPresent);
}
@Override
public void updateBottomCardContent(int currentStep,
int stepCount,
UploadModel.UploadItem uploadItem,
boolean isShowingItem) {
String cardTitle = getResources().getString(R.string.step_count, currentStep, stepCount);
String cardSubTitle = getResources().getString(R.string.image_in_set_label, currentStep);
bottomCardTitle.setText(cardTitle);
bottomCardSubtitle.setText(cardSubTitle);
categoryTitle.setText(cardTitle);
licenseTitle.setText(cardTitle);
if(isShowingItem) {
descriptionsAdapter.setItems(uploadItem.title, uploadItem.descriptions);
rvDescriptions.setAdapter(descriptionsAdapter);
}
}
@Override
public void updateLicenses(List<String> licenses, String selectedLicense) {
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, licenses);
licenseSpinner.setAdapter(adapter);
int position = licenses.indexOf(getString(Utils.licenseNameFor(selectedLicense)));
// Check position is valid
if (position < 0) {
Timber.d("Invalid position: %d. Using default license", position);
position = licenses.size() - 1;
}
Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(selectedLicense)));
licenseSpinner.setSelection(position);
}
@SuppressLint("StringFormatInvalid")
@Override
public void updateLicenseSummary(String selectedLicense) {
String licenseHyperLink = "<a href='" + Utils.licenseUrlFor(selectedLicense)+"'>" +
getString(Utils.licenseNameFor(selectedLicense)) + "</a><br>";
licenseSummary.setMovementMethod(LinkMovementMethod.getInstance());
licenseSummary.setText(
Html.fromHtml(
getString(R.string.share_license_summary, licenseHyperLink)));
}
@Override
public void updateTopCardContent() {
RecyclerView.Adapter adapter = topCardThumbnails.getAdapter();
if (adapter != null) {
adapter.notifyDataSetChanged();
}
}
@Override
public void setNextEnabled(boolean available) {
next.setEnabled(available);
categoryNext.setEnabled(available);
}
@Override
public void setSubmitEnabled(boolean available) {
submit.setEnabled(available);
}
@Override
public void setPreviousEnabled(boolean available) {
previous.setEnabled(available);
categoryPrevious.setEnabled(available);
licensePrevious.setEnabled(available);
}
@Override
public void setTopCardState(boolean state) {
updateCardState(state, topCardExpandButton, topCardThumbnails);
}
@Override
public void setTopCardVisibility(boolean visible) {
topCard.setVisibility(visible ? View.VISIBLE : View.GONE);
}
@Override
public void setBottomCardVisibility(boolean visible) {
bottomCard.setVisibility(visible ? View.VISIBLE : View.GONE);
}
@Override
public void setRightCardVisibility(boolean visible) {
rightCard.setVisibility(visible ? View.VISIBLE : View.GONE);
}
@Override
public void setBottomCardVisibility(@UploadPage int page) {
if (page == TITLE_CARD) {
viewFlipper.setDisplayedChild(0);
} else if (page == CATEGORIES) {
viewFlipper.setDisplayedChild(1);
} else if (page == LICENSE) {
viewFlipper.setDisplayedChild(2);
dismissKeyboard();
} else if (page == PLEASE_WAIT) {
viewFlipper.setDisplayedChild(3);
}
}
@Override
public void setBottomCardState(boolean state) {
updateCardState(state, bottomCardExpandButton, rvDescriptions, previous, next, bottomCardAddDescription);
}
@Override
public void setRightCardState(boolean state) {
rightCardExpandButton.animate().rotation(rightCardExpandButton.getRotation() + (state ? -180 : 180)).start();
//Add all items in rightCard here
rightCardMapButton.setVisibility(state ? View.VISIBLE : View.GONE);
}
@Override
public void setBackground(Uri mediaUri) {
background.setImageURI(mediaUri);
}
@Override
public void dismissKeyboard() {
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
// verify if the soft keyboard is open
if (imm != null && imm.isAcceptingText() && getCurrentFocus() != null) {
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}
}
@Override
public void showBadPicturePopup(@Result int result) {
String errorMessageForResult = getErrorMessageForResult(this, result);
if (StringUtils.isNullOrWhiteSpace(errorMessageForResult)) {
return;
}
DialogUtil.showAlertDialog(this,
getString(R.string.warning),
errorMessageForResult,
() -> presenter.deletePicture(),
() -> presenter.keepPicture());
}
@Override
public void showDuplicatePicturePopup() {
DialogUtil.showAlertDialog(this,
getString(R.string.warning),
String.format(getString(R.string.upload_title_duplicate), presenter.getCurrentImageFileName()),
null,
() -> {
presenter.keepPicture();
presenter.handleNext(descriptionsAdapter.getTitle(), getDescriptions());
});
}
public void showNoCategorySelectedWarning() {
DialogUtil.showAlertDialog(this,
getString(R.string.no_categories_selected),
getString(R.string.no_categories_selected_warning_desc),
getString(R.string.no_go_back),
getString(R.string.yes_submit),
null,
() -> presenter.handleCategoryNext(categoriesModel, true));
}
@Override
public void launchMapActivity(String decCoords) {
Utils.handleGeoCoordinates(this, decCoords);
}
@Override
public void showErrorMessage(int resourceId) {
ViewUtil.showShortToast(this, resourceId);
}
@Override
public void initDefaultCategories() {
updateCategoryList("");
}
@Override
protected void onAuthCookieAcquired(String authCookie) {
mwApi.setAuthCookie(authCookie);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) {
dexterPermissionObtainer.onManualPermissionReturned();
}
}
@Override
protected void onAuthFailure() {
Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG).show();
finish();
}
private void configureLicenses() {
licenseSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String licenseName = parent.getItemAtPosition(position).toString();
presenter.selectLicense(licenseName);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
presenter.selectLicense(null);
}
});
}
private void configureLayout() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
cardLayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
}
background.setScaleType(ImageView.ScaleType.CENTER_CROP);
background.setOnScaleChangeListener((scaleFactor, x, y) -> presenter.closeAllCards());
}
private void configureTopCard() {
topCardExpandButton.setOnClickListener(v -> presenter.toggleTopCardState());
topCardThumbnails.setLayoutManager(new LinearLayoutManager(this,
LinearLayoutManager.HORIZONTAL, false));
}
private void configureBottomCard() {
bottomCardExpandButton.setOnClickListener(v -> presenter.toggleBottomCardState());
bottomCardAddDescription.setOnClickListener(v -> addNewDescription());
}
private void addNewDescription() {
descriptionsAdapter.addDescription(new Description());
rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1);
}
private void configureRightCard() {
rightCardExpandButton.setOnClickListener(v -> presenter.toggleRightCardState());
rightCardMapButton.setOnClickListener(v -> presenter.openCoordinateMap());
}
private void configureNavigationButtons() {
// Navigation next / previous for each image as we're collecting title + description
next.setOnClickListener(v -> {
setTitleAndDescriptions();
presenter.handleNext(descriptionsAdapter.getTitle(),
descriptionsAdapter.getDescriptions());
});
previous.setOnClickListener(v -> presenter.handlePrevious());
// Next / previous for the category selection currentPage
categoryNext.setOnClickListener(v -> presenter.handleCategoryNext(categoriesModel, false));
categoryPrevious.setOnClickListener(v -> presenter.handlePrevious());
// Finally, the previous / submit buttons on the final currentPage of the wizard
licensePrevious.setOnClickListener(v -> presenter.handlePrevious());
submit.setOnClickListener(v -> {
Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG).show();
presenter.handleSubmit(categoriesModel);
finish();
});
}
private void setTitleAndDescriptions() {
List<Description> descriptions = descriptionsAdapter.getDescriptions();
Timber.d("Descriptions size is %d are %s", descriptions.size(), descriptions);
}
private void configureCategories() {
categoriesAdapter = new UploadCategoriesAdapterFactory(categoriesModel).create(new ArrayList<>());
categoriesList.setLayoutManager(new LinearLayoutManager(this));
categoriesList.setAdapter(categoriesAdapter);
}
@SuppressLint("CheckResult")
private void updateCategoryList(String filter) {
List<String> imageTitleList = presenter.getImageTitleList();
Observable.fromIterable(categoriesModel.getSelectedCategories())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(disposable -> {
categoriesSearchInProgress.setVisibility(View.VISIBLE);
categoriesSearchContainer.setError(null);
categoriesAdapter.clear();
})
.observeOn(Schedulers.io())
.concatWith(
categoriesModel.searchAll(filter, imageTitleList)
.mergeWith(categoriesModel.searchCategories(filter, imageTitleList))
.concatWith(TextUtils.isEmpty(filter)
? categoriesModel.defaultCategories(imageTitleList) : Observable.empty())
)
.filter(categoryItem -> !categoriesModel.containsYear(categoryItem.getName()))
.distinct()
.sorted(categoriesModel.sortBySimilarity(filter))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
s -> categoriesAdapter.add(s),
Timber::e,
() -> {
categoriesAdapter.notifyDataSetChanged();
categoriesSearchInProgress.setVisibility(View.GONE);
if (categoriesAdapter.getItemCount() == categoriesModel.selectedCategoriesCount()
&& !categoriesSearch.getText().toString().isEmpty()) {
categoriesSearchContainer.setError("No categories found");
}
}
);
}
private void receiveSharedItems() {
Intent intent = getIntent();
String mimeType = intent.getType();
String source;
if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
} else {
source = Contribution.SOURCE_EXTERNAL;
}
if (Intent.ACTION_SEND.equals(intent.getAction())) {
Uri mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (intent.getBooleanExtra("isDirectUpload", false)) {
String imageTitle = directPrefs.getString("Title", "");
String imageDesc = directPrefs.getString("Desc", "");
Timber.i("Received direct upload with title %s and description %s", imageTitle, imageDesc);
String wikidataEntityIdPref = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF);
presenter.receiveDirect(mediaUri, mimeType, source, wikidataEntityIdPref, imageTitle, imageDesc);
} else {
Timber.i("Received single upload");
presenter.receive(mediaUri, mimeType, source);
}
} else if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
ArrayList<Uri> urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
Timber.i("Received multiple upload %s", urisList.size());
presenter.receive(urisList, mimeType, source);
}
}
private void updateCardState(boolean state, ImageView button, View... content) {
button.animate().rotation(button.getRotation() + (state ? 180 : -180)).start();
if (content != null) {
for (View view : content) {
view.setVisibility(state ? View.VISIBLE : View.GONE);
}
}
}
@Override
public List<Description> getDescriptions() {
return descriptionsAdapter.getDescriptions();
}
private void initRecyclerView() {
descriptionsAdapter = new DescriptionsAdapter(this);
descriptionsAdapter.setCallback(this::showInfoAlert);
rvDescriptions.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
rvDescriptions.setAdapter(descriptionsAdapter);
addNewDescription();
}
private void showInfoAlert(int titleStringID, int messageStringId, String... formatArgs) {
new AlertDialog.Builder(this)
.setTitle(titleStringID)
.setMessage(getString(messageStringId, (Object[]) formatArgs))
.setCancelable(true)
.setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
.create()
.show();
}
@Override
public void showSimilarImageFragment(String originalFilePath, String possibleFilePath) {
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
Bundle args = new Bundle();
args.putString("originalImagePath", originalFilePath);
args.putString("possibleImagePath", possibleFilePath);
newFragment.setArguments(args);
newFragment.show(getSupportFragmentManager(), "dialog");
}
}

View file

@ -0,0 +1,27 @@
package fr.free.nrw.commons.upload;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
import java.util.Collections;
import java.util.List;
import fr.free.nrw.commons.category.CategoryClickedListener;
import fr.free.nrw.commons.category.CategoryItem;
public class UploadCategoriesAdapterFactory {
private final CategoryClickedListener listener;
public UploadCategoriesAdapterFactory(CategoryClickedListener listener) {
this.listener = listener;
}
public RVRendererAdapter<CategoryItem> create(List<CategoryItem> placeList) {
RendererBuilder<CategoryItem> builder = new RendererBuilder<CategoryItem>()
.bind(CategoryItem.class, new UploadCategoriesRenderer(listener));
ListAdapteeCollection<CategoryItem> collection = new ListAdapteeCollection<>(
placeList != null ? placeList : Collections.emptyList());
return new RVRendererAdapter<>(builder, collection);
}
}

View file

@ -0,0 +1,52 @@
package fr.free.nrw.commons.upload;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import com.pedrogomez.renderers.Renderer;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryClickedListener;
import fr.free.nrw.commons.category.CategoryItem;
public class UploadCategoriesRenderer extends Renderer<CategoryItem> {
@BindView(R.id.tvName) CheckBox checkedView;
private final CategoryClickedListener listener;
UploadCategoriesRenderer(CategoryClickedListener listener) {
this.listener = listener;
}
@Override
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
return layoutInflater.inflate(R.layout.layout_upload_categories_item, viewGroup, false);
}
@Override
protected void setUpView(View view) {
ButterKnife.bind(this, view);
}
@Override
protected void hookListeners(View view) {
view.setOnClickListener(v -> {
CategoryItem item = getContent();
item.setSelected(!item.isSelected());
checkedView.setChecked(item.isSelected());
if (listener != null) {
listener.categoryClicked(item);
}
});
}
@Override
public void render() {
CategoryItem item = getContent();
checkedView.setChecked(item.isSelected());
checkedView.setText(item.getName());
}
}

View file

@ -23,7 +23,6 @@ import java.io.InputStream;
import java.util.Date;
import java.util.concurrent.Executors;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
@ -87,49 +86,11 @@ public class UploadController {
/**
* Starts a new upload task.
* @param title the title of the contribution
* @param mediaUri the media URI of the contribution
* @param description the description of the contribution
* @param mimeType the MIME type of the contribution
* @param source the source of the contribution
* @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615")
* @param wikiDataEntityId
* @param onComplete the progress tracker
*
* @param contribution the contribution object
*/
public void startUpload(String title, Uri contentProviderUri, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) {
Contribution contribution;
//TODO: Modify this to include coords
contribution = new Contribution(mediaUri, null, title, description, -1,
null, null, sessionManager.getCurrentAccount().name,
CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords);
contribution.setTag("mimeType", mimeType);
contribution.setSource(source);
Timber.d("Wikidata entity ID received from Share activity is %s", wikiDataEntityId);
//TODO: Modify this to include coords
Account currentAccount = sessionManager.getCurrentAccount();
if(currentAccount == null) {
Timber.d("Current account is null");
ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in));
sessionManager.forceLogin(context);
return;
}
contribution = new Contribution(mediaUri, null, title, description, -1,
null, null, currentAccount.name,
CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords);
contribution.setTag("mimeType", mimeType);
contribution.setSource(source);
contribution.setWikiDataEntityId(wikiDataEntityId);
contribution.setContentProviderUri(contentProviderUri);
//Calls the next overloaded method
startUpload(contribution, onComplete);
public void startUpload(Contribution contribution) {
startUpload(contribution, c -> {});
}
/**
@ -142,7 +103,14 @@ public class UploadController {
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
//Set creator, desc, and license
if (TextUtils.isEmpty(contribution.getCreator())) {
contribution.setCreator(sessionManager.getCurrentAccount().name);
Account currentAccount = sessionManager.getCurrentAccount();
if (currentAccount == null) {
Timber.d("Current account is null");
ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in));
sessionManager.forceLogin(context);
return;
}
contribution.setCreator(currentAccount.name);
}
if (contribution.getDescription() == null) {
@ -163,8 +131,6 @@ public class UploadController {
long length;
ContentResolver contentResolver = context.getContentResolver();
try {
//TODO: understand do we really need this code
if (contribution.getDataLength() <= 0) {
Timber.d("UploadController/doInBackground, contribution.getLocalUri():" + contribution.getLocalUri());
AssetFileDescriptor assetFileDescriptor = contentResolver
@ -218,7 +184,7 @@ public class UploadController {
contribution.setDateCreated(new Date());
}
}
return contribution;
return contribution;
}
@Override

View file

@ -0,0 +1,400 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.graphics.BitmapRegionDecoder;
import android.net.Uri;
import android.support.annotation.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.utils.ImageUtils;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.BehaviorSubject;
import timber.log.Timber;
public class UploadModel {
private MediaWikiApi mwApi;
private static UploadItem DUMMY = new UploadItem(Uri.EMPTY, "", "", GPSExtractor.DUMMY, "", null,-1l) {
};
private final SharedPreferences prefs;
private final List<String> licenses;
private String license;
private final Map<String, String> licensesByName;
private List<UploadItem> items = new ArrayList<>();
private boolean topCardState = true;
private boolean bottomCardState = true;
private boolean rightCardState = true;
private int currentStepIndex = 0;
private Context context;
private ContentResolver contentResolver;
private boolean useExtStorage;
private Disposable badImageSubscription;
@Inject
SessionManager sessionManager;
private Uri currentMediaUri;
@Inject
UploadModel(@Named("licenses") List<String> licenses,
@Named("default_preferences") SharedPreferences prefs,
@Named("licenses_by_name") Map<String, String> licensesByName,
Context context,
MediaWikiApi mwApi) {
this.licenses = licenses;
this.prefs = prefs;
this.license = Prefs.Licenses.CC_BY_SA_3;
this.licensesByName = licensesByName;
this.context = context;
this.mwApi = mwApi;
this.contentResolver = context.getContentResolver();
useExtStorage = this.prefs.getBoolean("useExternalStorage", false);
}
@SuppressLint("CheckResult")
void receive(List<Uri> mediaUri, String mimeType, String source, SimilarImageInterface similarImageInterface) {
initDefaultValues();
Observable<UploadItem> itemObservable = Observable.fromIterable(mediaUri)
.map(media -> {
currentMediaUri=media;
return cacheFileUpload(media);
})
.map(filePath -> {
long fileCreatedDate = getFileCreatedDate(currentMediaUri);
Uri uri = Uri.fromFile(new File(filePath));
FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context);
UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface),
FileUtils.getFileExt(filePath), null,fileCreatedDate);
Single.zip(
Single.fromCallable(() ->
new FileInputStream(filePath))
.map(FileUtils::getSHA1)
.map(mwApi::existingFile)
.map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
Single.fromCallable(() ->
new FileInputStream(filePath))
.map(file -> BitmapRegionDecoder.newInstance(file, false))
.map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
(dupe, dark) -> dupe | dark)
.observeOn(Schedulers.io())
.subscribe(item.imageQuality::onNext, Timber::e);
return item;
});
items = itemObservable.toList().blockingGet();
items.get(0).selected = true;
items.get(0).first = true;
}
@SuppressLint("CheckResult")
void receiveDirect(Uri media, String mimeType, String source, String wikidataEntityIdPref, String title, String desc, SimilarImageInterface similarImageInterface) {
initDefaultValues();
long fileCreatedDate = getFileCreatedDate(media);
String filePath = this.cacheFileUpload(media);
Uri uri = Uri.fromFile(new File(filePath));
FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context);
UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface),
FileUtils.getFileExt(filePath), wikidataEntityIdPref,fileCreatedDate);
item.title.setTitleText(title);
item.descriptions.get(0).setDescriptionText(desc);
//TODO figure out if default descriptions in other languages exist
item.descriptions.get(0).setLanguageCode("en");
Single.zip(
Single.fromCallable(() ->
new FileInputStream(filePath))
.map(FileUtils::getSHA1)
.map(mwApi::existingFile)
.map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
Single.fromCallable(() ->
new FileInputStream(filePath))
.map(file -> BitmapRegionDecoder.newInstance(file, false))
.map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
(dupe, dark) -> dupe | dark).subscribe(item.imageQuality::onNext);
items.add(item);
items.get(0).selected = true;
items.get(0).first = true;
}
private void initDefaultValues() {
currentStepIndex = 0;
topCardState = true;
bottomCardState = true;
rightCardState = true;
items = new ArrayList<>();
}
/**
* Get file creation date from uri from all possible content providers
* @param media
* @return
*/
private long getFileCreatedDate(Uri media) {
try {
Cursor cursor = contentResolver.query(media, null, null, null, null);
if (cursor == null) {
return -1;//Could not fetch last_modified
}
//Content provider contracts for opening gallery from the app and that by sharing from gallery from outside are different and we need to handle both the cases
int lastModifiedColumnIndex = cursor.getColumnIndex("last_modified");//If gallery is opened from in app
if(lastModifiedColumnIndex==-1){
lastModifiedColumnIndex=cursor.getColumnIndex("datetaken");
}
//If both the content providers do not give the data, lets leave it to Jesus
if(lastModifiedColumnIndex==-1){
return -1l;
}
cursor.moveToFirst();
return cursor.getLong(lastModifiedColumnIndex);
} catch (Exception e) {
return -1;////Could not fetch last_modified
}
}
boolean isPreviousAvailable() {
return currentStepIndex > 0;
}
boolean isNextAvailable() {
return currentStepIndex < (items.size() + 1);
}
boolean isSubmitAvailable() {
int count = items.size();
boolean hasError = license == null;
for (int i = 0; i < count; i++) {
UploadItem item = items.get(i);
hasError |= item.error;
}
return !hasError;
}
int getCurrentStep() {
return currentStepIndex + 1;
}
int getStepCount() {
return items.size() + 2;
}
public int getCount() {
return items.size();
}
public List<UploadItem> getUploads() {
return items;
}
boolean isTopCardState() {
return topCardState;
}
void setTopCardState(boolean topCardState) {
this.topCardState = topCardState;
}
boolean isBottomCardState() {
return bottomCardState;
}
void setRightCardState(boolean rightCardState) {
this.rightCardState = rightCardState;
}
boolean isRightCardState() {
return rightCardState;
}
void setBottomCardState(boolean bottomCardState) {
this.bottomCardState = bottomCardState;
}
public void next() {
if (badImageSubscription != null)
badImageSubscription.dispose();
markCurrentUploadVisited();
if (currentStepIndex < items.size() + 1) {
currentStepIndex++;
}
updateItemState();
}
public void setCurrentTitleAndDescriptions(Title title, List<Description> descriptions) {
setCurrentUploadTitle(title);
setCurrentUploadDescriptions(descriptions);
}
private void setCurrentUploadTitle(Title title) {
if (currentStepIndex < items.size() && currentStepIndex >= 0) {
items.get(currentStepIndex).title = title;
}
}
private void setCurrentUploadDescriptions(List<Description> descriptions) {
if (currentStepIndex < items.size() && currentStepIndex >= 0) {
items.get(currentStepIndex).descriptions = descriptions;
}
}
public void previous() {
if (badImageSubscription != null)
badImageSubscription.dispose();
markCurrentUploadVisited();
if (currentStepIndex > 0) {
currentStepIndex--;
}
updateItemState();
}
void jumpTo(UploadItem item) {
currentStepIndex = items.indexOf(item);
item.visited = true;
updateItemState();
}
UploadItem getCurrentItem() {
return isShowingItem() ? items.get(currentStepIndex) : DUMMY;
}
boolean isShowingItem() {
return currentStepIndex < items.size();
}
private void updateItemState() {
int count = items.size();
for (int i = 0; i < count; i++) {
UploadItem item = items.get(i);
item.selected = (currentStepIndex >= count || i == currentStepIndex);
item.error = item.title == null || item.title.isEmpty();
}
}
private void markCurrentUploadVisited() {
if (currentStepIndex < items.size() && currentStepIndex >= 0) {
items.get(currentStepIndex).visited = true;
}
}
public List<String> getLicenses() {
return licenses;
}
String getSelectedLicense() {
return license;
}
void setSelectedLicense(String licenseName) {
this.license = licensesByName.get(licenseName);
}
Observable<Contribution> buildContributions(List<String> categoryStringList) {
return Observable.fromIterable(items).map(item ->
{
Contribution contribution = new Contribution(item.mediaUri, null, item.title + "." + item.fileExt,
Description.formatList(item.descriptions), -1,
null, null, sessionManager.getUserName(),
CommonsApplication.DEFAULT_EDIT_SUMMARY, item.gpsCoords.getCoords());
contribution.setWikiDataEntityId(item.wikidataEntityId);
contribution.setCategories(categoryStringList);
contribution.setTag("mimeType", item.mimeType);
contribution.setSource(item.source);
contribution.setContentProviderUri(item.mediaUri);
if (item.createdTimestamp != -1l) {
contribution.setDateCreated(new Date(item.createdTimestamp));
//Set the date only if you have it, else the upload service is gonna try it the other way
}
return contribution;
});
}
/**
* Copy files into local storage and return file path
*
* @param media Uri of the file
* @return path of the enw file
*/
private String cacheFileUpload(Uri media) {
try {
String copyPath;
if (useExtStorage)
copyPath = FileUtils.createExternalCopyPathAndCopy(media, contentResolver);
else
copyPath = FileUtils.createCopyPathAndCopy(media, context);
Timber.i("File path is " + copyPath);
return copyPath;
} catch (IOException e) {
Timber.w(e, "Error in copying URI " + media.getPath());
return null;
}
}
void keepPicture() {
items.get(currentStepIndex).imageQuality.onNext(ImageUtils.IMAGE_KEEP);
}
void deletePicture() {
badImageSubscription.dispose();
items.remove(currentStepIndex).imageQuality.onComplete();
updateItemState();
}
void subscribeBadPicture(Consumer<Integer> consumer) {
badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e);
}
@SuppressWarnings("WeakerAccess")
static class UploadItem {
public final Uri mediaUri;
public final String mimeType;
public final String source;
public final GPSExtractor gpsCoords;
public boolean selected = false;
public boolean first = false;
public String fileExt;
public BehaviorSubject<Integer> imageQuality;
Title title;
List<Description> descriptions;
public String wikidataEntityId;
public boolean visited;
public boolean error;
public long createdTimestamp;
@SuppressLint("CheckResult")
UploadItem(Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords, String fileExt, @Nullable String wikidataEntityId, long createdTimestamp) {
title = new Title();
descriptions = new ArrayList<>();
descriptions.add(new Description());
this.wikidataEntityId = wikidataEntityId;
this.mediaUri = mediaUri;
this.mimeType = mimeType;
this.source = source;
this.gpsCoords = gpsCoords;
this.fileExt = fileExt;
imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT);
this.createdTimestamp=createdTimestamp;
}
}
}

View file

@ -0,0 +1,430 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.net.Uri;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoriesModel;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ImageUtils;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static fr.free.nrw.commons.upload.UploadModel.UploadItem;
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE;
import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
/**
* The MVP pattern presenter of Upload GUI
*/
@Singleton
public class UploadPresenter {
private final UploadModel uploadModel;
private final UploadController uploadController;
private final MediaWikiApi mediaWikiApi;
private static final UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(),
new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null);
private UploadView view = DUMMY;
private static final SimilarImageInterface SIMILAR_IMAGE = (SimilarImageInterface) Proxy.newProxyInstance(SimilarImageInterface.class.getClassLoader(),
new Class[]{SimilarImageInterface.class}, (proxy, method, methodArgs) -> null);
private SimilarImageInterface similarImageInterface = SIMILAR_IMAGE;
@UploadView.UploadPage
private int currentPage = UploadView.PLEASE_WAIT;
@Inject
UploadPresenter(UploadModel uploadModel,
UploadController uploadController,
MediaWikiApi mediaWikiApi) {
this.uploadModel = uploadModel;
this.uploadController = uploadController;
this.mediaWikiApi = mediaWikiApi;
}
void receive(Uri mediaUri, String mimeType, String source) {
receive(Collections.singletonList(mediaUri), mimeType, source);
}
/**
* Passes the items received to {@link #uploadModel} and displays the items.
*
* @param media The Uri's of the media being uploaded.
* @param mimeType the mimeType of the files.
* @param source File source from {@link Contribution.FileSource}
*/
@SuppressLint("CheckResult")
void receive(List<Uri> media, String mimeType, @Contribution.FileSource String source) {
Completable.fromRunnable(() -> uploadModel.receive(media, mimeType, source, similarImageInterface))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
updateCards();
updateLicenses();
updateContent();
if (uploadModel.isShowingItem())
uploadModel.subscribeBadPicture(this::handleBadPicture);
}, Timber::e);
}
/**
* Passes the direct upload item received to {@link #uploadModel} and displays the items.
*
* @param media The Uri's of the media being uploaded.
* @param mimeType the mimeType of the files.
* @param source File source from {@link Contribution.FileSource}
*/
@SuppressLint("CheckResult")
void receiveDirect(Uri media, String mimeType, @Contribution.FileSource String source, String wikidataEntityIdPref, String title, String desc) {
Completable.fromRunnable(() -> uploadModel.receiveDirect(media, mimeType, source, wikidataEntityIdPref, title, desc, similarImageInterface))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
updateCards();
updateLicenses();
updateContent();
if (uploadModel.isShowingItem())
uploadModel.subscribeBadPicture(this::handleBadPicture);
}, Timber::e);
}
/**
* Sets the license to parameter and updates {@link UploadActivity}
*
* @param licenseName license name
*/
void selectLicense(String licenseName) {
uploadModel.setSelectedLicense(licenseName);
view.updateLicenseSummary(uploadModel.getSelectedLicense());
}
//region Wizard step management
/**
* Called by the next button in {@link UploadActivity}
*/
@SuppressLint("CheckResult")
void handleNext(Title title,
List<Description> descriptions) {
validateCurrentItemTitle()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(errorCode -> handleImage(errorCode, title, descriptions));
}
/**
* Called by the next button in {@link UploadActivity}
*/
@SuppressLint("CheckResult")
void handleCategoryNext(CategoriesModel categoriesModel,
boolean noCategoryWarningShown) {
if (categoriesModel.selectedCategoriesCount() < 1 && !noCategoryWarningShown) {
view.showNoCategorySelectedWarning();
} else {
nextUploadedItem();
}
}
private void handleImage(Integer errorCode, Title title, List<Description> descriptions) {
switch (errorCode) {
case EMPTY_TITLE:
view.showErrorMessage(R.string.add_title_toast);
break;
case FILE_NAME_EXISTS:
if(getCurrentItem().imageQuality.getValue().equals(IMAGE_KEEP)) {
setTitleAndDescription(title, descriptions);
nextUploadedItem();
} else {
view.showDuplicatePicturePopup();
}
break;
case IMAGE_OK:
default:
setTitleAndDescription(title, descriptions);
nextUploadedItem();
}
}
private void nextUploadedItem() {
uploadModel.next();
updateContent();
if (uploadModel.isShowingItem()) {
uploadModel.subscribeBadPicture(this::handleBadPicture);
}
view.dismissKeyboard();
}
private void setTitleAndDescription(Title title, List<Description> descriptions) {
uploadModel.setCurrentTitleAndDescriptions(title, descriptions);
}
private Title getCurrentImageTitle() {
return getCurrentItem().title;
}
String getCurrentImageFileName() {
UploadItem currentItem = getCurrentItem();
return currentItem.title + "." + uploadModel.getCurrentItem().fileExt;
}
@SuppressLint("CheckResult")
private Observable<Integer> validateCurrentItemTitle() {
Title title = getCurrentImageTitle();
if (title.isEmpty()) {
view.showErrorMessage(R.string.add_title_toast);
return Observable.just(EMPTY_TITLE);
}
return Observable.fromCallable(() -> mediaWikiApi.fileExistsWithName(getCurrentImageFileName()))
.subscribeOn(Schedulers.io())
.map(doesFileExist -> {
if (doesFileExist) {
return FILE_NAME_EXISTS;
}
return IMAGE_OK;
});
}
/**
* Called by the previous button in {@link UploadActivity}
*/
void handlePrevious() {
uploadModel.previous();
updateContent();
if (uploadModel.isShowingItem()) {
uploadModel.subscribeBadPicture(this::handleBadPicture);
}
view.dismissKeyboard();
}
/**
* Called when one of the pictures on the top card is clicked on in {@link UploadActivity}
*/
void thumbnailClicked(UploadItem item) {
uploadModel.jumpTo(item);
updateContent();
}
/**
* Called by the submit button in {@link UploadActivity}
*/
@SuppressLint("CheckResult")
void handleSubmit(CategoriesModel categoriesModel) {
if (view.checkIfLoggedIn())
uploadModel.buildContributions(categoriesModel.getCategoryStringList())
.observeOn(Schedulers.io())
.subscribe(uploadController::startUpload);
}
/**
* Called by the map button on the right card in {@link UploadActivity}
*/
void openCoordinateMap() {
GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords;
if (gpsObj != null && gpsObj.imageCoordsExists) {
view.launchMapActivity(gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude());
}
}
/**
* Called by the image processors when a result is obtained.
*
* @param result the result returned by the image procesors.
*/
private void handleBadPicture(@ImageUtils.Result int result) {
view.showBadPicturePopup(result);
}
void keepPicture() {
uploadModel.keepPicture();
}
void deletePicture() {
if (uploadModel.getCount() == 1)
view.finish();
else {
uploadModel.deletePicture();
updateCards();
updateContent();
if (uploadModel.isShowingItem())
uploadModel.subscribeBadPicture(this::handleBadPicture);
view.dismissKeyboard();
}
}
//endregion
//region Top Bottom and Right card state management
/**
* Toggles the top card's state between open and closed.
*/
void toggleTopCardState() {
uploadModel.setTopCardState(!uploadModel.isTopCardState());
view.setTopCardState(uploadModel.isTopCardState());
}
/**
* Toggles the bottom card's state between open and closed.
*/
void toggleBottomCardState() {
uploadModel.setBottomCardState(!uploadModel.isBottomCardState());
view.setBottomCardState(uploadModel.isBottomCardState());
}
/**
* Toggles the right card's state between open and closed.
*/
void toggleRightCardState() {
uploadModel.setRightCardState(!uploadModel.isRightCardState());
view.setRightCardState(uploadModel.isRightCardState());
}
/**
* Sets all the cards' states to closed.
*/
void closeAllCards() {
if (uploadModel.isTopCardState()) {
uploadModel.setTopCardState(false);
view.setTopCardState(false);
}
if (uploadModel.isRightCardState()) {
uploadModel.setRightCardState(false);
view.setRightCardState(false);
}
if (uploadModel.isBottomCardState()) {
uploadModel.setBottomCardState(false);
view.setBottomCardState(false);
}
}
//endregion
//region View / Lifecycle management
public void init() {
uploadController.prepareService();
}
void cleanup() {
uploadController.cleanup();
}
void removeView() {
this.view = DUMMY;
}
void addView(UploadView view) {
this.view = view;
updateCards();
updateLicenses();
updateContent();
}
/**
* Updates the cards for when there is a change to the amount of items being uploaded.
*/
private void updateCards() {
Timber.i("uploadModel.getCount():" + uploadModel.getCount());
view.updateThumbnails(uploadModel.getUploads());
view.setTopCardVisibility(uploadModel.getCount() > 1);
view.setBottomCardVisibility(uploadModel.getCount() > 0);
view.setTopCardState(uploadModel.isTopCardState());
view.setBottomCardState(uploadModel.isBottomCardState());
}
/**
* Sets the list of licences and the default license.
*/
private void updateLicenses() {
String selectedLicense = uploadModel.getSelectedLicense();
view.updateLicenses(uploadModel.getLicenses(), selectedLicense);
view.updateLicenseSummary(selectedLicense);
}
/**
* Updates the cards and the background when a new currentPage is selected.
*/
private void updateContent() {
Timber.i("Updating content for currentPage" + uploadModel.getCurrentStep());
view.setNextEnabled(uploadModel.isNextAvailable());
view.setPreviousEnabled(uploadModel.isPreviousAvailable());
view.setSubmitEnabled(uploadModel.isSubmitAvailable());
view.setBackground(uploadModel.getCurrentItem().mediaUri);
view.updateBottomCardContent(uploadModel.getCurrentStep(),
uploadModel.getStepCount(),
uploadModel.getCurrentItem(),
uploadModel.isShowingItem());
view.updateTopCardContent();
GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords;
view.updateRightCardContent(gpsObj != null && gpsObj.imageCoordsExists);
showCorrectCards(uploadModel.getCurrentStep(), uploadModel.getCount());
}
/**
* Updates the layout to show the correct bottom card.
*
* @param currentStep the current step
* @param uploadCount how many items are being uploaded
*/
private void showCorrectCards(int currentStep, int uploadCount) {
if (uploadCount == 0) {
currentPage = UploadView.PLEASE_WAIT;
} else if (currentStep <= uploadCount) {
currentPage = UploadView.TITLE_CARD;
view.setTopCardVisibility(uploadModel.getCount() > 1);
} else if (currentStep == uploadCount + 1) {
currentPage = UploadView.CATEGORIES;
view.setTopCardVisibility(false);
view.setRightCardVisibility(false);
view.initDefaultCategories();
} else {
currentPage = UploadView.LICENSE;
view.setTopCardVisibility(false);
view.setRightCardVisibility(false);
}
view.setBottomCardVisibility(currentPage);
}
//endregion
/**
* @return the item currently being displayed
*/
private UploadItem getCurrentItem() {
return uploadModel.getCurrentItem();
}
List<String> getImageTitleList() {
List<String> titleList = new ArrayList<>();
for (UploadItem item : uploadModel.getUploads()) {
if (item.title.isSet()) {
titleList.add(item.title.toString());
}
}
return titleList;
}
}

View file

@ -36,7 +36,7 @@ import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.UploadResult;
@ -62,7 +62,9 @@ public class UploadService extends HandlerService<Contribution> {
private NotificationCompat.Builder curProgressNotification;
private int toUpload;
// The file names of unfinished uploads, used to prevent overwriting
/**
* The file names of unfinished uploads, used to prevent overwriting
*/
private Set<String> unfinishedUploads = new HashSet<>();
// DO NOT HAVE NOTIFICATION ID OF 0 FOR ANYTHING
@ -193,7 +195,7 @@ public class UploadService extends HandlerService<Contribution> {
.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload))
.setOngoing(true)
.setProgress(100, 0, true)
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0))
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0))
.setTicker(getString(R.string.upload_progress_notification_title_in_progress, contribution.getDisplayTitle()));
}
@ -230,7 +232,7 @@ public class UploadService extends HandlerService<Contribution> {
//As the fileInputStream is null there's no point in continuing the upload process
//mwapi.upload accepts a NonNull input stream
if(fileInputStream == null) {
if (fileInputStream == null) {
Timber.d("File not found");
return;
}
@ -314,11 +316,12 @@ public class UploadService extends HandlerService<Contribution> {
}
@SuppressLint("StringFormatInvalid")
@SuppressWarnings("deprecation")
private void showFailedNotification(Contribution contribution) {
Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
.setAutoCancel(true)
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0))
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0))
.setTicker(getString(R.string.upload_failed_notification_title, contribution.getDisplayTitle()))
.setContentTitle(getString(R.string.upload_failed_notification_title, contribution.getDisplayTitle()))
.setContentText(getString(R.string.upload_failed_notification_subtitle))

View file

@ -0,0 +1,49 @@
package fr.free.nrw.commons.upload;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.facebook.drawee.view.SimpleDraweeView;
import com.pedrogomez.renderers.Renderer;
import fr.free.nrw.commons.R;
class UploadThumbnailRenderer extends Renderer<UploadModel.UploadItem> {
private ThumbnailClickedListener listener;
private SimpleDraweeView background;
private View space;
private ImageView error;
public UploadThumbnailRenderer(ThumbnailClickedListener listener) {
this.listener = listener;
}
@Override
protected View inflate(LayoutInflater inflater, ViewGroup parent) {
return inflater.inflate(R.layout.item_upload_thumbnail, parent, false);
}
@Override
protected void setUpView(View rootView) {
error = rootView.findViewById(R.id.error);
space = rootView.findViewById(R.id.left_space);
background = rootView.findViewById(R.id.thumbnail);
}
@Override
protected void hookListeners(View rootView) {
background.setOnClickListener(v -> listener.thumbnailClicked(getContent()));
}
@Override
public void render() {
UploadModel.UploadItem content = getContent();
background.setImageURI(content.mediaUri);
background.setAlpha(content.selected ? 1.0f : 0.5f);
space.setVisibility(content.first ? View.VISIBLE : View.GONE);
error.setVisibility(content.visited && content.error ? View.VISIBLE : View.GONE);
}
}

View file

@ -0,0 +1,26 @@
package fr.free.nrw.commons.upload;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
public class UploadThumbnailsAdapterFactory {
private ThumbnailClickedListener listener;
UploadThumbnailsAdapterFactory(ThumbnailClickedListener listener) {
this.listener = listener;
}
public RVRendererAdapter<UploadModel.UploadItem> create(List<UploadModel.UploadItem> placeList) {
RendererBuilder<UploadModel.UploadItem> builder = new RendererBuilder<UploadModel.UploadItem>()
.bind(UploadModel.UploadItem.class, new UploadThumbnailRenderer(listener));
ListAdapteeCollection<UploadModel.UploadItem> collection = new ListAdapteeCollection<>(
placeList != null ? placeList : Collections.emptyList());
return new RVRendererAdapter<>(builder, collection);
}
}

View file

@ -0,0 +1,82 @@
package fr.free.nrw.commons.upload;
import android.net.Uri;
import android.support.annotation.IntDef;
import java.lang.annotation.Retention;
import java.util.List;
import fr.free.nrw.commons.utils.ImageUtils;
import static java.lang.annotation.RetentionPolicy.SOURCE;
public interface UploadView {
// Dummy implementation of the view interface to allow us to have a 'null object pattern'
// in the presenter and avoid constant NULL checking.
// UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(),
// new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null);
List<Description> getDescriptions();
@Retention(SOURCE)
@IntDef({PLEASE_WAIT, TITLE_CARD, CATEGORIES, LICENSE})
@interface UploadPage {}
int PLEASE_WAIT = 0;
int TITLE_CARD = 1;
int CATEGORIES = 2;
int LICENSE = 3;
boolean checkIfLoggedIn();
void updateThumbnails(List<UploadModel.UploadItem> uploads);
void setNextEnabled(boolean available);
void setSubmitEnabled(boolean available);
void setPreviousEnabled(boolean available);
void setTopCardState(boolean state);
void setRightCardVisibility(boolean visible);
void setBottomCardState(boolean state);
void setRightCardState(boolean bottomCardState);
void setBackground(Uri mediaUri);
void setTopCardVisibility(boolean visible);
void setBottomCardVisibility(boolean visible);
void setBottomCardVisibility(@UploadPage int page);
void updateRightCardContent(boolean gpsPresent);
void updateBottomCardContent(int currentStep, int stepCount, UploadModel.UploadItem uploadItem, boolean isShowingItem);
void updateLicenses(List<String> licenses, String selectedLicense);
void updateLicenseSummary(String selectedLicense);
void updateTopCardContent();
void dismissKeyboard();
void showBadPicturePopup(@ImageUtils.Result int errorMessage);
void showDuplicatePicturePopup();
void finish();
void launchMapActivity(String decCoords);
void showErrorMessage(int resourceId);
void initDefaultCategories();
void showNoCategorySelectedWarning();
}

View file

@ -7,62 +7,62 @@ import java.util.HashMap;
* info in the user language
*/
public class UrlLicense {
HashMap<String,String> urlLicense = new HashMap<String, String>();
public void initialize(){
urlLicense.put("en","https://commons.wikimedia.org/wiki/Commons:Licensing");
urlLicense.put("ar","https://commons.wikimedia.org/wiki/Commons:Licensing/ar");
urlLicense.put("ast","https://commons.wikimedia.org/wiki/Commons:Licensing/ast");
urlLicense.put("az","https://commons.wikimedia.org/wiki/Commons:Licensing/az");
urlLicense.put("be","https://commons.wikimedia.org/wiki/Commons:Licensing/be");
urlLicense.put("bg","https://commons.wikimedia.org/wiki/Commons:Licensing/bg");
urlLicense.put("bn","https://commons.wikimedia.org/wiki/Commons:Licensing/bn");
urlLicense.put("ca","https://commons.wikimedia.org/wiki/Commons:Licensing/ca");
urlLicense.put("cs","https://commons.wikimedia.org/wiki/Commons:Licensing/cs");
urlLicense.put("da","https://commons.wikimedia.org/wiki/Commons:Licensing/da");
urlLicense.put("de","https://commons.wikimedia.org/wiki/Commons:Licensing/de");
urlLicense.put("el","https://commons.wikimedia.org/wiki/Commons:Licensing/el");
urlLicense.put("eo","https://commons.wikimedia.org/wiki/Commons:Licensing/eo");
urlLicense.put("es","https://commons.wikimedia.org/wiki/Commons:Licensing/es");
urlLicense.put("eu","https://commons.wikimedia.org/wiki/Commons:Licensing/eu");
urlLicense.put("fa","https://commons.wikimedia.org/wiki/Commons:Licensing/fa");
urlLicense.put("fi","https://commons.wikimedia.org/wiki/Commons:Licensing/fi");
urlLicense.put("fr","https://commons.wikimedia.org/wiki/Commons:Licensing/fr");
urlLicense.put("gl","https://commons.wikimedia.org/wiki/Commons:Licensing/gl");
urlLicense.put("gsw","https://commons.wikimedia.org/wiki/Commons:Licensing/gsw");
urlLicense.put("he","https://commons.wikimedia.org/wiki/Commons:Licensing/he");
urlLicense.put("hi","https://commons.wikimedia.org/wiki/Commons:Licensing/hi");
urlLicense.put("hu","https://commons.wikimedia.org/wiki/Commons:Licensing/hu");
urlLicense.put("id","https://commons.wikimedia.org/wiki/Commons:Licensing/id");
urlLicense.put("is","https://commons.wikimedia.org/wiki/Commons:Licensing/is");
urlLicense.put("it","https://commons.wikimedia.org/wiki/Commons:Licensing/it");
urlLicense.put("ja","https://commons.wikimedia.org/wiki/Commons:Licensing/ja");
urlLicense.put("ka","https://commons.wikimedia.org/wiki/Commons:Licensing/ka");
urlLicense.put("km","https://commons.wikimedia.org/wiki/Commons:Licensing/km");
urlLicense.put("ko","https://commons.wikimedia.org/wiki/Commons:Licensing/ko");
urlLicense.put("ku","https://commons.wikimedia.org/wiki/Commons:Licensing/ku");
urlLicense.put("mk","https://commons.wikimedia.org/wiki/Commons:Licensing/mk");
urlLicense.put("mr","https://commons.wikimedia.org/wiki/Commons:Licensing/mr");
urlLicense.put("ms","https://commons.wikimedia.org/wiki/Commons:Licensing/ms");
urlLicense.put("my","https://commons.wikimedia.org/wiki/Commons:Licensing/my");
urlLicense.put("nl","https://commons.wikimedia.org/wiki/Commons:Licensing/nl");
urlLicense.put("oc","https://commons.wikimedia.org/wiki/Commons:Licensing/oc");
urlLicense.put("pl","https://commons.wikimedia.org/wiki/Commons:Licensing/pl");
urlLicense.put("pt","https://commons.wikimedia.org/wiki/Commons:Licensing/pt");
urlLicense.put("pt-br","https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br");
urlLicense.put("ro","https://commons.wikimedia.org/wiki/Commons:Licensing/ro");
urlLicense.put("ru","https://commons.wikimedia.org/wiki/Commons:Licensing/ru");
urlLicense.put("scn","https://commons.wikimedia.org/wiki/Commons:Licensing/scn");
urlLicense.put("sk","https://commons.wikimedia.org/wiki/Commons:Licensing/sk");
urlLicense.put("sl","https://commons.wikimedia.org/wiki/Commons:Licensing/sl");
urlLicense.put("sv","https://commons.wikimedia.org/wiki/Commons:Licensing/sv");
urlLicense.put("tr","https://commons.wikimedia.org/wiki/Commons:Licensing/tr");
urlLicense.put("uk","https://commons.wikimedia.org/wiki/Commons:Licensing/uk");
urlLicense.put("ur","https://commons.wikimedia.org/wiki/Commons:Licensing/ur");
urlLicense.put("vi","https://commons.wikimedia.org/wiki/Commons:Licensing/vi");
urlLicense.put("zh","https://commons.wikimedia.org/wiki/Commons:Licensing/zh");
public static HashMap<String,String> urlLicense = new HashMap<>();
static {
urlLicense.put("en", "https://commons.wikimedia.org/wiki/Commons:Licensing");
urlLicense.put("ar", "https://commons.wikimedia.org/wiki/Commons:Licensing/ar");
urlLicense.put("ast", "https://commons.wikimedia.org/wiki/Commons:Licensing/ast");
urlLicense.put("az", "https://commons.wikimedia.org/wiki/Commons:Licensing/az");
urlLicense.put("be", "https://commons.wikimedia.org/wiki/Commons:Licensing/be");
urlLicense.put("bg", "https://commons.wikimedia.org/wiki/Commons:Licensing/bg");
urlLicense.put("bn", "https://commons.wikimedia.org/wiki/Commons:Licensing/bn");
urlLicense.put("ca", "https://commons.wikimedia.org/wiki/Commons:Licensing/ca");
urlLicense.put("cs", "https://commons.wikimedia.org/wiki/Commons:Licensing/cs");
urlLicense.put("da", "https://commons.wikimedia.org/wiki/Commons:Licensing/da");
urlLicense.put("de", "https://commons.wikimedia.org/wiki/Commons:Licensing/de");
urlLicense.put("el", "https://commons.wikimedia.org/wiki/Commons:Licensing/el");
urlLicense.put("eo", "https://commons.wikimedia.org/wiki/Commons:Licensing/eo");
urlLicense.put("es", "https://commons.wikimedia.org/wiki/Commons:Licensing/es");
urlLicense.put("eu", "https://commons.wikimedia.org/wiki/Commons:Licensing/eu");
urlLicense.put("fa", "https://commons.wikimedia.org/wiki/Commons:Licensing/fa");
urlLicense.put("fi", "https://commons.wikimedia.org/wiki/Commons:Licensing/fi");
urlLicense.put("fr", "https://commons.wikimedia.org/wiki/Commons:Licensing/fr");
urlLicense.put("gl", "https://commons.wikimedia.org/wiki/Commons:Licensing/gl");
urlLicense.put("gsw", "https://commons.wikimedia.org/wiki/Commons:Licensing/gsw");
urlLicense.put("he", "https://commons.wikimedia.org/wiki/Commons:Licensing/he");
urlLicense.put("hi", "https://commons.wikimedia.org/wiki/Commons:Licensing/hi");
urlLicense.put("hu", "https://commons.wikimedia.org/wiki/Commons:Licensing/hu");
urlLicense.put("id", "https://commons.wikimedia.org/wiki/Commons:Licensing/id");
urlLicense.put("is", "https://commons.wikimedia.org/wiki/Commons:Licensing/is");
urlLicense.put("it", "https://commons.wikimedia.org/wiki/Commons:Licensing/it");
urlLicense.put("ja", "https://commons.wikimedia.org/wiki/Commons:Licensing/ja");
urlLicense.put("ka", "https://commons.wikimedia.org/wiki/Commons:Licensing/ka");
urlLicense.put("km", "https://commons.wikimedia.org/wiki/Commons:Licensing/km");
urlLicense.put("ko", "https://commons.wikimedia.org/wiki/Commons:Licensing/ko");
urlLicense.put("ku", "https://commons.wikimedia.org/wiki/Commons:Licensing/ku");
urlLicense.put("mk", "https://commons.wikimedia.org/wiki/Commons:Licensing/mk");
urlLicense.put("mr", "https://commons.wikimedia.org/wiki/Commons:Licensing/mr");
urlLicense.put("ms", "https://commons.wikimedia.org/wiki/Commons:Licensing/ms");
urlLicense.put("my", "https://commons.wikimedia.org/wiki/Commons:Licensing/my");
urlLicense.put("nl", "https://commons.wikimedia.org/wiki/Commons:Licensing/nl");
urlLicense.put("oc", "https://commons.wikimedia.org/wiki/Commons:Licensing/oc");
urlLicense.put("pl", "https://commons.wikimedia.org/wiki/Commons:Licensing/pl");
urlLicense.put("pt", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt");
urlLicense.put("pt-br", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br");
urlLicense.put("ro", "https://commons.wikimedia.org/wiki/Commons:Licensing/ro");
urlLicense.put("ru", "https://commons.wikimedia.org/wiki/Commons:Licensing/ru");
urlLicense.put("scn", "https://commons.wikimedia.org/wiki/Commons:Licensing/scn");
urlLicense.put("sk", "https://commons.wikimedia.org/wiki/Commons:Licensing/sk");
urlLicense.put("sl", "https://commons.wikimedia.org/wiki/Commons:Licensing/sl");
urlLicense.put("sv", "https://commons.wikimedia.org/wiki/Commons:Licensing/sv");
urlLicense.put("tr", "https://commons.wikimedia.org/wiki/Commons:Licensing/tr");
urlLicense.put("uk", "https://commons.wikimedia.org/wiki/Commons:Licensing/uk");
urlLicense.put("ur", "https://commons.wikimedia.org/wiki/Commons:Licensing/ur");
urlLicense.put("vi", "https://commons.wikimedia.org/wiki/Commons:Licensing/vi");
urlLicense.put("zh", "https://commons.wikimedia.org/wiki/Commons:Licensing/zh");
}
public String getLicenseUrl ( String language){
if(urlLicense.containsKey(language)) {
public static String getLicenseUrl ( String language){
if (urlLicense.containsKey(language)) {
return urlLicense.get(language);
} else {
return urlLicense.get("en");

View file

@ -0,0 +1,30 @@
package fr.free.nrw.commons.utils;
import android.support.annotation.NonNull;
import android.text.Editable;
import android.text.TextWatcher;
public class AbstractTextWatcher implements TextWatcher {
private final TextChange textChange;
public AbstractTextWatcher(@NonNull TextChange textChange) {
this.textChange = textChange;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
textChange.onTextChanged(s.toString());
}
@Override
public void afterTextChanged(Editable s) {
}
public interface TextChange {
void onTextChanged(String value);
}
}

View file

@ -0,0 +1,41 @@
package fr.free.nrw.commons.utils;
import java.util.HashMap;
import java.util.Set;
/**
* HashMap that can be searched in both the forward and reverse directions.
*/
public class BiMap<K, V> {
private HashMap<K, V> map = new HashMap<K, V>();
private HashMap<V, K> inversedMap = new HashMap<V, K>();
public void put(K k, V v) {
map.put(k, v);
inversedMap.put(v, k);
}
public V get(K k) {
return map.get(k);
}
public K getKey(V v) {
return inversedMap.get(v);
}
public Set<V> getEntrySet(){
return inversedMap.keySet();
}
public void remove(K k){
inversedMap.remove(map.remove(k));
}
public boolean containsKey(V v){
return inversedMap.containsKey(v);
}
}

View file

@ -0,0 +1,39 @@
package fr.free.nrw.commons.utils;
import android.util.Log;
import android.view.View;
/**
* This class includes utilities for contribution list fragment indicators, such as number of
* uploads, notification and nearby cards and their progress bar behind them.
*/
public class ContributionListViewUtils {
/**
* Sets indicator and progress bar visibility according to 3 states, data is ready to display,
* data still loading, both should be invisible because media details fragment is visible
* @param indicator this can be numOfUploads text view, notification/nearby card views
* @param progressBar this is the progress bar behind indicators, displays they are loading
* @param isIndicatorReady is indicator fetched the information will be displayed
* @param isBothInvisible true if contribution list fragment is not active (ie. Media Details Fragment is active)
*/
public static void setIndicatorVisibility(View indicator, View progressBar, boolean isIndicatorReady, boolean isBothInvisible) {
if (indicator!=null && progressBar!=null) {
if (isIndicatorReady) {
// Indicator ready, display them
indicator.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
} else {
if (isBothInvisible) {
// Media Details Fragment is visible, hide both
indicator.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
} else {
// Indicator is not ready, still loading
indicator.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
}
}
}
}
}

View file

@ -5,6 +5,7 @@ import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
@ -114,7 +115,49 @@ public class DialogUtil {
.setIcon(iconResourceId).create();
return alertDialog;
}
public static void showAlertDialog(Activity activity,
String title,
String message,
final Runnable onPositiveBtnClick,
final Runnable onNegativeBtnClick) {
showAlertDialog(activity,
title,
message,
activity.getString(R.string.no),
activity.getString(R.string.yes),
onPositiveBtnClick,
onNegativeBtnClick);
}
public static void showAlertDialog(Activity activity,
String title,
String message,
String positiveButtonText,
String negativeButtonText,
final Runnable onPositiveBtnClick,
final Runnable onNegativeBtnClick) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(title);
builder.setMessage(message);
builder.setPositiveButton(positiveButtonText, (dialogInterface, i) -> {
dialogInterface.dismiss();
if (onPositiveBtnClick != null) {
onPositiveBtnClick.run();
}
});
builder.setNegativeButton(negativeButtonText, (DialogInterface dialogInterface, int i) -> {
dialogInterface.dismiss();
if (onNegativeBtnClick != null) {
onNegativeBtnClick.run();
}
});
AlertDialog dialog = builder.create();
showSafely(activity, dialog);
}
public interface Callback {

View file

@ -66,7 +66,7 @@ public class FileUtils {
*/
public static boolean checkIfDirectoryExists(String pathToCheck) {
File director = new File(pathToCheck);
if(director.exists() && director.isDirectory()) {
if (director.exists() && director.isDirectory()) {
return true;
} else {
return false;

View file

@ -7,6 +7,7 @@ import android.graphics.BitmapRegionDecoder;
import android.graphics.Color;
import android.graphics.Rect;
import android.net.Uri;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.facebook.common.executors.CallerThreadExecutor;
@ -20,6 +21,8 @@ import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import fr.free.nrw.commons.R;
import timber.log.Timber;
@ -30,20 +33,44 @@ import timber.log.Timber;
public class ImageUtils {
public enum Result {
IMAGE_DARK,
IMAGE_OK
public static final int IMAGE_DARK = 1;
public static final int IMAGE_BLURRY = 1 << 1;
public static final int IMAGE_DUPLICATE = 1 << 2;
public static final int IMAGE_OK = 0;
public static final int IMAGE_KEEP = -1;
public static final int IMAGE_WAIT = -2;
public static final int EMPTY_TITLE = -3;
public static final int FILE_NAME_EXISTS = -4;
public static final int NO_CATEGORY_SELECTED = -5;
@IntDef(
flag = true,
value = {
IMAGE_DARK,
IMAGE_BLURRY,
IMAGE_DUPLICATE,
IMAGE_OK,
IMAGE_KEEP,
IMAGE_WAIT,
EMPTY_TITLE,
FILE_NAME_EXISTS,
NO_CATEGORY_SELECTED
}
)
@Retention(RetentionPolicy.SOURCE)
public @interface Result {
}
/**
* @param bitmapRegionDecoder BitmapRegionDecoder for the image we wish to process
* @return Result.IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
* Result.IMAGE_DARK if image is too dark
* @return IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
* IMAGE_DARK if image is too dark
*/
public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
public static @Result
int checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
if (bitmapRegionDecoder == null) {
Timber.e("Expected bitmapRegionDecoder was null");
return Result.IMAGE_OK;
return IMAGE_OK;
}
int loadImageHeight = bitmapRegionDecoder.getHeight();
@ -59,10 +86,10 @@ public class ImageUtils {
Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null);
if (checkIfImageIsDark(processBitmap)) {
return Result.IMAGE_DARK;
return IMAGE_DARK;
}
return Result.IMAGE_OK;
return IMAGE_OK;
}
/**
@ -132,8 +159,9 @@ public class ImageUtils {
/**
* Downloads the image from the URL and sets it as the phone's wallpaper
* Fails silently if download or setting wallpaper fails.
* @param context
* @param imageUrl
*
* @param context context
* @param imageUrl Url of the image
*/
public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) {
Timber.d("Trying to set wallpaper from url %s", imageUrl.toString());
@ -150,7 +178,7 @@ public class ImageUtils {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
if (dataSource.isFinished() && bitmap != null){
if (dataSource.isFinished() && bitmap != null) {
Timber.d("Bitmap loaded from url %s", imageUrl.toString());
setWallpaper(context, Bitmap.createBitmap(bitmap));
dataSource.close();
@ -173,7 +201,29 @@ public class ImageUtils {
wallpaperManager.setBitmap(bitmap);
ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_successfully));
} catch (IOException e) {
Timber.e(e,"Error setting wallpaper");
Timber.e(e, "Error setting wallpaper");
}
}
public static String getErrorMessageForResult(Context context, @Result int result) {
String errorMessage;
if (result == ImageUtils.IMAGE_DARK)
errorMessage = context.getString(R.string.upload_image_problem_dark);
else if (result == ImageUtils.IMAGE_BLURRY)
errorMessage = context.getString(R.string.upload_image_problem_blurry);
else if (result == ImageUtils.IMAGE_DUPLICATE)
errorMessage = context.getString(R.string.upload_image_problem_duplicate);
else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY))
errorMessage = context.getString(R.string.upload_image_problem_dark_blurry);
else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_DUPLICATE))
errorMessage = context.getString(R.string.upload_image_problem_dark_duplicate);
else if (result == (ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE))
errorMessage = context.getString(R.string.upload_image_problem_blurry_duplicate);
else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE))
errorMessage = context.getString(R.string.upload_image_problem_dark_blurry_duplicate);
else
return "";
return errorMessage;
}
}

View file

@ -9,8 +9,14 @@ import android.support.v4.content.ContextCompat;
import fr.free.nrw.commons.CommonsApplication;
public class PermissionUtils {
public static final int CAMERA_PERMISSION_FROM_CONTRIBUTION_LIST = 100;
public static final int GALLERY_PERMISSION_FROM_CONTRIBUTION_LIST = 101;
public static final int CAMERA_PERMISSION_FROM_NEARBY_MAP = 102;
public static final int GALLERY_PERMISSION_FROM_NEARBY_MAP = 103;
/**
* This method can be used by any activity which requires a permission which has been blocked(marked never ask again by the user)
It open the app settings from where the user can manually give us the required permission.

View file

@ -12,4 +12,8 @@ public class StringUtils {
return Html.fromHtml(source).toString();
}
}
public static boolean isNullOrWhiteSpace(String value) {
return value == null || value.trim().isEmpty();
}
}

Some files were not shown because too many files have changed in this diff Show more