Merge branch 'master' into addedReasonList
|
|
@ -9,15 +9,19 @@ _Before creating an issue, please search the existing issues to see if a similar
|
|||
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:**
|
||||
|
||||
|
|
|
|||
|
|
@ -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._
|
||||
73
README.md
|
|
@ -1,4 +1,4 @@
|
|||
# Wikimedia Commons Android app [](https://travis-ci.org/commons-app/apps-android-commons)
|
||||
# Wikimedia Commons Android app [](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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ public class WelcomePagerAdapter extends PagerAdapter {
|
|||
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);
|
||||
TextView textView = layout.findViewById(R.id.welcomeYesButton);
|
||||
if (textView.getVisibility() != View.VISIBLE) {
|
||||
textView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
public interface CategoryClickedListener {
|
||||
void categoryClicked(CategoryItem item);
|
||||
}
|
||||
|
|
@ -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 + '\'';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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();
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
if (controller == null) {
|
||||
controller = new ContributionController(this);
|
||||
}
|
||||
controller.loadState(savedInstanceState);
|
||||
}
|
||||
|
||||
public void setAdapter(ListAdapter adapter) {
|
||||
this.contributionsList.setAdapter(adapter);
|
||||
|
||||
if(BuildConfig.FLAVOR.equalsIgnoreCase("beta")){
|
||||
((ContributionsActivity) getActivity()).betaSetUploadCount(adapter.getCount());
|
||||
@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);
|
||||
}
|
||||
|
||||
private void setListeners() {
|
||||
|
||||
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
|
||||
fabCamera.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
if (outState == null) {
|
||||
outState = new Bundle();
|
||||
}
|
||||
super.onSaveInstanceState(outState);
|
||||
controller.saveState(outState);
|
||||
outState.putInt("grid-position", contributionsList.getFirstVisiblePosition());
|
||||
}
|
||||
|
||||
@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);
|
||||
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 {
|
||||
controller.handleImagePicked(requestCode, data.getData(), false, null);
|
||||
// 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 {
|
||||
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
requestCode, resultCode, data);
|
||||
controller.startCameraCapture();
|
||||
}
|
||||
} else {
|
||||
controller.startCameraCapture();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fabGalery.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_from_gallery:
|
||||
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();
|
||||
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 {
|
||||
// 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.
|
||||
fabPlus.startAnimation(rotate_forward);
|
||||
fabCamera.startAnimation(fab_open);
|
||||
fabGalery.startAnimation(fab_open);
|
||||
fabCamera.show();
|
||||
fabGalery.show();
|
||||
}
|
||||
} else {
|
||||
controller.startCameraCapture();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
controller.startCameraCapture();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -123,6 +124,7 @@ 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) {
|
||||
|
|
@ -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,15 +166,13 @@ 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())) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -137,7 +136,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
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:
|
||||
|
|
@ -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:
|
||||
|
|
@ -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,11 +380,11 @@ 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);
|
||||
}
|
||||
|
|
@ -396,7 +395,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
Timber.d("Skipping getCount. Returning as activity is destroyed!");
|
||||
return 0;
|
||||
}
|
||||
return ((MediaDetailProvider) getActivity()).getTotalMediaCount();
|
||||
return ((MediaDetailProvider) getParentFragment()).getTotalMediaCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
public class NearbyFragment extends CommonsDaggerSupportFragment
|
||||
implements LocationUpdateListener,
|
||||
WikidataEditListener.WikidataP18EditListener {
|
||||
|
||||
private static final int LOCATION_REQUEST = 1;
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
public void onLocationChangedSignificantly(LatLng latLng) {
|
||||
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChangedSlightly(LatLng latLng) {
|
||||
refreshView(LOCATION_SLIGHTLY_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,8 +257,10 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
|
||||
if (curLatLng != null && curLatLng.equals(lastLocation)
|
||||
&& !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed
|
||||
if (!onOrientationChanged) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
curLatLng = lastLocation;
|
||||
|
||||
if (locationChangeType.equals(PERMISSION_JUST_GRANTED)) {
|
||||
|
|
@ -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,45 +364,14 @@ 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) {
|
||||
|
|
@ -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) {
|
||||
private void hideProgressBar() {
|
||||
if (progressBar != null) {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChangedSlightly(LatLng latLng) {
|
||||
refreshView(LOCATION_SLIGHTLY_CHANGED);
|
||||
}
|
||||
|
||||
public void prepareViewsForSheetPosition(int bottomSheetState) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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()));
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -309,6 +325,12 @@ 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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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,16 +529,20 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
.zoom(11)
|
||||
.build());
|
||||
|
||||
if (!getParentFragment().getActivity().isFinishing()) {
|
||||
mapView = new MapView(getParentFragment().getActivity(), options);
|
||||
// create map
|
||||
mapView = new MapView(getActivity(), options);
|
||||
mapView.onCreate(savedInstanceState);
|
||||
mapView.getMapAsync(mapboxMap -> {
|
||||
((NearbyActivity)getActivity()).setMapViewTutorialShowCase();
|
||||
mapView.getMapAsync(new OnMapReadyCallback() {
|
||||
@Override
|
||||
public void onMapReady(MapboxMap mapboxMap) {
|
||||
NearbyMapFragment.this.mapboxMap = mapboxMap;
|
||||
updateMapSignificantly();
|
||||
}
|
||||
});
|
||||
mapView.setStyleUrl("asset://mapstyle.json");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onLogoutComplete is called after shared preferences and data stored in local database are cleared.
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -63,7 +55,7 @@ public class QuizResultActivity extends AppCompatActivity {
|
|||
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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,154 +17,196 @@ 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())
|
||||
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());
|
||||
if (position == 0) {
|
||||
Timber.d("Title is " + title);
|
||||
if (!title.isEmpty()) {
|
||||
descItemEditText.setText(title.toString());
|
||||
} else {
|
||||
etDescriptionText.setText("");
|
||||
descItemEditText.setText("");
|
||||
}
|
||||
Drawable drawableRight = context.getResources()
|
||||
.getDrawable(R.drawable.mapbox_info_icon_default);
|
||||
if (position != 0) {
|
||||
etDescriptionText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
|
||||
|
||||
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 {
|
||||
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());
|
||||
uploadView.setTopCardState(false);
|
||||
}
|
||||
});
|
||||
|
||||
etDescriptionText.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
} else {
|
||||
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);
|
||||
Collections.sort(languages, (language, t1) -> language.getLocale().getDisplayLanguage()
|
||||
.compareTo(t1.getLocale().getDisplayLanguage().toString()));
|
||||
languagesAdapter.setLanguages(languages);
|
||||
R.layout.row_item_languages_spinner, selectedLanguages);
|
||||
languagesAdapter.notifyDataSetChanged();
|
||||
spinnerDescriptionLanguages.setAdapter(languagesAdapter);
|
||||
|
||||
if (description.getSelectedLanguageIndex() == -1) {
|
||||
if (position == 0) {
|
||||
int defaultLocaleIndex = getIndexOfUserDefaultLocale();
|
||||
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());
|
||||
}
|
||||
|
||||
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);
|
||||
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
|
||||
|
|
@ -172,27 +214,44 @@ class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewH
|
|||
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
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();// Do not do repeat the process
|
||||
findOtherImages(similarImageInterface);// Do not do repeat the process
|
||||
} else {
|
||||
useImageCoords();
|
||||
}
|
||||
|
||||
} catch (FileNotFoundException e) {
|
||||
Timber.w("File not found: " + mediaUri, e);
|
||||
}
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
@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(descriptor.getFileDescriptor(), copyPath);
|
||||
FileUtils.copy(fileDescriptor, copyPath);
|
||||
Timber.d("Filepath (copied): %s", copyPath);
|
||||
return copyPath;
|
||||
} catch (IOException e) {
|
||||
Timber.e(e);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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,39 +53,46 @@ 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 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;
|
||||
} 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) {
|
||||
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);
|
||||
|
||||
|
|
@ -83,7 +102,6 @@ public class GPSExtractor {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double getDecLatitude() {
|
||||
return decLatitude;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.upload;
|
|||
import java.util.Locale;
|
||||
|
||||
class Language {
|
||||
|
||||
private Locale locale;
|
||||
private boolean isSet = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
public interface SimilarImageInterface {
|
||||
void showSimilarImageFragment(String originalFilePath, String possibleFilePath);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +1,83 @@
|
|||
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
|
||||
|
|
@ -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);
|
||||
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]", language.getLocale().getDisplayName(),
|
||||
language.getLocale().getLanguage()));
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
public interface ThumbnailClickedListener {
|
||||
void thumbnailClicked(UploadModel.UploadItem content);
|
||||
}
|
||||
37
app/src/main/java/fr/free/nrw/commons/upload/Title.java
Normal 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();
|
||||
}
|
||||
}
|
||||
607
app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
400
app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
82
app/src/main/java/fr/free/nrw/commons/upload/UploadView.java
Normal 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();
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ import java.util.HashMap;
|
|||
* info in the user language
|
||||
*/
|
||||
public class UrlLicense {
|
||||
HashMap<String,String> urlLicense = new HashMap<String, String>();
|
||||
public void initialize(){
|
||||
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");
|
||||
|
|
@ -61,7 +61,7 @@ public class UrlLicense {
|
|||
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){
|
||||
public static String getLicenseUrl ( String language){
|
||||
if (urlLicense.containsKey(language)) {
|
||||
return urlLicense.get(language);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
41
app/src/main/java/fr/free/nrw/commons/utils/BiMap.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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_OK
|
||||
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());
|
||||
|
|
@ -176,4 +204,26 @@ public class ImageUtils {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -12,4 +12,8 @@ public class StringUtils {
|
|||
return Html.fromHtml(source).toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isNullOrWhiteSpace(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ package fr.free.nrw.commons.utils;
|
|||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.view.Display;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class ViewUtil {
|
||||
|
|
@ -30,6 +33,30 @@ public class ViewUtil {
|
|||
ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_LONG).show());
|
||||
}
|
||||
|
||||
public static void showLongToast(Context context, @StringRes int stringResourceId) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_LONG).show());
|
||||
}
|
||||
|
||||
public static void showShortToast(Context context, String text) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
public static void showShortToast(Context context, @StringRes int stringResourceId) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
public static boolean isPortrait(Context context) {
|
||||
Display orientation = ((Activity)context).getWindowManager().getDefaultDisplay();
|
||||
if (orientation.getWidth() < orientation.getHeight()){
|
||||
|
|
@ -49,4 +76,15 @@ public class ViewUtil {
|
|||
}
|
||||
}
|
||||
|
||||
public static void displayPopupWindow(View anchorView, Context context, View popupWindowLayout, String text) {
|
||||
|
||||
PopupWindow popup = new PopupWindow(context);
|
||||
popup.setContentView(popupWindowLayout);
|
||||
// Closes the popup window when touch outside of it - when looses focus
|
||||
popup.setOutsideTouchable(true);
|
||||
popup.setFocusable(true);
|
||||
// Show anchored to button
|
||||
popup.showAsDropDown(anchorView);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
BIN
app/src/main/res/drawable-hdpi/ic_arrow_back_white.png
Normal file
|
After Width: | Height: | Size: 197 B |
|
After Width: | Height: | Size: 312 B |
|
After Width: | Height: | Size: 429 B |
BIN
app/src/main/res/drawable-mdpi/ic_arrow_back_white.png
Normal file
|
After Width: | Height: | Size: 136 B |
|
After Width: | Height: | Size: 233 B |
|
After Width: | Height: | Size: 2.2 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_arrow_back_white.png
Normal file
|
After Width: | Height: | Size: 233 B |
|
After Width: | Height: | Size: 410 B |
|
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png
Normal file
|
After Width: | Height: | Size: 355 B |
|
After Width: | Height: | Size: 608 B |