From f855bb5da9b796dc0876ea710738f4b720d258b3 Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Mon, 10 Jun 2019 21:58:39 +0530 Subject: [PATCH 1/8] With attribution enabled for OSM (#2997) --- .../java/fr/free/nrw/commons/nearby/NearbyMapFragment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java index c701c3aae..bbc6cd09e 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -535,8 +535,8 @@ public class NearbyMapFragment extends DaggerFragment { .compassGravity(Gravity.BOTTOM | Gravity.LEFT) .compassMargins(new int[]{12, 0, 0, 24}) .styleUrl(isDarkTheme ? Style.DARK : Style.OUTDOORS) - .logoEnabled(false) - .attributionEnabled(false) + .logoEnabled(true) + .attributionEnabled(true) .camera(new CameraPosition.Builder() .target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude())) .zoom(ZOOM_LEVEL) From 69e23b73a8ceed68869bf747e09dd787f103f0ea Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 10 Jun 2019 20:17:37 +0200 Subject: [PATCH 2/8] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ar/strings.xml | 1 + app/src/main/res/values-bg/strings.xml | 1 + app/src/main/res/values-da/strings.xml | 4 ++++ app/src/main/res/values-fa/strings.xml | 8 ++++++++ app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 4 ++++ app/src/main/res/values-ko/strings.xml | 2 ++ app/src/main/res/values-lb/strings.xml | 5 +++++ app/src/main/res/values-mk/strings.xml | 1 + app/src/main/res/values-my/strings.xml | 6 ++++++ app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 11 +++++++++++ app/src/main/res/values-qq/strings.xml | 2 ++ app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sv/strings.xml | 1 + 15 files changed, 49 insertions(+) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 8185aab5b..a1267231f 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -179,6 +179,7 @@ طلب إذن التخزين صلاحية مطلوبة: قراءة وحدة التخزين الخارجية، لا يمكن للتطبيق الوصول إلى معرض الصور الخاص بك بدونها. صلاحية مطلوبة: اكتب وحدة التخزين الخارجية، لا يمكن للتطبيق الوصول إلى معرض الصور/الكاميرا الخاصة بك بدونها. + جارٍ طلب إذن الموقع صلاحية اختيارية: احصل على الموقع الحالي لاقتراحات التصنيفات موافق الأماكن القريبة diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 5371fbaa8..86a140485 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -215,6 +215,7 @@ Персонализираното авторско име, което ще се използва вместо потребителското ви име при качване Известия (архивирани) Списък + Изпращане Америка Европа Африка diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index b89520ef5..95d2f7ccc 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -15,6 +15,7 @@ Udseende Generelt Tilbagemelding + Privatliv Sted Commons @@ -370,5 +371,8 @@ Der opstod en fejl under udvælgelse af billeder Vent venligst… Spring over dette billede + Ophavsret + Kameramodel + Serienumre Del app via... diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 000091274..47760ee8a 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -339,6 +339,7 @@ آیا این تصویر برای بارگذاری مناسب است؟ پرسش نتیجه + شما %1$s پاسخ درست دادید. آفرین! یکی از دو گزینه را انتخاب کنید تا به سوال پاسخ دهید جلسه ورود به سیستم منقضی شد، لطفا دوباره وارد سیستم شوید. کویز خود را با دوستان خود به اشتراک بگذارید. @@ -383,6 +384,11 @@ بعدی قبلی ارسال + پرونده‌ای با نام %1$s وجود دارد. آیا اطمینان دارید که می‌خواهید ادامه دهید؟ + + %1$d بارگذاری + %1$d بارگذاری + نشانک‌ها نشانک‌ها تصویرها @@ -396,6 +402,7 @@ فهمیدم که این برای حریم خصوصی من بد است. من تغییر عقیده دادم، نمی‌خواهم دیگر برای همه قابل‌مشاهده باشد. با پوزش، این تصویر برای یک دانشنامه مناسب نیست + بارگذاری‌شده توسط خودم در %1$s؛ استفاده‌شده در %2$d مقاله. به کامانز خوش آمدید!\n\nاولین فایلتان را با فشردن کلید اضافه بارگذاری کنید. در سراسر جهان آمریکا @@ -423,6 +430,7 @@ به کاربر در صفحه بحثش خبر بده مطمئن نیستم ارسال تشکر: موفق + تشکر با موفقیت برای %1$s فرستاده شد تلاش برای فرستادن تشکر شکست خورد %1$s ارسال تشکر: ناموفق ارسال تشکر diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d191a98ad..80f92abde 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -189,6 +189,7 @@ Demande d\'autorisation d\'accès au stockage Autorisation nécessaire : Lire un stockage externe. L’application ne peut pas accéder à votre galerie sans cela. Permission obligatoire : Écriture sur stockage externe. L’application ne peut pas accéder à votre appareil photo/galerie sans cela. + Demande d\'autorisation d\'accès au stockage Autorisation facultative : Obtenir l’emplacement actuel pour des suggestions de catégorie OK Endroits à proximité diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 1228946f3..f37cbb58d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -21,6 +21,7 @@ Aspetto Generale Commenti + Privacy Posizione Commons @@ -470,6 +471,9 @@ Clicca per riusare il titolo e la descrizione dell\'immagine precedente e adattarli all\'immagine attuale. SALTA QUESTA IMMAGINE Autore + Modello fotocamera + Numeri seriali + Software Condividi applicazione tramite... Informazioni sull\'immagine diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 9eaf98ffa..664d1ff72 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -176,6 +176,7 @@ 기억 장치 권한 요청 중 권한 필요: 외부 저장소 읽기. 이것이 없으면 앱은 갤러리에 접근할 수 없습니다. 권한 필요: 외부 저장소 쓰기. 이것이 없으면 앱은 카메라에 접근할 수 없습니다. + 위치 권한 요청 중 선택적 권한: 분류 추천을 위해 현재 위치 정보를 가져옵니다. 확인 근처의 장소 @@ -458,6 +459,7 @@ 저작권 위치 카메라 모델 + 렌즈 모델 일련 번호 소프트웨어 앱 공유... diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index 1a7adaaa6..dc1a1c2b2 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -338,4 +338,9 @@ Sicht Biller eraus fir eropzelueden Waart w.e.g. ... DËST BILD IWWERWSPRANGEN + Auteur + Copyright + Plaz + Seriennummeren + Software diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 27cfbeda8..852f8ba13 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -169,6 +169,7 @@ Се бара дозвола за складирање Потребна дозвола: Треба да се прочита од надворешен склад. Прилогот без ова нема пристап до вашата галерија. Потребна дозвола: Треба да се запише на надворешен склад. Прилогот без ова нема пристап до вашата камера/галерија. + Се бара дозвола за утврдување на местоположбата Дозвола по желба: Утврдување на тековната местоположба за предлагање категории ОК Околни места diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index f6e5cef4a..465a91384 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -9,6 +9,7 @@ ပုံပန်းသွင်ပြင် အထွေထွေ အကြံပေးရန် + ကိုယ်ရေးမူဝါဒ နေရာ ကွန်မွန်းစ် @@ -129,6 +130,7 @@ ဆွေးနွေးချက် မရှိပါ အမည်မသိရသော လိုင်စင် ပြန်လည်ဆန်းသစ်ရန် + တည်နေရာ ခွင့်ပြုချက် တောင်းဆိုနေသည် အိုကေ အနီးအနား နေရာများ အနီးအနား နေရာများ မတွေ့ပါ @@ -281,4 +283,8 @@ ပြီးပြီ မသေချာပါ ကျေးဇူးပြု၍ ခဏစောင့်ပါ... + မူပိုင်ခွင့် + တည်နေရာ + ကင်မရာ မော်ဒယ် + ဆော့ဝဲလ် diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 0df25a964..2eba4e386 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -181,6 +181,7 @@ Solicitando permissão de armazenamento Permissão necessária: leia o armazenamento externo. App não pode acessar sua galeria sem isso. Permissão necessária: escreva o armazenamento externo. App não pode acessar sua câmera/galeria sem isso. + Autorização para identificar localização Permissão opcional: Obter a localização atual de sugestões de categoria OK Lugares próximos diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 477d9d333..96958bcec 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -23,6 +23,7 @@ Aparência Geral Comentários + Privacidade Localização Commons @@ -181,6 +182,7 @@ A pedir permissão de armazenamento Permissão necessária: Ler a armazenagem externa. A aplicação não pode aceder à sua galeria sem isto. Permissão necessária: Escrever na armazenagem externa. A aplicação não pode aceder à sua câmara/galeria sem isto. + Autorização para identificar localização Permissão opcional: Obter a localização atual para sugestões de categoria OK Locais Próximos @@ -520,6 +522,15 @@ Exemplos de imagens que não devem ser carregadas SALTAR ESTA IMAGEM O descarregamento falhou! Não podemos descarregar o ficheiro sem permissão de armazenagem externa. + Gerir etiquetas EXIF + Selecionar as etiquetas EXIF a manter nos carregamentos + Autor + Direitos de autor + Localização + Modelo da câmara + Modelo da lente + Números de série + \'\'Software\'\' Carregar fotografias na wiki Wikimedia Commons, do seu telemóvel Descarregar a aplicação Commons: %1$s Partilhar aplicação por... Informação da imagem diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index e2cec15dc..cbfe2eab6 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -156,4 +156,6 @@ {{Identical|Submit}} \"Send log file\" is {{msg-wm|Commons-android-strings-send log file}}. {{Identical|Done}} + {{Identical|Author}} + {{Identical|Location}} diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4ec460d30..bed086c67 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -187,6 +187,7 @@ Запрос разрешения по использованию внешнего носителя Требуемые разрешения: чтение с внешнего носителя. Приложение не сможет получить доступ к вашей галерее без этого разрешения. Требуемые разрешения: запись на внешнее хранилище. Приложение не сможет получить доступ к галерее/камере без этого разрешения. + Запрос разрешения по определение местоположения Необязательное разрешение: получение текущего местоположения для предложения категорий OK Места поблизости diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index fe80d2520..f290a3e1f 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -174,6 +174,7 @@ Begär lagringsbehörighet Nödvändig behörighet: Läs extern lagring. Appen kan inte komma åt ditt galleri utan detta. Nödvändig behörighet: Skriv till extern lagring. Appen kan inte komma åt din kamera/galleri utan detta. + Begär platsbehörighet Valfri behörighet: Hämta aktuell plats för kategoriförslag OK Platser i närheten From 6a9018b550c30196cd28605bd7f97c74c3f58748 Mon Sep 17 00:00:00 2001 From: sherlockbeard <46790654+sherlockbeard@users.noreply.github.com> Date: Tue, 11 Jun 2019 14:35:12 +0530 Subject: [PATCH 3/8] replaced wikidatCreateClaim to wikidataCreateClaim (#3010) --- .../fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java | 2 +- app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java | 2 +- .../java/fr/free/nrw/commons/wikidata/WikidataEditService.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 85076cb69..8f5b4213f 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -410,7 +410,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { */ @Nullable @Override - public String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException { + public String wikidataCreateClaim(String entityId, String property, String snaktype, String value) throws IOException { Timber.d("Filename is %s", value); CustomApiResult result = wikidataApi.action("wbcreateclaim") .param("entity", entityId) diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index 2d39740d5..e38c4dc0f 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -59,7 +59,7 @@ public interface MediaWikiApi { String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException; @Nullable - String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException; + String wikidataCreateClaim(String entityId, String property, String snaktype, String value) throws IOException; @Nullable boolean addWikidataEditTag(String revisionId) throws IOException; diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java index 4b51921ac..eac1f7cde 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java @@ -79,7 +79,7 @@ public class WikidataEditService { Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId); Observable.fromCallable(() -> { String propertyValue = getFileName(fileName); - return mediaWikiApi.wikidatCreateClaim(wikidataEntityId, "P18", "value", propertyValue); + return mediaWikiApi.wikidataCreateClaim(wikidataEntityId, "P18", "value", propertyValue); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) From 09459a37652c5c3c71ea04854d61be766689ca35 Mon Sep 17 00:00:00 2001 From: sherlockbeard <46790654+sherlockbeard@users.noreply.github.com> Date: Wed, 12 Jun 2019 17:07:22 +0530 Subject: [PATCH 4/8] user talk and dialog box fix (#3014) --- .../free/nrw/commons/delete/DeleteHelper.java | 2 +- .../commons/media/MediaDetailFragment.java | 27 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java index 0b0d8fde4..1b72833dd 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java @@ -110,7 +110,7 @@ public class DeleteHelper { mwApi.appendEdit(editToken, logPageString + "\n", "Commons:Deletion_requests/" + date, summary); mwApi.appendEdit(editToken, userPageString + "\n", - "User_Talk:" + sessionManager.getCurrentAccount().name, summary); + "User_Talk:" + media.getCreator(), summary); } catch (Exception e) { Timber.e(e); return false; diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 72789e1fc..759d45a00 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -44,6 +44,7 @@ import fr.free.nrw.commons.Media; import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.category.CategoryDetailsActivity; import fr.free.nrw.commons.contributions.ContributionsFragment; import fr.free.nrw.commons.delete.DeleteHelper; @@ -367,6 +368,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { @OnClick(R.id.nominateDeletion) public void onDeleteButtonClicked(){ + if(AccountUtil.getUserName(getContext()).equals(media.getCreator())){ final ArrayAdapter languageAdapter = new ArrayAdapter<>(getActivity(), R.layout.simple_spinner_dropdown_list, reasonList); final Spinner spinner = new Spinner(getActivity()); @@ -384,19 +386,19 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { if(isDeleted) { dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); } + } //Reviewer correct me if i have misunderstood something over here //But how does this if (delete.getVisibility() == View.VISIBLE) { // enableDeleteButton(true); makes sense ? + else{ AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); - alert.setMessage("Why should this fileckathon-2018 be deleted?"); + alert.setMessage("Why should "+ media.getDisplayTitle() +" be deleted?"); final EditText input = new EditText(getActivity()); alert.setView(input); input.requestFocus(); alert.setPositiveButton(R.string.ok, (dialog1, whichButton) -> { String reason = input.getText().toString(); - - deleteHelper.makeDeletion(getContext(), media, reason); - enableDeleteButton(false); + onDeleteClickeddialogtext(reason); }); alert.setNegativeButton(R.string.cancel, (dialog12, whichButton) -> { }); @@ -427,6 +429,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { d.show(); d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); } + } @SuppressLint("CheckResult") private void onDeleteClicked(Spinner spinner) { @@ -445,6 +448,22 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { } + @SuppressLint("CheckResult") + private void onDeleteClickeddialogtext(String reason) { + Single resultSingletext = reasonBuilder.getReason(media, reason) + .flatMap(reasonString -> deleteHelper.makeDeletion(getContext(), media, reason)); + compositeDisposable.add(resultSingletext + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(s -> { + if (getActivity() != null) { + isDeleted = true; + enableDeleteButton(false); + } + })); + + } + @OnClick(R.id.seeMore) public void onSeeMoreClicked(){ if (nominatedForDeletion.getVisibility() == VISIBLE && getActivity() != null) { From 6619ccf8d5f8edf8edf1de1d33dc642ed0c2bbed Mon Sep 17 00:00:00 2001 From: sherlockbeard <46790654+sherlockbeard@users.noreply.github.com> Date: Wed, 12 Jun 2019 18:36:25 +0530 Subject: [PATCH 5/8] removed the hard coded string (#3015) * removed the hard coded string * fix --- .../java/fr/free/nrw/commons/media/MediaDetailFragment.java | 2 +- app/src/main/res/values/strings.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 759d45a00..4521104c7 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -392,7 +392,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { // enableDeleteButton(true); makes sense ? else{ AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); - alert.setMessage("Why should "+ media.getDisplayTitle() +" be deleted?"); + alert.setMessage(getString(R.string.dialog_box_text_nomination,media.getDisplayTitle())); final EditText input = new EditText(getActivity()); alert.setView(input); input.requestFocus(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b460f358e..666ad5a64 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -554,4 +554,5 @@ Upload your first media by tapping on the add button. Upload photos to Wikimedia Commons on your phone Download the Commons app: %1$s Share app via... Image Info + Why should %1$s be deleted? From 30df3025b641ec757d1f9a4266fb1a0e4d447f94 Mon Sep 17 00:00:00 2001 From: Aadil Ahmed Date: Thu, 13 Jun 2019 08:43:51 -0700 Subject: [PATCH 6/8] Update to Gradle 5 (#2860) --- app/build.gradle | 1 + build.gradle | 2 +- gradle.properties | 3 ++- gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index fac1ab9ba..8a8ccd5e5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,6 +28,7 @@ dependencies { implementation 'com.facebook.fresco:fresco:1.13.0' implementation 'com.drewnoakes:metadata-extractor:2.11.0' implementation 'com.dmitrybrant:wikimedia-android-data-client:0.0.18' + implementation 'org.apache.commons:commons-lang3:3.8.1' // UI implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' diff --git a/build.gradle b/build.gradle index ad7e27f69..ac0402861 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.2' + classpath 'com.android.tools.build:gradle:3.4.1' classpath 'com.dicedmelon.gradle:jacoco-android:0.1.4' classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION" diff --git a/gradle.properties b/gradle.properties index fc38b3a14..742f1f5df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,4 +25,5 @@ DAGGER_VERSION=2.21 systemProp.http.proxyPort=0 systemProp.http.proxyHost= android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true +android.enableR8=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 394c95658..8e0247972 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip From 04b051b37aeda8a81ce71fb68991ff523fc63128 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 13 Jun 2019 20:02:26 +0200 Subject: [PATCH 7/8] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ar/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 11 ++++++----- app/src/main/res/values-is/strings.xml | 13 +++++++++++++ app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-mk/strings.xml | 1 + app/src/main/res/values-my/strings.xml | 13 ++++++++++++- app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 3 ++- app/src/main/res/values-sv/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 2 ++ app/src/main/res/values-zh-rTW/strings.xml | 4 +++- 11 files changed, 43 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a1267231f..873a3101d 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -531,4 +531,5 @@ ارفع الصور لويكيميديا ​​كومنز على هاتفك قم بتنزيل تطبيق كومنز: %1$s مشاركة التطبيق عبر... معلومات الصورة + لماذا يجب حذف %1$s؟ diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 80f92abde..850d1545b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -503,8 +503,8 @@ Semble correct Oui, pourquoi pas Image suivante - Cliquer sur ce bouton vous fournira une autre image récemment téléversée de Wikimédia Communs. - Vous pouvez revoir les images et améliorer la qualité de Wikimédia Communs.\n Les quatre paramètres à revoir sont : \n - Cette image est-elle à propos ? \n - Cette image respecte-t-elle les règles de droit d’auteur ? \n - Cette image est-elle bien catégorisée ? \n - Si tout va bien, vous pouvez aussi remercier le contributeur. + Cliquer sur ce bouton vous fournira une autre image récemment téléversée de Wikimédia Commons. + Vous pouvez revoir les images et améliorer la qualité de Wikimédia Commons.\n Les quatre paramètres à revoir sont : \n - Cette image est-elle à propos ? \n - Cette image respecte-t-elle les règles de droit d’auteur ? \n - Cette image est-elle bien catégorisée ? \n - Si tout va bien, vous pouvez aussi remercier le contributeur. Réception de contenu partagé. Le traitement de l\'image peut prendre un certain temps en fonction de la taille de l\'image et de votre matériel Réception de contenu partagé. Le traitement des images peut prendre un certain temps en fonction de la taille des images et de votre matériel @@ -520,7 +520,7 @@ Erreur lors de la sélection des images Choisir les images à téléverser Veuillez patienter… - Les images en vedette sont des images de photographes et d’illustrateurs très doués que la communauté de Wikimédia Communs a choisi comme étant de la meilleure qualité pour le site. + Les images en vedette sont des images de photographes et d’illustrateurs très doués que la communauté de Wikimédia Commons a choisi comme étant de la meilleure qualité pour le site. Les images téléversées par les lieux de proximité sont les images téléversées par la découverte des lieux sur la carte. Cette fonctionalité permet aux contributeurs d\'envoyer une notification de remerciement aux utilisateurs qui font des modifications utiles – en utilisant un petit lien de remerciement sur la page historique ou sur celle du diff. Copier le titre et la description précédente @@ -530,7 +530,7 @@ SAUTER CETTE IMAGE Échec du téléchargement ! Nous ne pouvons pas télécharger le fichier sans droit de stockage externe. Gérer les balises EXIF - Sélectionner quelles balises EXIF à conserver dans les téléchargements + Sélectionner quelles balises EXIF à conserver dans les téléversements Auteur Droits d’auteur Emplacement @@ -538,7 +538,8 @@ Modèle de lentille Numéros de série Logiciel - Téléverser des photos vers Wikimédia Communs, sur votre téléphone Téléchargez l’application Communs : %1$s + Téléverser des photos vers Wikimédia Commons, sur votre téléphone Téléchargez l’application Commons : %1$s Partager l’application via… Informations de l’image + Pourquoi %1$s devrait-il être supprimé ? diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 6e6272fad..67aae5ee2 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -9,6 +9,7 @@ Útlit Almennt Umsagnir + Persónuvernd Staðsetning Commons • \ @@ -167,6 +168,7 @@ Biður um aðgang að geymslurými Nauðsynlegar heimildir: Lesa ytri gagnageymslu. Forritið fær ekki aðgang að myndasafni ekki án þessa. Nauðsynlegar heimildir: Skrifa í ytri gagnageymslu. Forritið nær ekki sambandi við myndavél/myndasafn ekki án þessa. + Biður um aðgang að staðsetningu Nauðsynlegar heimildir: Lesa núverandi staðsetningu til að geta stungið upp á flokkum Í lagi Staðir í nágrenninu @@ -430,6 +432,8 @@ Sjá yfirstandandi herferðir Lokið Ekki viss + Senda þakkarboð + Sendi þakkarboð Er þetta rétt flokkað? Kemur þetta umfjöllunarefninu við? Það brýtur á móti höfundarrétti því það er @@ -451,4 +455,13 @@ Veldu myndir til að senda inn Bíddu aðeins… SLEPPA ÞESSARI MYND + Sýsla með EXIF-merki + Höfundur + Höfundarréttur + Staðsetning + Tegund myndavélar + Tegund linsu + Raðnúmer + Hugbúnaður + Upplýsingar í mynd diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f37cbb58d..32509e837 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -476,4 +476,5 @@ Software Condividi applicazione tramite... Informazioni sull\'immagine + Perché %1$s dovrebbe essere cancellato? diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 852f8ba13..516b42219 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -521,4 +521,5 @@ Подигајте слики на Ризницата од телефон. Преземете го прилогот на Ризницата: %1$s Сподели преку... Инфо за сликата + Зошто сметате дека %1$s треба да се избрише? diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 465a91384..5cc34d2cf 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -152,7 +152,7 @@ မကိုက်ညီသော ထည့်သွင်းမှု ၅၀၀ ထက်ပို၍ မပြသနိုင်ပါ ကိုက်ညီသောနံပါတ်တစ်ခု ရိုက်ထည့်ပါ - လတ်တလော အပ်ပလုတ်ကန့်သတ်ချက် သတ်မှတ်ရန် + လတ်တလော အပ်ပလုတ်ကန့်သတ်ချက် အမှန်တကယ် ထွက်သွားလိုပါသလား ကွန်မွန်းစ် လိုဂို ကွန်မွန်းစ် ဝဘ်ဆိုဒ် @@ -278,13 +278,24 @@ အာဖရိက အာရှ ပစိဖိတ် + ဟုတ်ကဲ့ ထည့်သွင်းမည် + ဟင်းအင်း၊ ပြန်သွားမည် ဤဧရိယာကို ရှာဖွေပါ ခွင့်ပြုချက် တောင်းခံရန် + နောက်တခါ ထပ်မမေးပါနှင့် ပြီးပြီ မသေချာပါ + ကျေးဇူးတင်မှု ပို့နေသည် + ကျေးဇူးတင်မှု ပို့နေသည် + %1$ အတွက် ကျေးဇူးတင်မှု ပို့နေသည် + နောက်ရုပ်ပုံ + မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ ကျေးဇူးပြု၍ ခဏစောင့်ပါ... + ဖန်တီးသူ မူပိုင်ခွင့် တည်နေရာ ကင်မရာ မော်ဒယ် ဆော့ဝဲလ် + ရုပ်ပုံ အချက်အလက် + %1$ ဟာ ဘာကြောင့် ဖျက်သင့်သလဲ? diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2eba4e386..2d1831e72 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -533,4 +533,5 @@ Faça o carregamento de fotos para o Wikimedia Commons no seu telefone ou baixe o aplicativo Commons: %1$s Compartilhar aplicativo via... Informação da imagem + Por que %1$s deve ser excluído? diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bed086c67..e05305690 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -187,7 +187,7 @@ Запрос разрешения по использованию внешнего носителя Требуемые разрешения: чтение с внешнего носителя. Приложение не сможет получить доступ к вашей галерее без этого разрешения. Требуемые разрешения: запись на внешнее хранилище. Приложение не сможет получить доступ к галерее/камере без этого разрешения. - Запрос разрешения по определение местоположения + Запрос на определение местоположения Необязательное разрешение: получение текущего местоположения для предложения категорий OK Места поблизости @@ -540,4 +540,5 @@ Чтобы загружать фото на Викисклад (Wikimedia Commons), скачайте одноимённое приложение «Викисклад» (Commons): %1$s Поделиться приложением с помощью... Информация об изображении + Почему %1$s должно быть удалено? diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index f290a3e1f..64003204b 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -526,4 +526,5 @@ Ladda upp foton till Wikimedia Commons på din telefon Ladda ned Commons-appen: %1$s Dela appen via... Bildinfo + Varför bör %1$s raderas? diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f03d043d0..f6c716434 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -186,6 +186,7 @@ Запит дозволу на зберігання Обов\'язковий дозвіл: читання зовнішньої пам\'яті. Без цього дозволу програма не зможе отримати доступ до вашої галереї. Обов\'язковий дозвіл: записування на зовнішнє сховище. Програма не зможе отримати доступ до камери/галереї без цього дозволу. + Запит на визначення місцезнаходження Додатковий дозвіл: отримувати поточне розташування для підказок категорій Гаразд Місця поблизу @@ -539,4 +540,5 @@ Вивантажуйте фото у Вікісховище зі свого телефона. Завантажте застосунок: %1$s Поділитися програмкою через… Інформація про зображення + Чому %1$s має бути видалено? diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 66b74b66d..b59fabad6 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -44,7 +44,7 @@ 輕觸來檢視您上傳的項目 開始上傳%1$s 正在上傳%1$s - 即將完成上傳 %1$s + 即將完成上傳%1$s 上傳%1$s失敗 輕觸檢視 @@ -178,6 +178,7 @@ 請求存儲裝置權限 必要權限:讀取外部存儲裝置。否則應用程式無法存取您的圖庫。 必要權限:寫入外部存儲裝置。否則應用程式無法取用您的相機/圖庫。 + 請求位置權限 可有可無的權限:獲取目前的地理位置,以用於分類建議 附近地點 @@ -529,4 +530,5 @@ 在您的手機上更新照片到維基共享資源,下載共享資源應用程式:%1$s 分享應用程式透過… 圖片資訊 + 為何應刪除%1$s? From 7a5dc77057f75a5fff24c1568304ef2bc0b65229 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Date: Fri, 14 Jun 2019 01:09:41 +0530 Subject: [PATCH 8/8] Refactor uploads (#2981) * Feature/refractor uploads [WIP] (#2887) * Fix duplicate param information (#2515) * Bug fix issue #2476 (#2526) * Added wikidataEntityID in all db versions, handled db.execSql via method runQuery * Versioning and changelog for v2.10.2 (#2531) * Update changelog.md * Versioning for v2.10.2 * Update changelog.md * Bugfix/issue 2580 (#2584) * Corrected string placedholders in certain string files * Corrected string placedholders in certain string files[Bug fix #2580] * Bug Fix #2585 (#2647) * Bug Fix #2585 * Added null checks on view in SearchImageFragment when updating views from external sources * Disposed the disposables in SearchActivity and SearchImageFragment when no longer in active lifecycle * use FragmentUtils to verify fragment active state * Bug Fix issue #2648 (#2678) * Bug Fix issue #2648 * Handled external storage permission before file download * * Removed redudant check for permission in MediaDetailPagerFragment (Dexter already does that) * Removed duplicate code in PermissionUtil$checkPermissionsAndPerformAction, used the existing function with conditional extra parameters * string name typo correction * BugFix issue #2652 (#2706) * Addded null check on bookmark before operating on it * BugFix issue #2711 (#2712) * Added null checks in OkHttpJsonApiClient$searchImages MwQueryResponse * BugFix #2718 (#2719) * Handled null auth cookies * Fix #2791: NPE when nominating for deletion and leaving screen (#2792) * Bug Fix issue #2789 (#2790) * Handled Illegal State Exception for non existent appropriate view parents in ViewUtils$showShortSnackbar * BugFix #2720 (#2831) BugFix deprecated licenes #2720 * ui fixes, wip, upload * *Issue #2886, BugFix #2832[wip] * updated UploadActivity code * modified ui * Updated UploadPresenterTest * * updated interfaces names to follow names suffixed with Contract * added test cases * card view elevation * view pager disabled swipe * bug fix, duplicate image * used existing non-swipable view pager * Avoid image view resize with keyboard, added adjustPan and stateVisible as softinputMode for UploadActivity * retain UploadBaseFragment instances on orientation changes * * Added test cases for UploadMediaPresenter * Injected io and main thread schedulers * categories presenter test cased wip * Added CategoriesPresenter test * * Added the logic to show open map (with to be uploaded image's coordinates while uploading image) * codacy suggested changes * added java docs * Added travis_wait fot android-wait-for-emulator * ranamed interface onResponseCallback to Callback * * Added api to delete picture in UploadModel * cleanUp in UploadModel. once upload has been initiated * Removed unused methods from UploadModel and the corresponding test class * * Added tests for UploadPresenter * Travis suggested changes * Addded copy previous title and description * * Made the upload add descriptions visible when keyboard visible * add description request focus only when user manually requests it * Added JavaDocs, review suggested changes * Fix dagger injection * use DialogUtil to show info in descriptions * use activity context for DialogUtil * Minor changes * Bug fix, reduced the add description edit text clickable bound (#2973) * Bugfix/uploads (#3000) * merged with master * BugFix IllegalStateException * setRetainState(true), not required with FragmentStatePagerAdapter * Increase the ViewPager's Offscreen Limit, we want all the fragments to be active * BugFix, clear selected categoris for previous upload session * Clear Selected Categories * Addded JavaDocs for CategoriesModel * Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java * Added class level JavaDoc UploadRemoteDataSource * Added class level JavaDoc for UploadRepository * Added JavaDocs for ThumbnailsAdapter * Added JavaDocs for MediaLicensePresenter, CategoriesPresenter * Removed null check on category query * Show default catgeories based on image title and gps location when category text empty * Allow search for empty category search * Attached image scale listener to upload media image * Bug fix, reduced the add description edit text clickable bound * Fix memory leak (#3001) * Bugfix/uploads (#3002) * merged with master * BugFix IllegalStateException * setRetainState(true), not required with FragmentStatePagerAdapter * Increase the ViewPager's Offscreen Limit, we want all the fragments to be active * BugFix, clear selected categoris for previous upload session * Clear Selected Categories * Addded JavaDocs for CategoriesModel * Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java * Added class level JavaDoc UploadRemoteDataSource * Added class level JavaDoc for UploadRepository * Added JavaDocs for ThumbnailsAdapter * Added JavaDocs for MediaLicensePresenter, CategoriesPresenter * Removed null check on category query * Show default catgeories based on image title and gps location when category text empty * Allow search for empty category search * Attached image scale listener to upload media image * Bug fix, reduced the add description edit text clickable bound * Added tooltip in Title in UploadMediaFragment * BugFix recent categories * Updated test methods * Bugfix/uploads (#3011) * merged with master * BugFix IllegalStateException * setRetainState(true), not required with FragmentStatePagerAdapter * Increase the ViewPager's Offscreen Limit, we want all the fragments to be active * BugFix, clear selected categoris for previous upload session * Clear Selected Categories * Addded JavaDocs for CategoriesModel * Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java * Added class level JavaDoc UploadRemoteDataSource * Added class level JavaDoc for UploadRepository * Added JavaDocs for ThumbnailsAdapter * Added JavaDocs for MediaLicensePresenter, CategoriesPresenter * Removed null check on category query * Show default catgeories based on image title and gps location when category text empty * Allow search for empty category search * Attached image scale listener to upload media image * Bug fix, reduced the add description edit text clickable bound * Added tooltip in Title in UploadMediaFragment * BugFix recent categories * Updated test methods * Avoid memory leak, free the adpater in MediaLicenseFragment.onDestroyView * bugfix/uploads (#3012) * merged with master * BugFix IllegalStateException * setRetainState(true), not required with FragmentStatePagerAdapter * Increase the ViewPager's Offscreen Limit, we want all the fragments to be active * BugFix, clear selected categoris for previous upload session * Clear Selected Categories * Addded JavaDocs for CategoriesModel * Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java * Added class level JavaDoc UploadRemoteDataSource * Added class level JavaDoc for UploadRepository * Added JavaDocs for ThumbnailsAdapter * Added JavaDocs for MediaLicensePresenter, CategoriesPresenter * Removed null check on category query * Show default catgeories based on image title and gps location when category text empty * Allow search for empty category search * Attached image scale listener to upload media image * Bug fix, reduced the add description edit text clickable bound * Added tooltip in Title in UploadMediaFragment * BugFix recent categories * Updated test methods * Avoid memory leak, free the adpater in MediaLicenseFragment.onDestroyView * BugFix Illegal State Exception in ViewpPagerAdapter * Remove irrelevant comment * merge conflict with strings (#3016) --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 7 +- .../fr/free/nrw/commons/BasePresenter.java | 4 +- .../main/java/fr/free/nrw/commons/Utils.java | 4 +- .../commons/campaigns/CampaignsPresenter.java | 7 +- .../nrw/commons/category/CategoriesModel.java | 156 ++-- .../nrw/commons/category/CategoryItem.java | 2 +- .../contributions/ContributionDao.java | 2 +- .../di/CommonsApplicationComponent.java | 3 +- .../commons/di/CommonsApplicationModule.java | 19 + .../nrw/commons/di/FragmentBuilderModule.java | 11 + .../nrw/commons/explore/SearchActivity.java | 11 - .../commons/media/MediaDetailFragment.java | 3 - .../repository/UploadLocalDataSource.java | 150 ++++ .../repository/UploadRemoteDataSource.java | 179 ++++ .../commons/repository/UploadRepository.java | 265 ++++++ .../free/nrw/commons/upload/Description.java | 18 +- .../commons/upload/DescriptionsAdapter.java | 235 ++--- .../nrw/commons/upload/FileProcessor.java | 5 +- .../free/nrw/commons/upload/GPSExtractor.java | 8 +- .../upload/SimilarImageDialogFragment.java | 15 +- .../upload/SpinnerLanguagesAdapter.java | 28 +- .../upload/ThumbnailClickedListener.java | 4 +- .../nrw/commons/upload/ThumbnailsAdapter.java | 112 +++ .../fr/free/nrw/commons/upload/Title.java | 4 + .../nrw/commons/upload/UploadActivity.java | 842 ++++++------------ .../commons/upload/UploadBaseFragment.java | 41 + .../nrw/commons/upload/UploadContract.java | 40 + .../nrw/commons/upload/UploadController.java | 6 +- .../free/nrw/commons/upload/UploadModel.java | 317 +++---- .../free/nrw/commons/upload/UploadModule.java | 36 + .../nrw/commons/upload/UploadPresenter.java | 464 ++-------- .../upload/UploadThumbnailRenderer.java | 53 -- .../UploadThumbnailsAdapterFactory.java | 24 - .../free/nrw/commons/upload/UploadView.java | 3 +- .../upload/categories/CategoriesContract.java | 42 + .../categories/CategoriesPresenter.java | 144 +++ .../categories/UploadCategoriesFragment.java | 200 +++++ .../upload/license/MediaLicenseContract.java | 26 + .../upload/license/MediaLicenseFragment.java | 181 ++++ .../upload/license/MediaLicensePresenter.java | 75 ++ .../UploadMediaDetailFragment.java | 402 +++++++++ .../UploadMediaDetailsContract.java | 52 ++ .../mediaDetails/UploadMediaPresenter.java | 194 ++++ .../fr/free/nrw/commons/utils/DialogUtil.java | 27 + .../res/drawable/drawable_thumbnail_image.xml | 9 + .../res/drawable/thumbnail_not_selected.xml | 5 + .../main/res/drawable/thumbnail_selected.xml | 11 + app/src/main/res/layout/activity_upload.xml | 93 +- .../layout/activity_upload_bottom_card.xml | 199 ----- .../res/layout/activity_upload_categories.xml | 127 --- .../res/layout/activity_upload_license.xml | 116 --- .../layout/activity_upload_please_wait.xml | 29 - .../res/layout/fragment_media_license.xml | 102 +++ .../fragment_upload_media_detail_fragment.xml | 161 ++++ .../main/res/layout/item_upload_thumbnail.xml | 59 +- .../main/res/layout/row_item_description.xml | 2 +- app/src/main/res/layout/row_item_title.xml | 2 +- .../res/layout/upload_categories_fragment.xml | 114 +++ app/src/main/res/values-ro/strings.xml | 2 +- app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 5 +- .../commons/upload/CategoriesPresenterTest.kt | 80 ++ .../upload/MediaLicensePresenterTest.kt | 66 ++ .../upload/UploadMediaPresenterTest.kt | 110 +++ .../nrw/commons/upload/UploadModelTest.kt | 50 -- .../nrw/commons/upload/UploadPresenterTest.kt | 73 +- .../fr.free.nrw.commons_2019.04.15_22.10.li | Bin 0 -> 1089676 bytes 68 files changed, 3753 insertions(+), 2086 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/repository/UploadLocalDataSource.java create mode 100644 app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java create mode 100644 app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadContract.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadModule.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java create mode 100644 app/src/main/res/drawable/drawable_thumbnail_image.xml create mode 100644 app/src/main/res/drawable/thumbnail_not_selected.xml create mode 100644 app/src/main/res/drawable/thumbnail_selected.xml delete mode 100644 app/src/main/res/layout/activity_upload_bottom_card.xml delete mode 100644 app/src/main/res/layout/activity_upload_categories.xml delete mode 100644 app/src/main/res/layout/activity_upload_license.xml delete mode 100644 app/src/main/res/layout/activity_upload_please_wait.xml create mode 100644 app/src/main/res/layout/fragment_media_license.xml create mode 100644 app/src/main/res/layout/fragment_upload_media_detail_fragment.xml create mode 100644 app/src/main/res/layout/upload_categories_fragment.xml create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/upload/MediaLicensePresenterTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt create mode 100644 captures/fr.free.nrw.commons_2019.04.15_22.10.li diff --git a/app/build.gradle b/app/build.gradle index 8a8ccd5e5..718057816 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,6 +65,8 @@ dependencies { testImplementation 'androidx.test:core:1.2.0' testImplementation 'com.nhaarman:mockito-kotlin:1.5.0' testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0' + testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5" + testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5" // Android testing androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dffea644c..b2e434bd4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -50,10 +50,13 @@ - + android:windowSoftInputMode="adjustResize" + > diff --git a/app/src/main/java/fr/free/nrw/commons/BasePresenter.java b/app/src/main/java/fr/free/nrw/commons/BasePresenter.java index 041fde6b2..2aa160520 100644 --- a/app/src/main/java/fr/free/nrw/commons/BasePresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/BasePresenter.java @@ -3,11 +3,11 @@ package fr.free.nrw.commons; /** * Base presenter, enforcing contracts to atach and detach view */ -public interface BasePresenter { +public interface BasePresenter { /** * Until a view is attached, it is open to listen events from the presenter */ - void onAttachView(MvpView view); + void onAttachView(T view); /** * Detaching a view makes sure that the view no more receives events from the presenter diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java index 43c708460..b7e09d9c2 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -11,6 +11,8 @@ import android.widget.Toast; import org.wikipedia.dataclient.WikiSite; import org.wikipedia.page.PageTitle; +import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.utils.ViewUtil; import java.util.Locale; import java.util.regex.Pattern; @@ -18,9 +20,7 @@ import java.util.regex.Pattern; import androidx.annotation.NonNull; import androidx.browser.customtabs.CustomTabsIntent; import androidx.core.content.ContextCompat; -import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.settings.Prefs; -import fr.free.nrw.commons.utils.ViewUtil; import timber.log.Timber; import static android.widget.Toast.LENGTH_SHORT; diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java index 0ac5ec8a7..4aa062718 100644 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java @@ -28,7 +28,7 @@ import timber.log.Timber; * success and error */ @Singleton -public class CampaignsPresenter implements BasePresenter { +public class CampaignsPresenter implements BasePresenter { private final OkHttpJsonApiClient okHttpJsonApiClient; private ICampaignsView view; @@ -40,8 +40,9 @@ public class CampaignsPresenter implements BasePresenter { this.okHttpJsonApiClient = okHttpJsonApiClient; } - @Override public void onAttachView(MvpView view) { - this.view = (ICampaignsView) view; + @Override + public void onAttachView(ICampaignsView view) { + this.view = view; } @Override public void onDetachView() { diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java index 68f53ca36..9b084da49 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java @@ -1,25 +1,25 @@ package fr.free.nrw.commons.category; import android.text.TextUtils; - +import fr.free.nrw.commons.kvstore.JsonKvStore; +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 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.kvstore.JsonKvStore; -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 { +/** + * The model class for categories in upload + */ +public class CategoriesModel{ private static final int SEARCH_CATS_LIMIT = 25; private final MediaWikiApi mwApi; @@ -41,13 +41,22 @@ public class CategoriesModel implements CategoryClickedListener { this.selectedCategories = new ArrayList<>(); } - //region Misc. utility methods + /** + * Sorts CategoryItem by similarity + * @param filter + * @return + */ public Comparator sortBySimilarity(final String filter) { Comparator stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter); return (firstItem, secondItem) -> stringSimilarityComparator .compare(firstItem.getName(), secondItem.getName()); } + /** + * Returns if the item contains an year + * @param item + * @return + */ public boolean containsYear(String item) { //Check for current and previous year to exclude these categories from removal Calendar now = Calendar.getInstance(); @@ -67,6 +76,10 @@ public class CategoriesModel implements CategoryClickedListener { || (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*"))); } + /** + * Updates category count in category dao + * @param item + */ public void updateCategoryCount(CategoryItem item) { Category category = categoryDao.find(item.getName()); @@ -78,29 +91,27 @@ public class CategoriesModel implements CategoryClickedListener { category.incTimesUsed(); categoryDao.save(category); } - //endregion - - //region Category Caching - public void cacheAll(HashMap> categories) { - categoriesCache.putAll(categories); - } - - public HashMap> getCategoriesCache() { - return categoriesCache; - } boolean cacheContainsKey(String term) { return categoriesCache.containsKey(term); } //endregion - //region Category searching + /** + * Regional category search + * @param term + * @param imageTitleList + * @return + */ public Observable searchAll(String term, List imageTitleList) { - //If user hasn't typed anything in yet, get GPS and recent items + //If query text is empty, show him category based on gps and title and recent searches if (TextUtils.isEmpty(term)) { - return gpsCategories() - .concatWith(titleCategories(imageTitleList)) - .concatWith(recentCategories()); + Observable categoryItemObservable = gpsCategories() + .concatWith(titleCategories(imageTitleList)); + if (hasDirectCategories()) { + categoryItemObservable.concatWith(directCategories().concatWith(recentCategories())); + } + return categoryItemObservable; } //if user types in something that is in cache, return cached category @@ -115,43 +126,28 @@ public class CategoriesModel implements CategoryClickedListener { .map(name -> new CategoryItem(name, false)); } - public Observable searchCategories(String term, List 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)); - } + /** + * Returns cached categories + * @param term + * @return + */ private ArrayList getCachedCategories(String term) { return categoriesCache.get(term); } - public Observable defaultCategories(List titleList) { - Observable 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()); - } - } - + /** + * Returns if we have a category in DirectKV Store + * @return + */ private boolean hasDirectCategories() { return !directKvStore.getString("Category", "").equals(""); } + /** + * Returns categories in DirectKVStore + * @return + */ private Observable directCategories() { String directCategory = directKvStore.getString("Category", ""); List categoryList = new ArrayList<>(); @@ -164,30 +160,49 @@ public class CategoriesModel implements CategoryClickedListener { return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false)); } + /** + * Returns GPS categories + * @return + */ Observable gpsCategories() { return Observable.fromIterable(gpsCategoryModel.getCategoryList()) .map(name -> new CategoryItem(name, false)); } + /** + * Returns title based categories + * @param titleList + * @return + */ private Observable titleCategories(List titleList) { return Observable.fromIterable(titleList) .concatMap(this::getTitleCategories); } + /** + * Return category for single title + * @param title + * @return + */ private Observable getTitleCategories(String title) { return mwApi.searchTitles(title, SEARCH_CATS_LIMIT) .map(name -> new CategoryItem(name, false)); } + /** + * Returns recent categories + * @return + */ private Observable 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) { + /** + * Handles category item selection + * @param item + */ + public void onCategoryItemClicked(CategoryItem item) { if (item.isSelected()) { selectCategory(item); updateCategoryCount(item); @@ -196,22 +211,35 @@ public class CategoriesModel implements CategoryClickedListener { } } + /** + * Select's category + * @param item + */ public void selectCategory(CategoryItem item) { selectedCategories.add(item); } + /** + * Unselect Category + * @param item + */ public void unselectCategory(CategoryItem item) { selectedCategories.remove(item); } - public int selectedCategoriesCount() { - return selectedCategories.size(); - } + /** + * Get Selected Categories + * @return + */ public List getSelectedCategories() { return selectedCategories; } + /** + * Get Categories String List + * @return + */ public List getCategoryStringList() { List output = new ArrayList<>(); for (CategoryItem item : selectedCategories) { @@ -219,6 +247,12 @@ public class CategoriesModel implements CategoryClickedListener { } return output; } - //endregion + /** + * Cleanup the existing in memory cache's + */ + public void cleanUp() { + this.categoriesCache.clear(); + this.selectedCategories.clear(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java index f3ade09d8..f6c954f43 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java @@ -19,7 +19,7 @@ public class CategoryItem implements Parcelable { } }; - CategoryItem(String name, boolean selected) { + public CategoryItem(String name, boolean selected) { this.name = name; this.selected = selected; } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java index 2e9ca5327..ec02c7313 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java @@ -100,7 +100,7 @@ public class ContributionDao { cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime()); } cv.put(Table.COLUMN_LENGTH, contribution.getDataLength()); - //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 + //This was always meant to store the date created..If somehow date created is not fetched while actually saving the contribution, lets saveValue 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()); diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java index e63f2b669..72793f2c8 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -15,6 +15,7 @@ import fr.free.nrw.commons.nearby.PlaceRenderer; import fr.free.nrw.commons.review.ReviewController; import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.upload.FileProcessor; +import fr.free.nrw.commons.upload.UploadModule; import fr.free.nrw.commons.widget.PicOfDayAppWidget; @@ -27,7 +28,7 @@ import fr.free.nrw.commons.widget.PicOfDayAppWidget; ActivityBuilderModule.class, FragmentBuilderModule.class, ServiceBuilderModule.class, - ContentProviderBuilderModule.class + ContentProviderBuilderModule.class, UploadModule.class }) public interface CommonsApplicationComponent extends AndroidInjector { void inject(CommonsApplication application); diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index 7f0ee4048..36aba1668 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -9,6 +9,11 @@ import com.google.gson.Gson; import org.wikipedia.dataclient.WikiSite; +import io.reactivex.Scheduler; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import org.wikipedia.dataclient.WikiSite; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -37,6 +42,8 @@ import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl; @SuppressWarnings({"WeakerAccess", "unused"}) public class CommonsApplicationModule { private Context applicationContext; + public static final String IO_THREAD="io_thread"; + public static final String MAIN_THREAD="main_thread"; public CommonsApplicationModule(Context applicationContext) { this.applicationContext = applicationContext; @@ -172,4 +179,16 @@ public class CommonsApplicationModule { public boolean provideIsBetaVariant() { return ConfigUtils.isBetaFlavour(); } + + @Named(IO_THREAD) + @Provides + public Scheduler providesIoThread(){ + return Schedulers.io(); + } + + @Named(MAIN_THREAD) + @Provides + public Scheduler providesMainThread(){ + return AndroidSchedulers.mainThread(); + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java index 97263d128..8c5a7bce7 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -18,6 +18,9 @@ import fr.free.nrw.commons.nearby.NearbyListFragment; import fr.free.nrw.commons.nearby.NearbyMapFragment; import fr.free.nrw.commons.review.ReviewImageFragment; import fr.free.nrw.commons.settings.SettingsFragment; +import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment; +import fr.free.nrw.commons.upload.license.MediaLicenseFragment; +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment; @Module @SuppressWarnings({"WeakerAccess", "unused"}) @@ -71,4 +74,12 @@ public abstract class FragmentBuilderModule { @ContributesAndroidInjector abstract ReviewImageFragment bindReviewOutOfContextFragment(); + @ContributesAndroidInjector + abstract UploadMediaDetailFragment bindUploadMediaDetailFragment(); + + @ContributesAndroidInjector + abstract UploadCategoriesFragment bindUploadCategoriesFragment(); + + @ContributesAndroidInjector + abstract MediaLicenseFragment bindMediaLicenseFragment(); } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java index 50eb1af56..6ca0a13af 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java @@ -16,10 +16,6 @@ import butterknife.ButterKnife; import com.google.android.material.tabs.TabLayout; import com.jakewharton.rxbinding2.view.RxView; import com.jakewharton.rxbinding2.widget.RxSearchView; -import butterknife.BindView; -import butterknife.ButterKnife; -import com.jakewharton.rxbinding2.view.RxView; -import com.jakewharton.rxbinding2.widget.RxSearchView; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.explore.categories.SearchCategoryFragment; @@ -33,13 +29,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import io.reactivex.disposables.Disposable; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; /** * Represents search screen of this app diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 4521104c7..18349f525 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -57,9 +57,6 @@ import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import org.apache.commons.lang3.StringUtils; -import org.wikipedia.util.DateUtil; -import org.wikipedia.util.StringUtil; import timber.log.Timber; import static android.view.View.GONE; diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadLocalDataSource.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadLocalDataSource.java new file mode 100644 index 000000000..3f4a58bd3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadLocalDataSource.java @@ -0,0 +1,150 @@ +package fr.free.nrw.commons.repository; + +import androidx.annotation.Nullable; +import fr.free.nrw.commons.kvstore.JsonKvStore; +import fr.free.nrw.commons.upload.UploadModel; +import fr.free.nrw.commons.upload.UploadModel.UploadItem; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +/** + * The Local Data Source for UploadRepository, fetches and returns data from local db/shared prefernces + */ + +@Singleton +public class UploadLocalDataSource { + + private final UploadModel uploadModel; + private JsonKvStore defaultKVStore; + + @Inject + public UploadLocalDataSource( + @Named("default_preferences") JsonKvStore defaultKVStore, + UploadModel uploadModel) { + this.defaultKVStore = defaultKVStore; + this.uploadModel = uploadModel; + } + + + /** + * Fetches and returns the string list of valid licenses + * + * @return + */ + public List getLicenses() { + return uploadModel.getLicenses(); + } + + /** + * Returns the number of Upload Items + * + * @return + */ + public int getCount() { + return uploadModel.getCount(); + } + + /** + * Fetches and return the selected license for the current upload + * + * @return + */ + public String getSelectedLicense() { + return uploadModel.getSelectedLicense(); + } + + /** + * Set selected license for the current upload + * + * @param licenseName + */ + public void setSelectedLicense(String licenseName) { + uploadModel.setSelectedLicense(licenseName); + } + + /** + * Updates the current upload item + * + * @param index + * @param uploadItem + */ + public void updateUploadItem(int index, UploadItem uploadItem) { + uploadModel.updateUploadItem(index, uploadItem); + } + + /** + * upload is halted, cleanup the acquired resources + */ + public void cleanUp() { + uploadModel.cleanUp(); + } + + /** + * Deletes the upload item at the current index + * + * @param filePath + */ + public void deletePicture(String filePath) { + uploadModel.deletePicture(filePath); + } + + /** + * Fethces and returns the previous upload item, if any, returns null otherwise + * + * @param index + * @return + */ + @Nullable + public UploadItem getPreviousUploadItem(int index) { + if (index - 1 >= 0) { + return uploadModel.getItems().get(index - 1); + } + return null; //There is no previous item to copy details + } + + /** + * saves boolean value in default store + * + * @param key + * @param value + */ + public void saveValue(String key, boolean value) { + defaultKVStore.putBoolean(key, value); + } + + /** + * saves string value in default store + * + * @param key + * @param value + */ + public void saveValue(String key, String value) { + defaultKVStore.putString(key, value); + } + + /** + * Fetches and returns string value from the default store + * + * @param key + * @param defaultValue + * @return + */ + public String getValue(String key, String defaultValue) { + return defaultKVStore.getString(key, defaultValue); + } + + /** + * Fetches and returns boolean value from the default store + * + * @param key + * @param defaultValue + * @return + */ + public boolean getValue(String key, boolean defaultValue) { + return defaultKVStore.getBoolean(key, defaultValue); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java new file mode 100644 index 000000000..938b6f30d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java @@ -0,0 +1,179 @@ +package fr.free.nrw.commons.repository; + +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.filepicker.UploadableFile; +import fr.free.nrw.commons.nearby.Place; +import fr.free.nrw.commons.upload.SimilarImageInterface; +import fr.free.nrw.commons.upload.UploadController; +import fr.free.nrw.commons.upload.UploadModel; +import fr.free.nrw.commons.upload.UploadModel.UploadItem; +import io.reactivex.Observable; +import io.reactivex.Single; + +import java.util.Comparator; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * This class would act as the data source for remote operations for UploadActivity + */ +@Singleton +public class UploadRemoteDataSource { + + private UploadModel uploadModel; + private UploadController uploadController; + private CategoriesModel categoriesModel; + + @Inject + public UploadRemoteDataSource(UploadModel uploadModel, UploadController uploadController, + CategoriesModel categoriesModel) { + this.uploadModel = uploadModel; + this.uploadController = uploadController; + this.categoriesModel = categoriesModel; + } + + /** + * asks the UploadModel to build the contributions + * + * @return + */ + public Observable buildContributions() { + return uploadModel.buildContributions(); + } + + /** + * asks the UploadService to star the uplaod for + * + * @param contribution + */ + public void startUpload(Contribution contribution) { + uploadController.startUpload(contribution); + } + + /** + * returns the list of UploadItem from the UploadModel + * + * @return + */ + public List getUploads() { + return uploadModel.getUploads(); + } + + /** + * Prepare the UploadService for the upload + */ + public void prepareService() { + uploadController.prepareService(); + } + + /** + * Clean up the UploadController + */ + public void cleanup() { + uploadController.cleanup(); + } + + /** + * Clean up the selected categories + */ + public void clearSelectedCategories(){ + //This needs further refactoring, this should not be here, right now the structure wont suppoort rhis + categoriesModel.cleanUp(); + } + + /** + * returnt the list of selected categories + * + * @return + */ + public List getSelectedCategories() { + return categoriesModel.getSelectedCategories(); + } + + /** + * all categories from MWApi + * + * @param query + * @param imageTitleList + * @return + */ + public Observable searchAll(String query, List imageTitleList) { + return categoriesModel.searchAll(query, imageTitleList); + } + + /** + * returns the string list of categories + * + * @return + */ + public List getCategoryStringList() { + return categoriesModel.getCategoryStringList(); + } + + /** + * sets the selected categories in the UploadModel + * + * @param categoryStringList + */ + public void setSelectedCategories(List categoryStringList) { + uploadModel.setSelectedCategories(categoryStringList); + } + + /** + * handles category selection/unselection + * + * @param categoryItem + */ + public void onCategoryClicked(CategoryItem categoryItem) { + categoriesModel.onCategoryItemClicked(categoryItem); + } + + /** + * returns category sorted based on similarity with query + * + * @param query + * @return + */ + public Comparator sortBySimilarity(String query) { + return categoriesModel.sortBySimilarity(query); + } + + /** + * prunes the category list for irrelevant categories see #750 + * + * @param name + * @return + */ + public boolean containsYear(String name) { + return categoriesModel.containsYear(name); + } + + /** + * pre process the UploadableFile + * + * @param uploadableFile + * @param place + * @param source + * @param similarImageInterface + * @return + */ + public Observable preProcessImage(UploadableFile uploadableFile, Place place, + String source, SimilarImageInterface similarImageInterface) { + return uploadModel.preProcessImage(uploadableFile, place, source, similarImageInterface); + } + + /** + * ask the UplaodModel for the image quality of the UploadItem + * + * @param uploadItem + * @param shouldValidateTitle + * @return + */ + public Single getImageQuality(UploadItem uploadItem, boolean shouldValidateTitle) { + return uploadModel.getImageQuality(uploadItem, shouldValidateTitle); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java new file mode 100644 index 000000000..dbd0f6134 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java @@ -0,0 +1,265 @@ +package fr.free.nrw.commons.repository; + +import fr.free.nrw.commons.category.CategoryItem; +import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.filepicker.UploadableFile; +import fr.free.nrw.commons.nearby.Place; +import fr.free.nrw.commons.upload.SimilarImageInterface; +import fr.free.nrw.commons.upload.UploadModel.UploadItem; +import io.reactivex.Observable; +import io.reactivex.Single; + +import java.util.Comparator; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * The repository class for UploadActivity + */ +@Singleton +public class UploadRepository { + + private UploadLocalDataSource localDataSource; + private UploadRemoteDataSource remoteDataSource; + + @Inject + public UploadRepository(UploadLocalDataSource localDataSource, + UploadRemoteDataSource remoteDataSource) { + this.localDataSource = localDataSource; + this.remoteDataSource = remoteDataSource; + } + + /** + * asks the RemoteDataSource to build contributions + * + * @return + */ + public Observable buildContributions() { + return remoteDataSource.buildContributions(); + } + + /** + * asks the RemoteDataSource to start upload for the contribution + * + * @param contribution + */ + public void startUpload(Contribution contribution) { + remoteDataSource.startUpload(contribution); + } + + /** + * Fetches and returns all the Upload Items + * + * @return + */ + public List getUploads() { + return remoteDataSource.getUploads(); + } + + /** + * asks the RemoteDataSource to prepare the Upload Service + */ + public void prepareService() { + remoteDataSource.prepareService(); + } + + /** + *Prepare for a fresh upload + */ + public void cleanup() { + localDataSource.cleanUp(); + remoteDataSource.clearSelectedCategories(); + } + + /** + * Fetches and returns the selected categories for the current upload + * + * @return + */ + public List getSelectedCategories() { + return remoteDataSource.getSelectedCategories(); + } + + /** + * all categories from MWApi + * + * @param query + * @param imageTitleList + * @return + */ + public Observable searchAll(String query, List imageTitleList) { + return remoteDataSource.searchAll(query, imageTitleList); + } + + /** + * returns the string list of categories + * + * @return + */ + + public List getCategoryStringList() { + return remoteDataSource.getCategoryStringList(); + } + + /** + * sets the list of selected categories for the current upload + * + * @param categoryStringList + */ + public void setSelectedCategories(List categoryStringList) { + remoteDataSource.setSelectedCategories(categoryStringList); + } + + /** + * handles the category selection/deselection + * + * @param categoryItem + */ + public void onCategoryClicked(CategoryItem categoryItem) { + remoteDataSource.onCategoryClicked(categoryItem); + } + + /** + * returns category sorted based on similarity with query + * + * @param query + * @return + */ + public Comparator sortBySimilarity(String query) { + return remoteDataSource.sortBySimilarity(query); + } + + /** + * prunes the category list for irrelevant categories see #750 + * + * @param name + * @return + */ + public boolean containsYear(String name) { + return remoteDataSource.containsYear(name); + } + + /** + * retursn the string list of available license from the LocalDataSource + * + * @return + */ + public List getLicenses() { + return localDataSource.getLicenses(); + } + + /** + * returns the selected license for the current upload + * + * @return + */ + public String getSelectedLicense() { + return localDataSource.getSelectedLicense(); + } + + /** + * returns the number of Upload Items + * + * @return + */ + public int getCount() { + return localDataSource.getCount(); + } + + /** + * ask the RemoteDataSource to pre process the image + * + * @param uploadableFile + * @param place + * @param source + * @param similarImageInterface + * @return + */ + public Observable preProcessImage(UploadableFile uploadableFile, Place place, + String source, SimilarImageInterface similarImageInterface) { + return remoteDataSource + .preProcessImage(uploadableFile, place, source, similarImageInterface); + } + + /** + * query the RemoteDataSource for image quality + * + * @param uploadItem + * @param shouldValidateTitle + * @return + */ + public Single getImageQuality(UploadItem uploadItem, boolean shouldValidateTitle) { + return remoteDataSource.getImageQuality(uploadItem, shouldValidateTitle); + } + + /** + * asks the LocalDataSource to update the Upload Item + * + * @param index + * @param uploadItem + */ + public void updateUploadItem(int index, UploadItem uploadItem) { + localDataSource.updateUploadItem(index, uploadItem); + } + + /** + * asks the LocalDataSource to delete the file with the given file path + * + * @param filePath + */ + public void deletePicture(String filePath) { + localDataSource.deletePicture(filePath); + } + + /** + * fetches and returns the previous upload item + * + * @param index + * @return + */ + public UploadItem getPreviousUploadItem(int index) { + return localDataSource.getPreviousUploadItem(index); + } + + /** + * Save boolean value locally + * + * @param key + * @param value + */ + public void saveValue(String key, boolean value) { + localDataSource.saveValue(key, value); + } + + /** + * save string value locally + * + * @param key + * @param value + */ + public void saveValue(String key, String value) { + localDataSource.saveValue(key, value); + } + + /** + * fetch the string value for the associated key + * + * @param key + * @param value + * @return + */ + public String getValue(String key, String value) { + return localDataSource.getValue(key, value); + } + + /** + * set selected license for the current upload + * + * @param licenseName + */ + public void setSelectedLicense(String licenseName) { + localDataSource.setSelectedLicense(licenseName); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Description.java b/app/src/main/java/fr/free/nrw/commons/upload/Description.java index ae18d4adb..c6f69584e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/Description.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/Description.java @@ -5,11 +5,12 @@ import java.util.List; /** * Holds a description of an item being uploaded by {@link UploadActivity} */ -class Description { +public class Description { private String languageCode; private String descriptionText; private int selectedLanguageIndex = -1; + private boolean isManuallyAdded=false; /** * @return The language code ie. "en" or "fr" @@ -47,6 +48,21 @@ class Description { this.selectedLanguageIndex = selectedLanguageIndex; } + /** + * returns if the description was added manually (by the user, or we have added it programaticallly) + * @return + */ + public boolean isManuallyAdded() { + return isManuallyAdded; + } + + /** + * sets to true if the description was manually added by the user + * @param manuallyAdded + */ + public void setManuallyAdded(boolean manuallyAdded) { + isManuallyAdded = manuallyAdded; + } /** * Formats the list of descriptions into the format Commons requires for uploads. diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java index 7aca51908..a24a791bf 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java @@ -1,22 +1,22 @@ package fr.free.nrw.commons.upload; -import android.annotation.SuppressLint; import android.content.Context; import android.graphics.drawable.Drawable; import android.text.TextUtils; +import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.EditText; import java.util.ArrayList; import java.util.List; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatEditText; import androidx.appcompat.widget.AppCompatSpinner; import androidx.recyclerview.widget.RecyclerView; import butterknife.BindView; @@ -24,60 +24,35 @@ import butterknife.ButterKnife; 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; -class DescriptionsAdapter extends RecyclerView.Adapter { +public class DescriptionsAdapter extends RecyclerView.Adapter { - private Title title; private List descriptions; - private Context context; private Callback callback; - private Subject titleChangedSubject; private BiMap selectedLanguages; - private UploadView uploadView; - DescriptionsAdapter(UploadView uploadView) { - title = new Title(); + public DescriptionsAdapter() { descriptions = new ArrayList<>(); - titleChangedSubject = BehaviorSubject.create(); selectedLanguages = new BiMap<>(); - this.uploadView = uploadView; } - void setCallback(Callback callback) { + public void setCallback(Callback callback) { this.callback = callback; } - void setItems(Title title, List descriptions) { + public void setItems(List descriptions) { this.descriptions = descriptions; - this.title = title; selectedLanguages = new BiMap<>(); notifyDataSetChanged(); } - @Override - public int getItemViewType(int position) { - if (position == 0) return 1; - else return 2; - } - @NonNull @Override 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(); - return new ViewHolder(view); + return new ViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.row_item_description, parent, false)); } @Override @@ -87,29 +62,21 @@ class DescriptionsAdapter extends RecyclerView.Adapter getDescriptions() { + public List getDescriptions() { return descriptions; } - void addDescription(Description description) { + public void addDescription(Description description) { this.descriptions.add(description); - notifyItemInserted(descriptions.size() + 1); - } - - public Title getTitle() { - return title; - } - - public void setTitle(Title title) { - this.title = title; - notifyItemInserted(0); + notifyItemInserted(descriptions.size()); } public class ViewHolder extends RecyclerView.ViewHolder { @@ -119,98 +86,53 @@ class DescriptionsAdapter extends RecyclerView.Adapter{ - title.setTitleText(titleText); - titleChangedSubject.onNext(titleText); - })); - - descItemEditText.setOnFocusChangeListener((v, hasFocus) -> { - if (!hasFocus) { - ViewUtil.hideKeyboard(v); - } - }); - + descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), + null); descItemEditText.setOnTouchListener((v, event) -> { - // Check this is a touch up event - if(event.getAction() != MotionEvent.ACTION_UP) return false; - - // Check we are tapping within 15px of the info icon - int extraTapArea = 15; - Drawable info = descItemEditText.getCompoundDrawables()[2]; - int infoHitboxX = descItemEditText.getWidth() - info.getBounds().width(); - if (event.getX() + extraTapArea < infoHitboxX) return false; - - // If the above are true, show the info dialog - callback.showAlert(R.string.media_detail_title, R.string.title_info); - return true; + //2 is for drawable right + float twelveDpInPixels = convertDpToPixel(12, descItemEditText.getContext()); + if (event.getAction() == MotionEvent.ACTION_UP && descItemEditText.getCompoundDrawables()[2].getBounds().contains((int)(descItemEditText.getWidth()-(event.getX()+twelveDpInPixels)),(int)(event.getY()-twelveDpInPixels))){ + if (getAdapterPosition() == 0) { + callback.showAlert(R.string.media_detail_description, + R.string.description_info); + } + return true; + } + return false; }); } else { - Description description = descriptions.get(position - 1); - Timber.d("Description is " + description); - if (!TextUtils.isEmpty(description.getDescriptionText())) { - descItemEditText.setText(description.getDescriptionText()); - } else { - descItemEditText.setText(""); - } - - // Show the info icon for the first description - if (position == 1) { - descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), null); - descItemEditText.setOnTouchListener((v, event) -> { - // Check this is a touch up event - if(event.getAction() != MotionEvent.ACTION_UP) return false; - - // Check we are tapping within 15px of the info icon - int extraTapArea = 15; - Drawable info = descItemEditText.getCompoundDrawables()[2]; - int infoHitboxX = descItemEditText.getWidth() - info.getBounds().width(); - if (event.getX() + extraTapArea < infoHitboxX) return false; - - // If the above are true, show the info dialog - callback.showAlert(R.string.media_detail_description, R.string.description_info); - return true; - }); - } - - descItemEditText.addTextChangedListener(new AbstractTextWatcher(descriptionText->{ - descriptions.get(position - 1).setDescriptionText(descriptionText); - })); - - descItemEditText.setOnFocusChangeListener((v, hasFocus) -> { - if (!hasFocus) { - ViewUtil.hideKeyboard(v); - } else { - uploadView.setTopCardState(false); - } - }); - - initLanguageSpinner(position, description); + descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); } + descItemEditText.addTextChangedListener(new AbstractTextWatcher( + descriptionText -> descriptions.get(position) + .setDescriptionText(descriptionText))); + initLanguageSpinner(position, description); + + //If the description was manually added by the user, it deserves focus, if not, let the user decide + if (description.isManuallyAdded()) { + descItemEditText.requestFocus(); + } else { + descItemEditText.clearFocus(); + } } /** @@ -219,48 +141,24 @@ class DescriptionsAdapter extends RecyclerView.Adapter= 0) { - // sets the spinner value to the index of first non-selected language - spinnerDescriptionLanguages.setSelection(availableLangIndex); - selectedLanguages.put(spinnerDescriptionLanguages, languagesAdapter.getLanguageCode(position)); - } - } - } else { - spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex()); - selectedLanguages.put(spinnerDescriptionLanguages, description.getLanguageCode()); - } - - //TODO do it the butterknife way spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView adapterView, View view, int position, - long l) { + long l) { description.setSelectedLanguageIndex(position); - String languageCode = ((SpinnerLanguagesAdapter) adapterView.getAdapter()).getLanguageCode(position); + String languageCode = ((SpinnerLanguagesAdapter) adapterView.getAdapter()) + .getLanguageCode(position); description.setLanguageCode(languageCode); selectedLanguages.remove(adapterView); selectedLanguages.put(adapterView, languageCode); - ((SpinnerLanguagesAdapter) adapterView.getAdapter()).selectedLangCode = languageCode; + ((SpinnerLanguagesAdapter) adapterView + .getAdapter()).selectedLangCode = languageCode; } @Override @@ -268,18 +166,43 @@ class DescriptionsAdapter extends RecyclerView.Adapter { + + List uploadableFiles; + private Callback callback; + + public ThumbnailsAdapter(Callback callback) { + this.uploadableFiles = new ArrayList<>(); + this.callback = callback; + } + + /** + * Sets the data, the media files + * @param uploadableFiles + */ + public void setUploadableFiles( + List uploadableFiles) { + this.uploadableFiles=uploadableFiles; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { + return new ViewHolder(LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.item_upload_thumbnail, viewGroup, false)); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) { + viewHolder.bind(position); + } + + @Override + public int getItemCount() { + return uploadableFiles.size(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.rl_container) + RelativeLayout rlContainer; + @BindView(R.id.iv_thumbnail) + SimpleDraweeView background; + @BindView(R.id.iv_error) + ImageView ivError; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + + /** + * Binds a row item to the ViewHolder + * @param position + */ + public void bind(int position) { + UploadableFile uploadableFile = uploadableFiles.get(position); + Uri uri = uploadableFile.getMediaUri(); + background.setImageURI(Uri.fromFile(new File(String.valueOf(uri)))); + + if (position == callback.getCurrentSelectedFilePosition()) { + rlContainer.setEnabled(true); + rlContainer.setClickable(true); + rlContainer.setAlpha(1.0f); + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + rlContainer.setElevation(10); + } + } else { + rlContainer.setEnabled(false); + rlContainer.setClickable(false); + rlContainer.setAlpha(0.5f); + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + rlContainer.setElevation(0); + } + } + } + } + + /** + * Callback used to get the current selected file position + */ + interface Callback { + + int getCurrentSelectedFilePosition(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Title.java b/app/src/main/java/fr/free/nrw/commons/upload/Title.java index bc2d55640..380b2c1de 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/Title.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/Title.java @@ -31,4 +31,8 @@ public class Title{ public boolean isEmpty() { return titleText==null || titleText.isEmpty(); } + + public String getTitleText() { + return titleText; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java index 347d6e71c..8881151e1 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java @@ -1,162 +1,115 @@ package fr.free.nrw.commons.upload; +import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS; +import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES; +import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; + import android.Manifest; import android.annotation.SuppressLint; import android.app.ProgressDialog; import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.Bundle; -import com.google.android.material.textfield.TextInputLayout; import androidx.appcompat.app.AlertDialog; import androidx.cardview.widget.CardView; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.fragment.app.FragmentStatePagerAdapter; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import android.text.TextUtils; -import android.text.method.LinkMovementMethod; -import android.text.style.ClickableSpan; -import android.text.style.URLSpan; -import android.view.MotionEvent; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + 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.FrameLayout; -import android.widget.ImageView; -import android.widget.ProgressBar; +import android.widget.ImageButton; +import android.widget.LinearLayout; import android.widget.RelativeLayout; -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.LinkedList; import java.util.List; -import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Named; import butterknife.BindView; import butterknife.ButterKnife; +import butterknife.OnClick; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.SessionManager; 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.contributions.ContributionController; import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.ui.widget.HtmlTextView; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.NetworkUtils; +import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment; +import fr.free.nrw.commons.upload.license.MediaLicenseFragment; +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment; +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback; import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; +import io.reactivex.disposables.CompositeDisposable; +import java.util.Collections; import timber.log.Timber; -import static fr.free.nrw.commons.contributions.Contribution.SOURCE_EXTERNAL; -import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS; -import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES; -import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; - -public class UploadActivity extends BaseActivity implements UploadView, SimilarImageInterface { - @Inject MediaWikiApi mwApi; +public class UploadActivity extends BaseActivity implements UploadContract.View ,UploadBaseFragment.Callback{ @Inject ContributionController contributionController; @Inject @Named("default_preferences") JsonKvStore directKvStore; - @Inject UploadPresenter presenter; + @Inject UploadContract.UserActionListener presenter; @Inject CategoriesModel categoriesModel; @Inject SessionManager sessionManager; - // Main GUI - @BindView(R.id.backgroundImage) PhotoView background; - @BindView(R.id.upload_root_layout) - RelativeLayout rootLayout; - @BindView(R.id.view_flipper) ViewFlipper viewFlipper; + @BindView(R.id.cv_container_top_card) + CardView cvContainerTopCard; - // 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; + @BindView(R.id.ll_container_top_card) + LinearLayout llContainerTopCard; - // 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; - @BindView(R.id.prev_title_desc) Button prevTitleDecs; - @BindView(R.id.categories_subtitle) TextView categoriesSubtitle; - @BindView(R.id.license_subtitle) TextView licenseSubtitle; - @BindView(R.id.please_wait_text_view) TextView pleaseWaitTextView; + @BindView(R.id.rl_container_title) + RelativeLayout rlContainerTitle; + @BindView(R.id.tv_top_card_title) + TextView tvTopCardTitle; - @BindView(R.id.right_card_map_button) View rightCardMapButton; + @BindView(R.id.ib_toggle_top_card) + ImageButton ibToggleTopCard; - // 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; - @BindView(R.id.category_search_layout) - FrameLayout categoryFrameLayout; + @BindView(R.id.rv_thumbnails) + RecyclerView rvThumbnails; - // Final Submission - @BindView(R.id.license_title) TextView licenseTitle; - @BindView(R.id.share_license_summary) HtmlTextView licenseSummary; - @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; + @BindView(R.id.vp_upload) + ViewPager vpUpload; - private DescriptionsAdapter descriptionsAdapter; - private RVRendererAdapter categoriesAdapter; + private boolean isTitleExpanded=true; + + private CompositeDisposable compositeDisposable; private ProgressDialog progressDialog; - private boolean multipleUpload = false, flagForSubmit = false; + private UploadImageAdapter uploadImagesAdapter; + private List fragments; + private UploadCategoriesFragment uploadCategoriesFragment; + private MediaLicenseFragment mediaLicenseFragment; + private ThumbnailsAdapter thumbnailsAdapter; + private String source; + private Place place; + private List uploadableFiles= Collections.emptyList(); + private int currentSelectedPosition=0; + @SuppressLint("CheckResult") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_upload); + ButterKnife.bind(this); - - configureLayout(); - configureTopCard(); - configureBottomCard(); - initRecyclerView(); - configureRightCard(); - configureNavigationButtons(); - configureCategories(); - configureLicenses(); - - presenter.init(); + compositeDisposable = new CompositeDisposable(); + init(); PermissionUtils.checkPermissionsAndPerformAction(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, @@ -165,283 +118,150 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI R.string.write_storage_permission_rationale_for_image_share); } - @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; + private void init() { + initProgressDialog(); + initViewPager(); + initThumbnailsRecyclerView(); + //And init other things you need to + } + + private void initProgressDialog() { + progressDialog = new ProgressDialog(this); + progressDialog.setMessage(getString(R.string.please_wait)); + } + + private void initThumbnailsRecyclerView() { + rvThumbnails.setLayoutManager(new LinearLayoutManager(this, + LinearLayoutManager.HORIZONTAL, false)); + thumbnailsAdapter=new ThumbnailsAdapter(() -> currentSelectedPosition); + rvThumbnails.setAdapter(thumbnailsAdapter); + + } + + private void initViewPager() { + uploadImagesAdapter=new UploadImageAdapter(getSupportFragmentManager()); + vpUpload.setAdapter(uploadImagesAdapter); + vpUpload.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, + int positionOffsetPixels) { + + } + + @Override + public void onPageSelected(int position) { + currentSelectedPosition=position; + if (position >= uploadableFiles.size()) { + cvContainerTopCard.setVisibility(View.GONE); + } else { + thumbnailsAdapter.notifyDataSetChanged(); + cvContainerTopCard.setVisibility(View.VISIBLE); + } + + } + + @Override + public void onPageScrollStateChanged(int state) { + + } + }); } @Override - protected void onDestroy() { - presenter.cleanup(); - super.onDestroy(); + public boolean isLoggedIn() { + return sessionManager.isUserLoggedIn(); } @Override protected void onResume() { super.onResume(); - checkIfLoggedIn(); - + presenter.onAttachView(this); + if (!isLoggedIn()) { + askUserToLogIn(); + } checkStoragePermissions(); - 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) - ); } private void checkStoragePermissions() { PermissionUtils.checkPermissionsAndPerformAction(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, - () -> presenter.addView(this), + () -> { + //TODO handle this + }, R.string.storage_permission_title, R.string.write_storage_permission_rationale_for_image_share); } - @Override - protected void onPause() { - presenter.removeView(); - super.onPause(); - } @Override - public void updateThumbnails(List 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) { - boolean saveForPrevImage = false; - int singleUploadStepCount = 3; - - 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 (currentStep == stepCount) { - dismissKeyboard(); - } - if (stepCount > singleUploadStepCount) { - multipleUpload = true; - } - if (multipleUpload && currentStep != 1) { - saveForPrevImage = true; - } - configurePrevButton(saveForPrevImage); - if(isShowingItem) { - descriptionsAdapter.setItems(uploadItem.getTitle(), uploadItem.getDescriptions()); - rvDescriptions.setAdapter(descriptionsAdapter); - } - } - - @Override - public void updateLicenses(List licenses, String selectedLicense) { - ArrayAdapter 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, int imageCount) { - String licenseHyperLink = "" + - getString(Utils.licenseNameFor(selectedLicense)) + "
"; - licenseSummary.setHtmlText(getResources().getQuantityString(R.plurals.share_license_summary, imageCount, 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) { - rightCardMapButton.setVisibility(visible ? View.VISIBLE : View.GONE); - } - - @Override - public void setBottomCardVisibility(@UploadPage int page, int uploadCount) { - 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); - pleaseWaitTextView.setText(getResources().getQuantityText(R.plurals.receiving_shared_content, uploadCount)); - } + protected void onStop() { + super.onStop(); } /** - * Only show the subtitle ("For all images in set") if multiple images being uploaded - * @param imageCount Number of images being uploaded + * Show/Hide the progress dialog */ @Override - public void updateSubtitleVisibility(int imageCount) { - categoriesSubtitle.setVisibility(imageCount > 1 ? View.VISIBLE : View.GONE); - licenseSubtitle.setVisibility(imageCount > 1 ? View.VISIBLE : View.GONE); - } - - @Override - public void setBottomCardState(boolean state) { - updateCardState(state, bottomCardExpandButton, rvDescriptions, previous, next, prevTitleDecs, bottomCardAddDescription); - } - - - @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); + public void showProgress(boolean shouldShow) { + if (shouldShow) { + if (!progressDialog.isShowing()) { + progressDialog.show(); + } + } else { + if (progressDialog != null && !isFinishing()) { + progressDialog.dismiss(); + } } } @Override - public void showBadPicturePopup(String errorMessage) { - DialogUtil.showAlertDialog(this, - getString(R.string.warning), - errorMessage, - () -> presenter.deletePicture(), - () -> presenter.keepPicture()); + public int getIndexInViewFlipper(UploadBaseFragment fragment) { + return fragments.indexOf(fragment); } @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)); + public int getTotalNumberOfSteps() { + return fragments.size(); } @Override - public void showProgressDialog() { - if (progressDialog == null) { - progressDialog = new ProgressDialog(this); - } - progressDialog.setMessage(getString(R.string.please_wait)); - progressDialog.show(); + public void showMessage(int messageResourceId) { + ViewUtil.showLongToast(this, messageResourceId); } @Override - public void hideProgressDialog() { - if (progressDialog != null && !isFinishing()) { - progressDialog.dismiss(); - } + public List getUploadableFiles() { + return uploadableFiles; } @Override - public void launchMapActivity(LatLng decCoords) { - Utils.handleGeoCoordinates(this, decCoords); + public void showHideTopCard(boolean shouldShow) { + llContainerTopCard.setVisibility(shouldShow?View.VISIBLE:View.GONE); } @Override - public void showErrorMessage(int resourceId) { - ViewUtil.showShortToast(this, resourceId); + public void onUploadMediaDeleted(int index) { + fragments.remove(index);//Remove the corresponding fragment + uploadableFiles.remove(index);//Remove the files from the list + thumbnailsAdapter.notifyItemRemoved(index); //Notify the thumbnails adapter + uploadImagesAdapter.notifyDataSetChanged(); //Notify the ViewPager } @Override - public void initDefaultCategories() { - updateCategoryList(""); + public void updateTopCardTitle() { + tvTopCardTitle.setText(getResources() + .getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size())); } + @Override + public void askUserToLogIn() { + Timber.d("current session is null, asking user to login"); + ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in)); + Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class); + startActivity(loginIntent); + } + + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); @@ -450,179 +270,6 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI } } - 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() { - 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() { - boolean flagVal = directKvStore.getBoolean("flagForSubmit"); - if(flagVal){ - prevTitleDecs.setVisibility(View.VISIBLE); - } - else { - prevTitleDecs.setVisibility(View.INVISIBLE); - } - bottomCardExpandButton.setOnClickListener(v -> presenter.toggleBottomCardState()); - bottomCard.setOnClickListener(v -> presenter.toggleBottomCardState()); - bottomCardAddDescription.setOnClickListener(v -> addNewDescription()); - } - - private void addNewDescription() { - descriptionsAdapter.addDescription(new Description()); - rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1); - } - - private void configureRightCard() { - rightCardMapButton.setOnClickListener(v -> presenter.openCoordinateMap()); - } - - @SuppressLint("ClickableViewAccessibility") - public void configurePrevButton(Boolean saveForPrevImage){ - prevTitleDecs.setCompoundDrawablesWithIntrinsicBounds(null, null, getResources().getDrawable(R.drawable.mapbox_info_icon_default), null); - - String name = "prev_"; - if (saveForPrevImage) { - name = name + "image_"; - } else { - name = name + "upload_"; - } - String title = directKvStore.getString(name + "title"); - Title t = new Title(); - t.setTitleText(title); - - List finalDesc = new LinkedList<>(); - int descCount = directKvStore.getInt(name + "descCount"); - for (int i = 0; i < descCount; i++) { - Description description= new Description(); - String desc = directKvStore.getString(name + "description_<" + i + ">"); - description.setDescriptionText(desc); - finalDesc.add(description); - int position = directKvStore.getInt(name + "spinnerPosition_<" + i + ">"); - description.setSelectedLanguageIndex(position); - } - prevTitleDecs.setOnTouchListener((v, event) -> { - // Check this is a touch up event - if(event.getAction() != MotionEvent.ACTION_UP) return false; - // Check we are tapping within 15px of the info icon - int extraTapArea = 15; - Drawable info = prevTitleDecs.getCompoundDrawables()[2]; - int infoHintbox = prevTitleDecs.getWidth() - info.getBounds().width(); - if (event.getX() + extraTapArea < infoHintbox) return false; - - DialogUtil.showAlertDialog(this, null, getString(R.string.previous_button_tooltip_message), "okay", null, null, null); - - return true; - }); - prevTitleDecs.setOnClickListener((View v) -> { - descriptionsAdapter.setItems(t, finalDesc); - rvDescriptions.setAdapter(descriptionsAdapter); - }); - } - - private void configureNavigationButtons() { - // Navigation next / previous for each image as we're collecting title + description - next.setOnClickListener(v -> { - if (!NetworkUtils.isInternetConnectionEstablished(this)) { - ViewUtil.showShortSnackbar(rootLayout, R.string.no_internet); - return; - } - setTitleAndDescriptions(); - if (multipleUpload) { - savePrevTitleDesc("prev_image_"); - } - 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 -> { - flagForSubmit = true; - directKvStore.putBoolean("flagForSubmit", flagForSubmit); - savePrevTitleDesc("prev_upload_"); - Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG).show(); - presenter.handleSubmit(categoriesModel); - finish(); - }); - - } - - private void setTitleAndDescriptions() { - List descriptions = descriptionsAdapter.getDescriptions(); - Timber.d("Descriptions size is %d are %s", descriptions.size(), descriptions); - } - - private void configureCategories() { - categoryFrameLayout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); - categoriesAdapter = new UploadCategoriesAdapterFactory(categoriesModel).create(new ArrayList<>()); - categoriesList.setLayoutManager(new LinearLayoutManager(this)); - categoriesList.setAdapter(categoriesAdapter); - } - - @SuppressLint("CheckResult") - private void updateCategoryList(String filter) { - List imageTitleList = presenter.getImageTitleList(); - compositeDisposable.add(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 action = intent.getAction(); @@ -631,21 +278,79 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI } else if (ACTION_INTERNAL_UPLOADS.equals(action)) { receiveInternalSharedItems(); } + + if (uploadableFiles == null || uploadableFiles.isEmpty()) { + handleNullMedia(); + } else { + //Show thumbnails + if (uploadableFiles.size() + > 1) {//If there is only file, no need to show the image thumbnails + thumbnailsAdapter.setUploadableFiles(uploadableFiles); + } else { + llContainerTopCard.setVisibility(View.GONE); + } + tvTopCardTitle.setText(getResources() + .getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(),uploadableFiles.size())); + + fragments = new ArrayList<>(); + for (UploadableFile uploadableFile : uploadableFiles) { + UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment(); + uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, source, place); + uploadMediaDetailFragment.setCallback(new UploadMediaDetailFragmentCallback(){ + @Override + public void deletePictureAtIndex(int index) { + presenter.deletePictureAtIndex(index); + } + + @Override + public void onNextButtonClicked(int index) { + UploadActivity.this.onNextButtonClicked(index); + } + + @Override + public void onPreviousButtonClicked(int index) { + UploadActivity.this.onPreviousButtonClicked(index); + } + + @Override + public void showProgress(boolean shouldShow) { + UploadActivity.this.showProgress(shouldShow); + } + + @Override + public int getIndexInViewFlipper(UploadBaseFragment fragment) { + return fragments.indexOf(fragment); + } + + @Override + public int getTotalNumberOfSteps() { + return fragments.size(); + } + }); + fragments.add(uploadMediaDetailFragment); + } + + uploadCategoriesFragment = new UploadCategoriesFragment(); + uploadCategoriesFragment.setCallback(this); + + mediaLicenseFragment = new MediaLicenseFragment(); + mediaLicenseFragment.setCallback(this); + + + fragments.add(uploadCategoriesFragment); + fragments.add(mediaLicenseFragment); + + uploadImagesAdapter.setFragments(fragments); + vpUpload.setOffscreenPageLimit(fragments.size()); + } } private void receiveExternalSharedItems() { - List uploadableFiles = contributionController.handleExternalImagesPicked(this, getIntent()); - if (uploadableFiles.isEmpty()) { - handleNullMedia(); - return; - } - - presenter.receive(uploadableFiles, SOURCE_EXTERNAL, null); + uploadableFiles = contributionController.handleExternalImagesPicked(this, getIntent()); } private void receiveInternalSharedItems() { Intent intent = getIntent(); - String source; if (intent.hasExtra(UploadService.EXTRA_SOURCE)) { source = intent.getStringExtra(UploadService.EXTRA_SOURCE); @@ -658,17 +363,10 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI intent.getAction(), source); - ArrayList uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES); + uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES); Timber.i("Received multiple upload %s", uploadableFiles.size()); - if (uploadableFiles.isEmpty()) { - handleNullMedia(); - return; - } - - Place place = intent.getParcelableExtra(PLACE_OBJECT); - presenter.receive(uploadableFiles, source, place); - + place = intent.getParcelableExtra(PLACE_OBJECT); resetDirectPrefs(); } @@ -685,39 +383,6 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI finish(); } - /** - * Rotates the button and shows or hides the content based on the given state. Typically used - * for collapsing or expanding {@link CardView} animation. - * - * @param state the expanded state of the View whose elements are to be updated. True if - * expanded. - * @param button the image to rotate. Typically an arrow points up when the CardView is - * collapsed and down when it is expanded. - * @param content the Views that should be shown or hidden based on the state. - */ - private void updateCardState(boolean state, ImageView button, View... content) { - button.animate().rotation(state ? 180 : 0).start(); - if (content != null) { - for (View view : content) { - view.setVisibility(state ? View.VISIBLE : View.GONE); - } - } - } - - @Override - public List 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) @@ -729,23 +394,66 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI } @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"); - } - - public void savePrevTitleDesc(String name){ - - directKvStore.putString(name + "title", descriptionsAdapter.getTitle().toString()); - int n = descriptionsAdapter.getItemCount() - 1; - directKvStore.putInt(name + "descCount", n); - for (int i = 0; i < n; i++) { - directKvStore.putString(name + "description_<" + i + ">", descriptionsAdapter.getDescriptions().get(i).getDescriptionText()); - directKvStore.putInt(name + "spinnerPosition_<" + i + ">", descriptionsAdapter.getDescriptions().get(i).getSelectedLanguageIndex()); + public void onNextButtonClicked(int index) { + if (index < fragments.size()-1) { + vpUpload.setCurrentItem(index + 1, false); + } else { + presenter.handleSubmit(); } } + + @Override + public void onPreviousButtonClicked(int index) { + if (index != 0) { + vpUpload.setCurrentItem(index - 1, true); + } + } + + /** + * The adapter used to show image upload intermediate fragments + */ + + private class UploadImageAdapter extends FragmentStatePagerAdapter { + List fragments; + + public UploadImageAdapter(FragmentManager fragmentManager) { + super(fragmentManager); + this.fragments = new ArrayList<>(); + } + + public void setFragments(List fragments) { + this.fragments = fragments; + notifyDataSetChanged(); + } + + @Override public Fragment getItem(int position) { + return fragments.get(position); + } + + @Override public int getCount() { + return fragments.size(); + } + + @Override + public int getItemPosition(Object object){ + return PagerAdapter.POSITION_NONE; + } + } + + + @OnClick(R.id.rl_container_title) + public void onRlContainerTitleClicked(){ + rvThumbnails.setVisibility(isTitleExpanded ? View.GONE : View.VISIBLE); + isTitleExpanded = !isTitleExpanded; + ibToggleTopCard.setRotation(ibToggleTopCard.getRotation() + 180); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + presenter.onDetachView(); + compositeDisposable.clear(); + mediaLicenseFragment.setCallback(null); + uploadCategoriesFragment.setCallback(null); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.java new file mode 100644 index 000000000..afd1a694b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.java @@ -0,0 +1,41 @@ +package fr.free.nrw.commons.upload; + +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment; + +/** + * The base fragment of the fragments in upload + */ +public class UploadBaseFragment extends CommonsDaggerSupportFragment { + + public Callback callback; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + public void setCallback(Callback callback) { + this.callback = callback; + } + + public interface Callback { + + void onNextButtonClicked(int index); + + void onPreviousButtonClicked(int index); + + void showProgress(boolean shouldShow); + + int getIndexInViewFlipper(UploadBaseFragment fragment); + + int getTotalNumberOfSteps(); + + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.java new file mode 100644 index 000000000..f90496da0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.java @@ -0,0 +1,40 @@ +package fr.free.nrw.commons.upload; + +import fr.free.nrw.commons.BasePresenter; +import fr.free.nrw.commons.filepicker.UploadableFile; + +import java.util.List; + +/** + * The contract using which the UplaodActivity would communicate with its presenter + */ +public interface UploadContract { + + public interface View { + + boolean isLoggedIn(); + + void finish(); + + void askUserToLogIn(); + + void showProgress(boolean shouldShow); + + void showMessage(int messageResourceId); + + List getUploadableFiles(); + + void showHideTopCard(boolean shouldShow); + + void onUploadMediaDeleted(int index); + + void updateTopCardTitle(); + } + + public interface UserActionListener extends BasePresenter { + + void handleSubmit(); + + void deletePictureAtIndex(int index); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index 34603f0b8..1a602a001 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -75,7 +75,7 @@ public class UploadController { /** * Prepares the upload service. */ - void prepareService() { + public void prepareService() { Intent uploadServiceIntent = new Intent(context, UploadService.class); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); context.startService(uploadServiceIntent); @@ -85,7 +85,7 @@ public class UploadController { /** * Disconnects the upload service. */ - void cleanup() { + public void cleanup() { if (isUploadServiceConnected) { context.unbindService(uploadServiceConnection); } @@ -96,7 +96,7 @@ public class UploadController { * * @param contribution the contribution object */ - void startUpload(Contribution contribution) { + public void startUpload(Contribution contribution) { startUpload(contribution, c -> {}); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java index c94f9f496..65f12354b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java @@ -3,16 +3,7 @@ package fr.free.nrw.commons.upload; import android.annotation.SuppressLint; import android.content.Context; import android.net.Uri; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import javax.inject.Inject; -import javax.inject.Named; - +import androidx.annotation.Nullable; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.SessionManager; @@ -25,14 +16,20 @@ 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.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.functions.Consumer; -import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.BehaviorSubject; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; import timber.log.Timber; - +@Singleton public class UploadModel { private static UploadItem DUMMY = new UploadItem( @@ -49,24 +46,22 @@ public class UploadModel { private String license; private final Map licensesByName; private List items = new ArrayList<>(); - private boolean topCardState = true; - private boolean bottomCardState = true; - private boolean rightCardState = true; private int currentStepIndex = 0; private CompositeDisposable compositeDisposable = new CompositeDisposable(); private SessionManager sessionManager; private FileProcessor fileProcessor; private final ImageProcessingService imageProcessingService; + private List selectedCategories; @Inject UploadModel(@Named("licenses") List licenses, - @Named("default_preferences") JsonKvStore store, - @Named("licenses_by_name") Map licensesByName, - Context context, - SessionManager sessionManager, - FileProcessor fileProcessor, - ImageProcessingService imageProcessingService) { + @Named("default_preferences") JsonKvStore store, + @Named("licenses_by_name") Map licensesByName, + Context context, + SessionManager sessionManager, + FileProcessor fileProcessor, + ImageProcessingService imageProcessingService) { this.licenses = licenses; this.store = store; this.license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); @@ -77,31 +72,61 @@ public class UploadModel { this.imageProcessingService = imageProcessingService; } - void cleanup() { + /** + * cleanup the resources, I am Singleton, preparing for fresh upload + */ + public void cleanUp() { compositeDisposable.clear(); fileProcessor.cleanup(); + this.items.clear(); + if (this.selectedCategories != null) { + this.selectedCategories.clear(); + } } + public void setSelectedCategories(List selectedCategories) { + if (null == selectedCategories) { + selectedCategories = new ArrayList<>(); + } + this.selectedCategories = selectedCategories; + } + + /** + * pre process a list of items + */ @SuppressLint("CheckResult") Observable preProcessImages(List uploadableFiles, - Place place, - String source, - SimilarImageInterface similarImageInterface) { - initDefaultValues(); + Place place, + String source, + SimilarImageInterface similarImageInterface) { return Observable.fromIterable(uploadableFiles) - .map(uploadableFile -> getUploadItem(uploadableFile, place, source, similarImageInterface)); + .map(uploadableFile -> getUploadItem(uploadableFile, place, source, + similarImageInterface)); } - Single getImageQuality(UploadItem uploadItem, boolean checkTitle) { + + /** + * pre process a one item at a time + */ + public Observable preProcessImage(UploadableFile uploadableFile, + Place place, + String source, + SimilarImageInterface similarImageInterface) { + return Observable.just(getUploadItem(uploadableFile, place, source, similarImageInterface)); + } + + public Single getImageQuality(UploadItem uploadItem, boolean checkTitle) { return imageProcessingService.validateImage(uploadItem, checkTitle); } private UploadItem getUploadItem(UploadableFile uploadableFile, - Place place, - String source, - SimilarImageInterface similarImageInterface) { - fileProcessor.initFileDetails(Objects.requireNonNull(uploadableFile.getFilePath()), context.getContentResolver()); - UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile.getFileCreatedDate(context); + Place place, + String source, + SimilarImageInterface similarImageInterface) { + fileProcessor.initFileDetails(Objects.requireNonNull(uploadableFile.getFilePath()), + context.getContentResolver()); + UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile + .getFileCreatedDate(context); long fileCreatedDate = -1; String createdTimestampSource = ""; if (dateTimeWithSource != null) { @@ -109,52 +134,21 @@ public class UploadModel { createdTimestampSource = dateTimeWithSource.getSource(); } Timber.d("File created date is %d", fileCreatedDate); - GPSExtractor gpsExtractor = fileProcessor.processFileCoordinates(similarImageInterface, context); - return new UploadItem(uploadableFile.getContentUri(), Uri.parse(uploadableFile.getFilePath()), uploadableFile.getMimeType(context), source, gpsExtractor, place, fileCreatedDate, createdTimestampSource); - } - - void onItemsProcessed(Place place, List uploadItems) { - items = uploadItems; - if (items.isEmpty()) { - return; - } - - UploadItem uploadItem = items.get(0); - uploadItem.selected = true; - uploadItem.first = true; - + GPSExtractor gpsExtractor = fileProcessor + .processFileCoordinates(similarImageInterface, context); + UploadItem uploadItem = new UploadItem(uploadableFile.getContentUri(), + Uri.parse(uploadableFile.getFilePath()), + uploadableFile.getMimeType(context), source, gpsExtractor, place, fileCreatedDate, + createdTimestampSource); if (place != null) { - uploadItem.title.setTitleText(place.getName()); - uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription().equals("?")?"":place.getLongDescription()); - //TODO figure out if default descriptions in other languages exist + uploadItem.title.setTitleText(place.name); + uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription()); uploadItem.descriptions.get(0).setLanguageCode("en"); } - } - - private void initDefaultValues() { - currentStepIndex = 0; - topCardState = true; - bottomCardState = true; - rightCardState = true; - items = new ArrayList<>(); - } - - 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; + if (!items.contains(uploadItem)) { + items.add(uploadItem); } - return !hasError; + return uploadItem; } int getCurrentStep() { @@ -173,110 +167,20 @@ public class UploadModel { 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; - } - - @SuppressLint("CheckResult") - public void next() { - markCurrentUploadVisited(); - if (currentStepIndex < items.size() + 1) { - currentStepIndex++; - } - updateItemState(); - } - - void setCurrentTitleAndDescriptions(Title title, List 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 descriptions) { - if (currentStepIndex < items.size() && currentStepIndex >= 0) { - items.get(currentStepIndex).descriptions = descriptions; - } - } - - public void previous() { - cleanup(); - 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() { - Timber.d("Updating item state"); - 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() { - Timber.d("Marking current upload visited"); - if (currentStepIndex < items.size() && currentStepIndex >= 0) { - items.get(currentStepIndex).visited = true; - } - } - public List getLicenses() { return licenses; } - String getSelectedLicense() { + public String getSelectedLicense() { return license; } - void setSelectedLicense(String licenseName) { + public void setSelectedLicense(String licenseName) { this.license = licensesByName.get(licenseName); store.putString(Prefs.DEFAULT_LICENSE, license); } - Observable buildContributions(List categoryStringList) { + public Observable buildContributions() { return Observable.fromIterable(items).map(item -> { Contribution contribution = new Contribution(item.mediaUri, null, @@ -287,7 +191,10 @@ public class UploadModel { if (item.place != null) { contribution.setWikiDataEntityId(item.place.getWikiDataEntityId()); } - contribution.setCategories(categoryStringList); + if (null == selectedCategories) {//Just a fail safe, this should never be null + selectedCategories = new ArrayList<>(); + } + contribution.setCategories(selectedCategories); contribution.setTag("mimeType", item.mimeType); contribution.setSource(item.source); contribution.setContentProviderUri(item.mediaUri); @@ -304,21 +211,16 @@ public class UploadModel { }); } - void keepPicture() { - items.get(currentStepIndex).setImageQuality(ImageUtils.IMAGE_KEEP); - } - - void deletePicture() { - cleanup(); - updateItemState(); - } - - void subscribeBadPicture(Consumer consumer, boolean checkTitle) { - if (isShowingItem()) { - compositeDisposable.add(getImageQuality(getCurrentItem(), checkTitle) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(consumer, Timber::e)); + public void deletePicture(String filePath) { + Iterator iterator = items.iterator(); + while (iterator.hasNext()) { + if (iterator.next().mediaUri.toString().contains(filePath)) { + iterator.remove(); + break; + } + } + if (items.isEmpty()) { + cleanUp(); } } @@ -326,8 +228,15 @@ public class UploadModel { return items; } + public void updateUploadItem(int index, UploadItem uploadItem) { + UploadItem uploadItem1 = items.get(index); + uploadItem1.setDescriptions(uploadItem.descriptions); + uploadItem1.setTitle(uploadItem.title); + } + @SuppressWarnings("WeakerAccess") - static class UploadItem { + public static class UploadItem { + private final Uri originalContentUri; private final Uri mediaUri; private final String mimeType; @@ -347,10 +256,10 @@ public class UploadModel { @SuppressLint("CheckResult") UploadItem(Uri originalContentUri, - Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords, - Place place, - long createdTimestamp, - String createdTimestampSource) { + Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords, + Place place, + long createdTimestamp, + String createdTimestampSource) { this.originalContentUri = originalContentUri; this.createdTimestampSource = createdTimestampSource; title = new Title(); @@ -426,16 +335,40 @@ public class UploadModel { } public String getFileName() { - return Utils.fixExtension(title.toString(), getFileExt()); + return title + != null ? Utils.fixExtension(title.toString(), getFileExt()) : null; } public Place getPlace() { return place; } + public void setTitle(Title title) { + this.title = title; + } + + public void setDescriptions(List descriptions) { + this.descriptions = descriptions; + } + public Uri getContentUri() { return originalContentUri; } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof UploadItem)) { + return false; + } + return this.mediaUri.toString().contains(((UploadItem) (obj)).mediaUri.toString()); + + } + + //Travis is complaining :P + @Override + public int hashCode() { + return super.hashCode(); + } } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModule.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModule.java new file mode 100644 index 000000000..9e4f572a7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModule.java @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.upload; + +import dagger.Binds; +import dagger.Module; +import fr.free.nrw.commons.upload.categories.CategoriesContract; +import fr.free.nrw.commons.upload.categories.CategoriesPresenter; +import fr.free.nrw.commons.upload.license.MediaLicenseContract; +import fr.free.nrw.commons.upload.license.MediaLicensePresenter; +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract; +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter; + +/** + * The Dagger Module for upload related presenters and (some other objects maybe in future) + */ +@Module +public abstract class UploadModule { + + @Binds + public abstract UploadContract.UserActionListener bindHomePresenter(UploadPresenter + presenter); + + @Binds + public abstract CategoriesContract.UserActionListener bindsCategoriesPresenter(CategoriesPresenter + presenter); + + @Binds + public abstract MediaLicenseContract.UserActionListener bindsMediaLicensePresenter( + MediaLicensePresenter + presenter); + + @Binds + public abstract UploadMediaDetailsContract.UserActionListener bindsUploadMediaPresenter( + UploadMediaPresenter + presenter); + +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java index 5e0dd3231..a08a547a9 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java @@ -1,420 +1,126 @@ package fr.free.nrw.commons.upload; import android.annotation.SuppressLint; -import android.content.Context; -import android.text.TextUtils; + +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.category.CategoriesModel; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.filepicker.UploadableFile; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.settings.Prefs; -import fr.free.nrw.commons.utils.CustomProxy; -import fr.free.nrw.commons.utils.CustomProxy; -import fr.free.nrw.commons.utils.StringSortingUtils; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; +import fr.free.nrw.commons.repository.UploadRepository; +import io.reactivex.Observer; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import org.apache.commons.lang3.StringUtils; -import java.util.ArrayList; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; 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; -import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult; /** * The MVP pattern presenter of Upload GUI */ @Singleton -public class UploadPresenter { +public class UploadPresenter implements UploadContract.UserActionListener { - private static final UploadView DUMMY = - (UploadView) CustomProxy.newInstance(UploadView.class.getClassLoader(), - new Class[] { UploadView.class }); + private static final UploadContract.View DUMMY = (UploadContract.View) Proxy.newProxyInstance( + UploadContract.View.class.getClassLoader(), + new Class[]{UploadContract.View.class}, (proxy, method, methodArgs) -> null); + private final UploadRepository repository; + private UploadContract.View view = DUMMY; - private UploadView view = DUMMY; - - private static final SimilarImageInterface SIMILAR_IMAGE = - (SimilarImageInterface) CustomProxy.newInstance( - SimilarImageInterface.class.getClassLoader(), - new Class[] { SimilarImageInterface.class }); - private SimilarImageInterface similarImageInterface = SIMILAR_IMAGE; - - @UploadView.UploadPage - private int currentPage = UploadView.PLEASE_WAIT; - - private final UploadModel uploadModel; - private final UploadController uploadController; - private final Context context; - private final JsonKvStore directKvStore; - private CompositeDisposable compositeDisposable = new CompositeDisposable(); + private CompositeDisposable compositeDisposable; @Inject - UploadPresenter(UploadModel uploadModel, - UploadController uploadController, - Context context, - @Named("default_preferences") JsonKvStore directKvStore) { - this.uploadModel = uploadModel; - this.uploadController = uploadController; - this.context = context; - this.directKvStore = directKvStore; + UploadPresenter(UploadRepository uploadRepository) { + this.repository = uploadRepository; + compositeDisposable = new CompositeDisposable(); } - /** - * Passes the items received to {@link #uploadModel} and displays the items. - * - * @param media The Uri's of the media being uploaded. - * @param source File source from {@link Contribution.FileSource} - */ - @SuppressLint("CheckResult") - void receive(List media, - @Contribution.FileSource String source, - Place place) { - Observable uploadItemObservable = uploadModel - .preProcessImages(media, place, source, similarImageInterface); - - compositeDisposable.add(uploadItemObservable - .toList() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(uploadItems -> onImagesProcessed(uploadItems, place), - throwable -> Timber.e(throwable, "Error occurred in processing images"))); - } - - private void onImagesProcessed(List uploadItems, Place place) { - uploadModel.onItemsProcessed(place, uploadItems); - updateCards(); - updateLicenses(); - updateContent(); - uploadModel.subscribeBadPicture(this::handleBadImage, false); - } - - /** - * Sets the license to parameter and updates {@link UploadActivity} - * - * @param licenseName license name - */ - void selectLicense(String licenseName) { - uploadModel.setSelectedLicense(licenseName); - view.updateLicenseSummary(uploadModel.getSelectedLicense(), uploadModel.getCount()); - } - - //region Wizard step management - - /** - * Called by the next button in {@link UploadActivity} - */ - @SuppressLint("CheckResult") - void handleNext(Title title, - List descriptions) { - Timber.e("Inside handleNext"); - view.showProgressDialog(); - setTitleAndDescription(title, descriptions); - compositeDisposable.add(uploadModel.getImageQuality(uploadModel.getCurrentItem(), true) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(imageResult -> handleImage(title, descriptions, imageResult), - throwable -> Timber.e(throwable, "Error occurred while handling image"))); - } - - private void handleImage(Title title, List descriptions, Integer imageResult) { - view.hideProgressDialog(); - if (imageResult == IMAGE_KEEP || imageResult == IMAGE_OK) { - Timber.d("Set title and desc; Show next uploaded item"); - setTitleAndDescription(title, descriptions); - directKvStore.putBoolean("Picture_Has_Correct_Location", true); - nextUploadedItem(); - } else { - handleBadImage(imageResult); - } - } - - /** - * 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 handleBadImage(Integer errorCode) { - Timber.d("Handle bad picture with error code %d", errorCode); - if (errorCode >= 8) { // If location of image and nearby does not match, then set shared preferences to disable wikidata edits - directKvStore.putBoolean("Picture_Has_Correct_Location", false); - } - - switch (errorCode) { - case EMPTY_TITLE: - Timber.d("Title is empty. Showing toast"); - view.showErrorMessage(R.string.add_title_toast); - break; - case FILE_NAME_EXISTS: - Timber.d("Trying to show duplicate picture popup"); - view.showDuplicatePicturePopup(); - break; - default: - String errorMessageForResult = getErrorMessageForResult(context, errorCode); - if (TextUtils.isEmpty(errorMessageForResult)) { - return; - } - view.showBadPicturePopup(errorMessageForResult); - } - } - - private void nextUploadedItem() { - Timber.d("Trying to show next uploaded item"); - uploadModel.next(); - updateContent(); - uploadModel.subscribeBadPicture(this::handleBadImage, false); - view.dismissKeyboard(); - } - - private void setTitleAndDescription(Title title, List descriptions) { - Timber.d("setTitleAndDescription: Setting title and desc"); - uploadModel.setCurrentTitleAndDescriptions(title, descriptions); - } - - String getCurrentImageFileName() { - UploadItem currentItem = getCurrentItem(); - return currentItem.getFileName(); - } - - /** - * Called by the previous button in {@link UploadActivity} - */ - void handlePrevious() { - uploadModel.previous(); - updateContent(); - uploadModel.subscribeBadPicture(this::handleBadImage, false); - 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()) - compositeDisposable.add(uploadModel.buildContributions(categoriesModel.getCategoryStringList()) + @Override + public void handleSubmit() { + if (view.isLoggedIn()) { + view.showProgress(true); + repository.buildContributions() .observeOn(Schedulers.io()) - .subscribe(uploadController::startUpload)); - } + .subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + view.showProgress(false); + view.showMessage(R.string.uploading_started); + compositeDisposable.add(d); + } - /** - * Called by the map button on the right card in {@link UploadActivity} - */ - void openCoordinateMap() { - GPSExtractor gpsObj = uploadModel.getCurrentItem().getGpsCoords(); - if (gpsObj != null && gpsObj.imageCoordsExists) { - view.launchMapActivity(new LatLng(gpsObj.getDecLatitude(), gpsObj.getDecLongitude(), 0.0f)); - } - } + @Override + public void onNext(Contribution contribution) { + repository.startUpload(contribution); + } - void keepPicture() { - uploadModel.keepPicture(); - } + @Override + public void onError(Throwable e) { + view.showMessage(R.string.upload_failed); + repository.cleanup(); + view.finish(); + compositeDisposable.clear(); + Timber.e("failed to upload: " + e.getMessage()); + } - void deletePicture() { - if (uploadModel.getCount() == 1) - view.finish(); - else { - uploadModel.deletePicture(); - updateCards(); - updateContent(); - uploadModel.subscribeBadPicture(this::handleBadImage, false); - 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()); - } - - /** - * Sets all the cards' states to closed. - */ - void closeAllCards() { - if (uploadModel.isTopCardState()) { - uploadModel.setTopCardState(false); - view.setTopCardState(false); - } - if (uploadModel.isRightCardState()) { - uploadModel.setRightCardState(false); - } - if (uploadModel.isBottomCardState()) { - uploadModel.setBottomCardState(false); - view.setBottomCardState(false); - } - } - //endregion - - //region View / Lifecycle management - public void init() { - uploadController.prepareService(); - } - - void cleanup() { - compositeDisposable.clear(); - uploadModel.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 = directKvStore.getString(Prefs.DEFAULT_LICENSE, - Prefs.Licenses.CC_BY_SA_4);//CC_BY_SA_4 is the default one used by the commons web app - try {//I have to make sure that the stored default license was not one of the deprecated one's - Utils.licenseNameFor(selectedLicense); - } catch (IllegalStateException exception) { - Timber.e(exception.getMessage()); - selectedLicense = Prefs.Licenses.CC_BY_SA_4; - directKvStore.putString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_4); - } - view.updateLicenses(uploadModel.getLicenses(), selectedLicense); - view.updateLicenseSummary(selectedLicense, uploadModel.getCount()); - } - - /** - * 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().getMediaUri()); - - view.updateBottomCardContent(uploadModel.getCurrentStep(), - uploadModel.getStepCount(), - uploadModel.getCurrentItem(), - uploadModel.isShowingItem()); - - view.updateTopCardContent(); - - GPSExtractor gpsObj = uploadModel.getCurrentItem().getGpsCoords(); - view.updateRightCardContent(gpsObj != null && gpsObj.imageCoordsExists); - - view.updateSubtitleVisibility(uploadModel.getCount()); - - 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(); + @Override + public void onComplete() { + repository.cleanup(); + view.finish(); + compositeDisposable.clear(); + } + }); } else { - currentPage = UploadView.LICENSE; - view.setTopCardVisibility(false); - view.setRightCardVisibility(false); + view.askUserToLogIn(); } - view.setBottomCardVisibility(currentPage, uploadCount); } - //endregion + @Override + public void deletePictureAtIndex(int index) { + List uploadableFiles = view.getUploadableFiles(); + if (index == uploadableFiles.size() - 1) {//If the next fragment to be shown is not one of the MediaDetailsFragment, lets hide the top card + view.showHideTopCard(false); + } + //Ask the repository to delete the picture + repository.deletePicture(uploadableFiles.get(index).getFilePath()); + if (uploadableFiles.size() == 1) { + view.showMessage(R.string.upload_cancelled); + view.finish(); + return; + } else { + view.onUploadMediaDeleted(index); + } + if (uploadableFiles.size() < 2) { + view.showHideTopCard(false); + } + + //In case lets update the number of uploadable media + view.updateTopCardTitle(); - /** - * @return the item currently being displayed - */ - private UploadItem getCurrentItem() { - return uploadModel.getCurrentItem(); } - List getImageTitleList() { - List titleList = new ArrayList<>(); - for (UploadItem item : uploadModel.getUploads()) { - if (item.getTitle().isSet()) { - titleList.add(item.getTitle().toString()); - } - } - return titleList; + @Override + public void onAttachView(UploadContract.View view) { + this.view = view; + repository.prepareService(); + } + + @Override + public void onDetachView() { + this.view = DUMMY; + compositeDisposable.clear(); + repository.cleanup(); } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java deleted file mode 100644 index afcc42aed..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java +++ /dev/null @@ -1,53 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.net.Uri; -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 java.io.File; - -import fr.free.nrw.commons.R; - -class UploadThumbnailRenderer extends Renderer { - 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(); - Uri uri = Uri.parse(content.getMediaUri().toString()); - background.setImageURI(Uri.fromFile(new File(String.valueOf(uri)))); - background.setAlpha(content.isSelected() ? 1.0f : 0.5f); - space.setVisibility(content.isFirst() ? View.VISIBLE : View.GONE); - error.setVisibility(content.isVisited() && content.isError() ? View.VISIBLE : View.GONE); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java index 3ea4dfa62..e69de29bb 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java @@ -1,24 +0,0 @@ -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; - -public class UploadThumbnailsAdapterFactory { - private ThumbnailClickedListener listener; - - UploadThumbnailsAdapterFactory(ThumbnailClickedListener listener) { - this.listener = listener; - } - - public RVRendererAdapter create(List placeList) { - RendererBuilder builder = new RendererBuilder() - .bind(UploadModel.UploadItem.class, new UploadThumbnailRenderer(listener)); - ListAdapteeCollection collection = new ListAdapteeCollection<>( - placeList != null ? placeList : Collections.emptyList()); - return new RVRendererAdapter<>(builder, collection); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java index 9fb50c7ca..ec1854ffc 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java @@ -15,7 +15,6 @@ public interface UploadView { // UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(), // new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null); - List getDescriptions(); @Retention(SOURCE) @IntDef({PLEASE_WAIT, TITLE_CARD, CATEGORIES, LICENSE}) @@ -82,4 +81,6 @@ public interface UploadView { void showProgressDialog(); void hideProgressDialog(); + + void askUserToLogIn(); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.java b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.java new file mode 100644 index 000000000..6ff51632a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.java @@ -0,0 +1,42 @@ +package fr.free.nrw.commons.upload.categories; + +import fr.free.nrw.commons.BasePresenter; +import fr.free.nrw.commons.category.CategoryItem; + +import java.util.List; + +/** + * The contract with with UploadCategoriesFragment and its presenter would talk to each other + */ +public interface CategoriesContract { + + public interface View { + + void showProgress(boolean shouldShow); + + void showError(String error); + + void showError(int stringResourceId); + + void setCategories(List categories); + + void addCategory(CategoryItem category); + + void goToNextScreen(); + + void showNoCategorySelected(); + + void setSelectedCategories(List selectedCategories); + } + + public interface UserActionListener extends BasePresenter { + + void searchForCategories(String query); + + void verifyCategories(); + + void onCategoryItemClicked(CategoryItem categoryItem); + } + + +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.java new file mode 100644 index 000000000..a0a776246 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.java @@ -0,0 +1,144 @@ +package fr.free.nrw.commons.upload.categories; + +import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; +import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD; + +import android.text.TextUtils; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.category.CategoryItem; +import fr.free.nrw.commons.repository.UploadRepository; +import fr.free.nrw.commons.upload.UploadModel.UploadItem; +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import timber.log.Timber; + +/** + * The presenter class for UploadCategoriesFragment + */ +@Singleton +public class CategoriesPresenter implements CategoriesContract.UserActionListener { + + private static final CategoriesContract.View DUMMY = (CategoriesContract.View) Proxy + .newProxyInstance( + CategoriesContract.View.class.getClassLoader(), + new Class[]{CategoriesContract.View.class}, + (proxy, method, methodArgs) -> null); + private final Scheduler ioScheduler; + private final Scheduler mainThreadScheduler; + + CategoriesContract.View view = DUMMY; + private UploadRepository repository; + + private CompositeDisposable compositeDisposable; + + @Inject + public CategoriesPresenter(UploadRepository repository, @Named(IO_THREAD) Scheduler ioScheduler, + @Named(MAIN_THREAD) Scheduler mainThreadScheduler) { + this.repository = repository; + this.ioScheduler = ioScheduler; + this.mainThreadScheduler = mainThreadScheduler; + compositeDisposable = new CompositeDisposable(); + } + + @Override + public void onAttachView(CategoriesContract.View view) { + this.view = view; + } + + @Override + public void onDetachView() { + this.view = DUMMY; + compositeDisposable.clear(); + } + + /** + * asks the repository to fetch categories for the query + * @param query + * + */ + @Override + public void searchForCategories(String query) { + List categoryItems = new ArrayList<>(); + List imageTitleList = getImageTitleList(); + Observable distinctCategoriesObservable = Observable + .fromIterable(repository.getSelectedCategories()) + .subscribeOn(ioScheduler) + .observeOn(mainThreadScheduler) + .doOnSubscribe(disposable -> { + view.showProgress(true); + view.showError(null); + view.setCategories(null); + }) + .observeOn(ioScheduler) + .concatWith( + repository.searchAll(query, imageTitleList) + ) + .filter(categoryItem -> !repository.containsYear(categoryItem.getName())) + .distinct(); + if(!TextUtils.isEmpty(query)) { + distinctCategoriesObservable=distinctCategoriesObservable.sorted(repository.sortBySimilarity(query)); + } + Disposable searchCategoriesDisposable = distinctCategoriesObservable + .observeOn(mainThreadScheduler) + .subscribe( + s -> categoryItems.add(s), + Timber::e, + () -> { + view.setCategories(categoryItems); + view.showProgress(false); + + if (categoryItems.isEmpty()) { + view.showError(R.string.no_categories_found); + } + } + ); + + compositeDisposable.add(searchCategoriesDisposable); + } + + /** + * Returns image title list from UploadItem + * @return + */ + private List getImageTitleList() { + List titleList = new ArrayList<>(); + for (UploadItem item : repository.getUploads()) { + if (item.getTitle().isSet()) { + titleList.add(item.getTitle().toString()); + } + } + return titleList; + } + + /** + * Verifies the number of categories selected, prompts the user if none selected + */ + @Override + public void verifyCategories() { + List selectedCategories = repository.getSelectedCategories(); + if (selectedCategories != null && !selectedCategories.isEmpty()) { + repository.setSelectedCategories(repository.getCategoryStringList()); + view.goToNextScreen(); + } else { + view.showNoCategorySelected(); + } + } + + /** + * ask repository to handle category clicked + * + * @param categoryItem + */ + @Override + public void onCategoryItemClicked(CategoryItem categoryItem) { + repository.onCategoryClicked(categoryItem); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java new file mode 100644 index 000000000..48485c17a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java @@ -0,0 +1,200 @@ +package fr.free.nrw.commons.upload.categories; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; +import com.jakewharton.rxbinding2.view.RxView; +import com.jakewharton.rxbinding2.widget.RxTextView; +import com.pedrogomez.renderers.RVRendererAdapter; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.category.CategoryClickedListener; +import fr.free.nrw.commons.category.CategoryItem; +import fr.free.nrw.commons.upload.UploadBaseFragment; +import fr.free.nrw.commons.upload.UploadCategoriesAdapterFactory; +import fr.free.nrw.commons.utils.DialogUtil; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.inject.Inject; +import timber.log.Timber; + +public class UploadCategoriesFragment extends UploadBaseFragment implements CategoriesContract.View, + CategoryClickedListener { + + @BindView(R.id.tv_title) + TextView tvTitle; + @BindView(R.id.til_container_search) + TextInputLayout tilContainerEtSearch; + @BindView(R.id.et_search) + TextInputEditText etSearch; + @BindView(R.id.pb_categories) + ProgressBar pbCategories; + @BindView(R.id.rv_categories) + RecyclerView rvCategories; + + @Inject + CategoriesContract.UserActionListener presenter; + private RVRendererAdapter adapter; + private List mediaTitleList=new ArrayList<>(); + private Disposable subscribe; + private List categories; + private boolean isVisible; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + public void setMediaTitleList(List mediaTitleList) { + this.mediaTitleList = mediaTitleList; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.upload_categories_fragment, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ButterKnife.bind(this, view); + init(); + } + + private void init() { + tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1, + callback.getTotalNumberOfSteps())); + presenter.onAttachView(this); + initRecyclerView(); + addTextChangeListenerToEtSearch(); + //get default categories for empty query + } + + @Override + public void onResume() { + super.onResume(); + if (presenter != null && isVisible && (categories == null || categories.isEmpty())) { + presenter.searchForCategories(null); + } + } + + private void addTextChangeListenerToEtSearch() { + subscribe = RxTextView.textChanges(etSearch) + .doOnEach(v -> tilContainerEtSearch.setError(null)) + .takeUntil(RxView.detaches(etSearch)) + .debounce(500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(filter -> searchForCategory(filter.toString()), Timber::e); + } + + private void searchForCategory(String query) { + presenter.searchForCategories(query); + } + + private void initRecyclerView() { + adapter = new UploadCategoriesAdapterFactory(this) + .create(new ArrayList<>()); + rvCategories.setLayoutManager(new LinearLayoutManager(getContext())); + rvCategories.setAdapter(adapter); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + presenter.onDetachView(); + subscribe.dispose(); + } + + @Override + public void showProgress(boolean shouldShow) { + pbCategories.setVisibility(shouldShow ? View.VISIBLE : View.GONE); + } + + @Override + public void showError(String error) { + tilContainerEtSearch.setError(error); + } + + @Override + public void showError(int stringResourceId) { + tilContainerEtSearch.setError(getString(stringResourceId)); + } + + @Override + public void setCategories(List categories) { + adapter.clear(); + if (categories != null) { + this.categories = categories; + adapter.addAll(categories); + adapter.notifyDataSetChanged(); + } + } + + @Override + public void addCategory(CategoryItem category) { + adapter.add(category); + adapter.notifyItemInserted(adapter.getItemCount()); + } + + @Override + public void goToNextScreen() { + callback.onNextButtonClicked(callback.getIndexInViewFlipper(this)); + } + + @Override + public void showNoCategorySelected() { + DialogUtil.showAlertDialog(getActivity(), + 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, + () -> goToNextScreen()); + } + + @Override + public void setSelectedCategories(List selectedCategories) { + + } + + @OnClick(R.id.btn_next) + public void onNextButtonClicked() { + presenter.verifyCategories(); + } + + @OnClick(R.id.btn_previous) + public void onPreviousButtonClicked() { + callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this)); + } + + @Override + public void categoryClicked(CategoryItem item) { + presenter.onCategoryItemClicked(item); + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + isVisible = isVisibleToUser; + + if (presenter != null && isResumed() && (categories == null || categories.isEmpty())) { + presenter.searchForCategories(null); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.java b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.java new file mode 100644 index 000000000..68e6affb4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.java @@ -0,0 +1,26 @@ +package fr.free.nrw.commons.upload.license; + +import fr.free.nrw.commons.BasePresenter; + +import java.util.List; + +/** + * The contract with with MediaLicenseFragment and its presenter would talk to each other + */ +public interface MediaLicenseContract { + + interface View { + void setLicenses(List licenses); + + void setSelectedLicense(String license); + + void updateLicenseSummary(String selectedLicense, int numberOfItems); + } + + interface UserActionListener extends BasePresenter { + void getLicenses(); + + void selectLicense(String licenseName); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java new file mode 100644 index 000000000..8836c9bdf --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java @@ -0,0 +1,181 @@ +package fr.free.nrw.commons.upload.license; + +import android.net.Uri; +import android.os.Bundle; +import android.text.Html; +import android.text.SpannableStringBuilder; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.text.style.URLSpan; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.upload.UploadBaseFragment; +import timber.log.Timber; + +public class MediaLicenseFragment extends UploadBaseFragment implements MediaLicenseContract.View { + + @BindView(R.id.tv_title) + TextView tvTitle; + @BindView(R.id.spinner_license_list) + Spinner spinnerLicenseList; + @BindView(R.id.tv_share_license_summary) + TextView tvShareLicenseSummary; + + @Inject + MediaLicenseContract.UserActionListener presenter; + + private ArrayAdapter adapter; + private List licenses; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_media_license, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ButterKnife.bind(this, view); + init(); + } + + private void init() { + tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1, + callback.getTotalNumberOfSteps())); + initPresenter(); + initLicenseSpinner(); + presenter.getLicenses(); + } + + private void initPresenter() { + presenter.onAttachView(this); + } + + /** + * Initialise the license spinner + */ + private void initLicenseSpinner() { + adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item); + spinnerLicenseList.setAdapter(adapter); + spinnerLicenseList.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int position, + long l) { + String licenseName = adapterView.getItemAtPosition(position).toString(); + presenter.selectLicense(licenseName); + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + presenter.selectLicense(null); + } + }); + } + + @Override + public void setLicenses(List licenses) { + adapter.clear(); + this.licenses = licenses; + adapter.addAll(this.licenses); + adapter.notifyDataSetChanged(); + } + + @Override + public void setSelectedLicense(String license) { + int position = licenses.indexOf(getString(Utils.licenseNameFor(license))); + // Check if position is valid + if (position < 0) { + Timber.d("Invalid position: %d. Using default licenses", position); + position = licenses.size() - 1; + } else { + Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license))); + } + spinnerLicenseList.setSelection(position); + } + + @Override + public void updateLicenseSummary(String licenseSummary, int numberOfItems) { + String licenseHyperLink = "" + + getString(Utils.licenseNameFor(licenseSummary)) + "
"; + + setTextViewHTML(tvShareLicenseSummary, getResources() + .getQuantityString(R.plurals.share_license_summary, numberOfItems, + licenseHyperLink)); + } + + private void setTextViewHTML(TextView textView, String text) { + CharSequence sequence = Html.fromHtml(text); + SpannableStringBuilder strBuilder = new SpannableStringBuilder(sequence); + URLSpan[] urls = strBuilder.getSpans(0, sequence.length(), URLSpan.class); + for (URLSpan span : urls) { + makeLinkClickable(strBuilder, span); + } + textView.setText(strBuilder); + textView.setMovementMethod(LinkMovementMethod.getInstance()); + } + + private void makeLinkClickable(SpannableStringBuilder strBuilder, final URLSpan span) { + int start = strBuilder.getSpanStart(span); + int end = strBuilder.getSpanEnd(span); + int flags = strBuilder.getSpanFlags(span); + ClickableSpan clickable = new ClickableSpan() { + public void onClick(View view) { + // Handle hyperlink click + String hyperLink = span.getURL(); + launchBrowser(hyperLink); + } + }; + strBuilder.setSpan(clickable, start, end, flags); + strBuilder.removeSpan(span); + } + + private void launchBrowser(String hyperLink) { + Utils.handleWebUrl(getContext(), Uri.parse(hyperLink)); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + presenter.onDetachView(); + //Free the adapter to avoid memory leaks + adapter=null; + } + + + @OnClick(R.id.btn_previous) + public void onPreviousButtonClicked() { + callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this)); + } + + @OnClick(R.id.btn_submit) + public void onSubmitButtonClicked() { + callback.onNextButtonClicked(callback.getIndexInViewFlipper(this)); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java new file mode 100644 index 000000000..881f21369 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java @@ -0,0 +1,75 @@ +package fr.free.nrw.commons.upload.license; + +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.repository.UploadRepository; +import fr.free.nrw.commons.settings.Prefs; +import fr.free.nrw.commons.upload.license.MediaLicenseContract.View; + +import java.lang.reflect.Proxy; +import java.util.List; + +import javax.inject.Inject; + +import timber.log.Timber; + +/** + * Added JavaDocs for MediaLicensePresenter + */ +public class MediaLicensePresenter implements MediaLicenseContract.UserActionListener { + + private static final MediaLicenseContract.View DUMMY = (MediaLicenseContract.View) Proxy + .newProxyInstance( + MediaLicenseContract.View.class.getClassLoader(), + new Class[]{MediaLicenseContract.View.class}, + (proxy, method, methodArgs) -> null); + + private final UploadRepository repository; + private MediaLicenseContract.View view = DUMMY; + + @Inject + public MediaLicensePresenter(UploadRepository uploadRepository) { + this.repository = uploadRepository; + } + + @Override + public void onAttachView(View view) { + this.view = view; + } + + @Override + public void onDetachView() { + this.view = DUMMY; + } + + /** + * asks the repository for the available licenses, and informs the view on the same + */ + @Override + public void getLicenses() { + List licenses = repository.getLicenses(); + view.setLicenses(licenses); + + String selectedLicense = repository.getValue(Prefs.DEFAULT_LICENSE, + Prefs.Licenses.CC_BY_SA_4);//CC_BY_SA_4 is the default one used by the commons web app + try {//I have to make sure that the stored default license was not one of the deprecated one's + Utils.licenseNameFor(selectedLicense); + } catch (IllegalStateException exception) { + Timber.e(exception.getMessage()); + selectedLicense = Prefs.Licenses.CC_BY_SA_4; + repository.saveValue(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_4); + } + view.setSelectedLicense(selectedLicense); + + } + + /** + * ask the repository to select a license for the current upload + * + * @param licenseName + */ + @Override + public void selectLicense(String licenseName) { + repository.setSelectedLicense(licenseName); + view.updateLicenseSummary(repository.getSelectedLicense(), repository.getCount()); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java new file mode 100644 index 000000000..0b589fc77 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java @@ -0,0 +1,402 @@ +package fr.free.nrw.commons.upload.mediaDetails; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatButton; +import androidx.appcompat.widget.AppCompatImageButton; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.chrisbanes.photoview.OnScaleChangedListener; +import com.github.chrisbanes.photoview.PhotoView; +import com.jakewharton.rxbinding2.widget.RxTextView; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.filepicker.UploadableFile; +import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.nearby.Place; +import fr.free.nrw.commons.upload.Description; +import fr.free.nrw.commons.upload.DescriptionsAdapter; +import fr.free.nrw.commons.upload.SimilarImageDialogFragment; +import fr.free.nrw.commons.upload.Title; +import fr.free.nrw.commons.upload.UploadBaseFragment; +import fr.free.nrw.commons.upload.UploadModel; +import fr.free.nrw.commons.upload.UploadModel.UploadItem; +import fr.free.nrw.commons.utils.DialogUtil; +import fr.free.nrw.commons.utils.ImageUtils; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.disposables.Disposable; +import timber.log.Timber; + +import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult; + +public class UploadMediaDetailFragment extends UploadBaseFragment implements + UploadMediaDetailsContract.View { + + @BindView(R.id.tv_title) + TextView tvTitle; + @BindView(R.id.ib_map) + AppCompatImageButton ibMap; + @BindView(R.id.ib_expand_collapse) + AppCompatImageButton ibExpandCollapse; + @BindView(R.id.ll_container_media_detail) + LinearLayout llContainerMediaDetail; + @BindView(R.id.et_title) + EditText etTitle; + @BindView(R.id.rv_descriptions) + RecyclerView rvDescriptions; + @BindView(R.id.backgroundImage) + PhotoView photoViewBackgroundImage; + @BindView(R.id.btn_next) + AppCompatButton btnNext; + @BindView(R.id.btn_previous) + AppCompatButton btnPrevious; + private DescriptionsAdapter descriptionsAdapter; + @BindView(R.id.btn_copy_prev_title_desc) + AppCompatButton btnCopyPreviousTitleDesc; + + private UploadModel.UploadItem uploadItem; + private List descriptions; + + @Inject + UploadMediaDetailsContract.UserActionListener presenter; + private UploadableFile uploadableFile; + private String source; + private Place place; + + private Title title; + private boolean isExpanded = true; + + private UploadMediaDetailFragmentCallback callback; + + public void setCallback(UploadMediaDetailFragmentCallback callback) { + this.callback = callback; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + public void setImageTobeUploaded(UploadableFile uploadableFile, String source, Place place) { + this.uploadableFile = uploadableFile; + this.source = source; + this.place = place; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_upload_media_detail_fragment, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ButterKnife.bind(this, view); + init(); + } + + private void init() { + tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1, + callback.getTotalNumberOfSteps())); + title = new Title(); + initRecyclerView(); + initPresenter(); + Disposable disposable = RxTextView.textChanges(etTitle) + .subscribe(text -> { + if (!TextUtils.isEmpty(text)) { + btnNext.setEnabled(true); + btnNext.setClickable(true); + btnNext.setAlpha(1.0f); + title.setTitleText(text.toString()); + uploadItem.setTitle(title); + } else { + btnNext.setAlpha(0.5f); + btnNext.setEnabled(false); + btnNext.setClickable(false); + } + }); + compositeDisposable.add(disposable); + presenter.receiveImage(uploadableFile, source, place); + + if (callback.getIndexInViewFlipper(this) == 0) { + btnPrevious.setEnabled(false); + btnPrevious.setAlpha(0.5f); + } else { + btnPrevious.setEnabled(true); + btnPrevious.setAlpha(1.0f); + } + + //If this is the first media, we have nothing to copy, lets not show the button + if (callback.getIndexInViewFlipper(this) == 0) { + btnCopyPreviousTitleDesc.setVisibility(View.GONE); + } else { + btnCopyPreviousTitleDesc.setVisibility(View.VISIBLE); + } + + attachImageViewScaleChangeListener(); + + addEtTitleTouchListener(); + } + + /** + * Handles the drawable click listener for Edit Text + */ + private void addEtTitleTouchListener() { + etTitle.setOnTouchListener((v, event) -> { + //2 is for drawable right + float twelveDpInPixels = convertDpToPixel(12, getContext()); + if (event.getAction() == MotionEvent.ACTION_UP && etTitle.getCompoundDrawables()[2].getBounds().contains((int)(etTitle.getWidth()-(event.getX()+twelveDpInPixels)),(int)(event.getY()-twelveDpInPixels))){ + showInfoAlert(R.string.media_detail_title,R.string.title_info); + return true; + } + return false; + }); + } + + /** + * converts dp to pixel + * @param dp + * @param context + * @return + */ + private float convertDpToPixel(float dp, Context context) { + return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT); + } + + /** + * Attaches the scale change listener to the image view + */ + private void attachImageViewScaleChangeListener() { + photoViewBackgroundImage.setOnScaleChangeListener( + (scaleFactor, focusX, focusY) -> { + //Whenever the uses plays with the image, lets collapse the media detail container + expandCollapseLlMediaDetail(false); + }); + } + + /** + * attach the presenter with the view + */ + private void initPresenter() { + presenter.onAttachView(this); + } + + /** + * init the recycler veiw + */ + private void initRecyclerView() { + descriptionsAdapter = new DescriptionsAdapter(); + descriptionsAdapter.setCallback(this::showInfoAlert); + rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext())); + rvDescriptions.setAdapter(descriptionsAdapter); + } + + /** + * show dialog with info + * @param titleStringID + * @param messageStringId + */ + private void showInfoAlert(int titleStringID, int messageStringId) { + DialogUtil.showAlertDialog(getActivity(), getString(titleStringID), getString(messageStringId), getString(android.R.string.ok), null, true); + } + + @OnClick(R.id.btn_next) + public void onNextButtonClicked() { + uploadItem.setDescriptions(descriptionsAdapter.getDescriptions()); + presenter.verifyImageQuality(uploadItem, true); + } + + @OnClick(R.id.btn_previous) + public void onPreviousButtonClicked() { + callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this)); + } + + @OnClick(R.id.btn_add_description) + public void onButtonAddDescriptionClicked() { + Description description = new Description(); + description.setManuallyAdded(true);//This was manually added by the user + descriptionsAdapter.addDescription(description); + } + + @Override + public void showSimilarImageFragment(String originalFilePath, String possibleFilePath) { + SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment(); + newFragment.setCallback(new SimilarImageDialogFragment.Callback() { + @Override + public void onPositiveResponse() { + Timber.d("positive response from similar image fragment"); + } + + @Override + public void onNegativeResponse() { + Timber.d("negative response from similar image fragment"); + } + }); + Bundle args = new Bundle(); + args.putString("originalImagePath", originalFilePath); + args.putString("possibleImagePath", possibleFilePath); + newFragment.setArguments(args); + newFragment.show(getChildFragmentManager(), "dialog"); + } + + @Override + public void onImageProcessed(UploadItem uploadItem, Place place) { + this.uploadItem = uploadItem; + if (uploadItem.getTitle() != null) { + etTitle.setText(uploadItem.getTitle().toString()); + } + + descriptions = uploadItem.getDescriptions(); + photoViewBackgroundImage.setImageURI(uploadItem.getMediaUri()); + setDescriptionsInAdapter(descriptions); + } + + @Override + public void showProgress(boolean shouldShow) { + callback.showProgress(shouldShow); + } + + @Override + public void onImageValidationSuccess() { + presenter.setUploadItem(callback.getIndexInViewFlipper(this), uploadItem); + callback.onNextButtonClicked(callback.getIndexInViewFlipper(this)); + } + + @Override + public void showMessage(int stringResourceId, int colorResourceId) { + ViewUtil.showLongToast(getContext(), stringResourceId); + } + + @Override + public void showMessage(String message, int colorResourceId) { + ViewUtil.showLongToast(getContext(), message); + } + + @Override + public void showDuplicatePicturePopup() { + String uploadTitleFormat = getString(R.string.upload_title_duplicate); + DialogUtil.showAlertDialog(getActivity(), + getString(R.string.warning), + String.format(Locale.getDefault(), + uploadTitleFormat, + uploadItem.getFileName()), + () -> { + + }, + () -> { + uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP); + onNextButtonClicked(); + }); + } + + @Override + public void showBadImagePopup(Integer errorCode) { + String errorMessageForResult = getErrorMessageForResult(getContext(), errorCode); + if (!StringUtils.isBlank(errorMessageForResult)) { + DialogUtil.showAlertDialog(getActivity(), + getString(R.string.warning), + errorMessageForResult, + () -> deleteThisPicture(), + () -> { + uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP); + onNextButtonClicked(); + }); + } + //If the error message is null, we will probably not show anything + } + + @Override public void showMapWithImageCoordinates(boolean shouldShow) { + ibMap.setVisibility(shouldShow ? View.VISIBLE : View.GONE); + } + + @Override + public void setTitleAndDescription(String title, List descriptions) { + etTitle.setText(title); + setDescriptionsInAdapter(descriptions); + } + + private void deleteThisPicture() { + callback.deletePictureAtIndex(callback.getIndexInViewFlipper(this)); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + presenter.onDetachView(); + } + + @OnClick(R.id.rl_container_title) + public void onRlContainerTitleClicked() { + expandCollapseLlMediaDetail(!isExpanded); + } + + /** + * show hide media detail based on + * @param shouldExpand + */ + private void expandCollapseLlMediaDetail(boolean shouldExpand){ + llContainerMediaDetail.setVisibility(shouldExpand ? View.VISIBLE : View.GONE); + isExpanded = !isExpanded; + ibExpandCollapse.setRotation(ibExpandCollapse.getRotation() + 180); + } + + @OnClick(R.id.ib_map) public void onIbMapClicked() { + Utils.handleGeoCoordinates(getContext(), + new LatLng(uploadItem.getGpsCoords().getDecLatitude(), + uploadItem.getGpsCoords().getDecLongitude(), 0.0f)); + } + + + public interface UploadMediaDetailFragmentCallback extends Callback { + + void deletePictureAtIndex(int index); + } + + + @OnClick(R.id.btn_copy_prev_title_desc) + public void onButtonCopyPreviousTitleDesc(){ + presenter.fetchPreviousTitleAndDescription(callback.getIndexInViewFlipper(this)); + } + + private void setDescriptionsInAdapter(List descriptions){ + if(descriptions==null){ + descriptions=new ArrayList<>(); + } + + if(descriptions.size()==0){ + descriptions.add(new Description()); + } + + descriptionsAdapter.setItems(descriptions); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java new file mode 100644 index 000000000..9447000ab --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java @@ -0,0 +1,52 @@ +package fr.free.nrw.commons.upload.mediaDetails; + +import java.util.List; + +import fr.free.nrw.commons.BasePresenter; +import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.filepicker.UploadableFile; +import fr.free.nrw.commons.nearby.Place; +import fr.free.nrw.commons.upload.Description; +import fr.free.nrw.commons.upload.SimilarImageInterface; +import fr.free.nrw.commons.upload.Title; +import fr.free.nrw.commons.upload.UploadModel.UploadItem; + +/** + * The contract with with UploadMediaDetails and its presenter would talk to each other + */ +public interface UploadMediaDetailsContract { + + interface View extends SimilarImageInterface { + + void onImageProcessed(UploadItem uploadItem, Place place); + + void showProgress(boolean shouldShow); + + void onImageValidationSuccess(); + + void showMessage(int stringResourceId, int colorResourceId); + + void showMessage(String message, int colorResourceId); + + void showDuplicatePicturePopup(); + + void showBadImagePopup(Integer errorCode); + + void showMapWithImageCoordinates(boolean shouldShow); + + void setTitleAndDescription(String title, List descriptions); + } + + interface UserActionListener extends BasePresenter { + + void receiveImage(UploadableFile uploadableFile, @Contribution.FileSource String source, + Place place); + + void verifyImageQuality(UploadItem uploadItem, boolean validateTitle); + + void setUploadItem(int index, UploadItem uploadItem); + + void fetchPreviousTitleAndDescription(int indexInViewFlipper); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java new file mode 100644 index 000000000..3cccfe89d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java @@ -0,0 +1,194 @@ +package fr.free.nrw.commons.upload.mediaDetails; + +import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; +import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD; +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; + +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.filepicker.UploadableFile; +import fr.free.nrw.commons.nearby.Place; +import fr.free.nrw.commons.repository.UploadRepository; +import fr.free.nrw.commons.upload.GPSExtractor; +import fr.free.nrw.commons.upload.SimilarImageInterface; +import fr.free.nrw.commons.upload.UploadModel.UploadItem; +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.UserActionListener; +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.View; +import io.reactivex.Scheduler; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +import java.lang.reflect.Proxy; + +import javax.inject.Inject; +import javax.inject.Named; + +import timber.log.Timber; + +public class UploadMediaPresenter implements UserActionListener, SimilarImageInterface { + + private static final UploadMediaDetailsContract.View DUMMY = (UploadMediaDetailsContract.View) Proxy + .newProxyInstance( + UploadMediaDetailsContract.View.class.getClassLoader(), + new Class[]{UploadMediaDetailsContract.View.class}, + (proxy, method, methodArgs) -> null); + + private final UploadRepository repository; + private UploadMediaDetailsContract.View view = DUMMY; + + private CompositeDisposable compositeDisposable; + + private Scheduler ioScheduler; + private Scheduler mainThreadScheduler; + + @Inject + public UploadMediaPresenter(UploadRepository uploadRepository, + @Named(IO_THREAD) Scheduler ioScheduler, + @Named(MAIN_THREAD) Scheduler mainThreadScheduler) { + this.repository = uploadRepository; + this.ioScheduler = ioScheduler; + this.mainThreadScheduler = mainThreadScheduler; + compositeDisposable = new CompositeDisposable(); + } + + @Override + public void onAttachView(View view) { + this.view = view; + } + + @Override + public void onDetachView() { + this.view = DUMMY; + compositeDisposable.clear(); + } + + /** + * Receives the corresponding uploadable file, processes it and return the view with and uplaod item + * + * @param uploadableFile + * @param source + * @param place + */ + @Override + public void receiveImage(UploadableFile uploadableFile, String source, Place place) { + view.showProgress(true); + Disposable uploadItemDisposable = repository + .preProcessImage(uploadableFile, place, source, this) + .subscribeOn(ioScheduler) + .observeOn(mainThreadScheduler) + .subscribe(uploadItem -> + { + view.onImageProcessed(uploadItem, place); + GPSExtractor gpsCoords = uploadItem.getGpsCoords(); + view.showMapWithImageCoordinates((gpsCoords != null && gpsCoords.imageCoordsExists) ? true : false); + view.showProgress(false); + }, + throwable -> Timber.e(throwable, "Error occurred in processing images")); + compositeDisposable.add(uploadItemDisposable); + } + + /** + * asks the repository to verify image quality + * + * @param uploadItem + * @param validateTitle + */ + @Override + public void verifyImageQuality(UploadItem uploadItem, boolean validateTitle) { + view.showProgress(true); + Disposable imageQualityDisposable = repository + .getImageQuality(uploadItem, true) + .subscribeOn(ioScheduler) + .observeOn(mainThreadScheduler) + .subscribe(imageResult -> { + view.showProgress(false); + handleImageResult(imageResult); + }, + throwable -> { + view.showProgress(false); + view.showMessage("" + throwable.getLocalizedMessage(), + R.color.color_error); + Timber.e(throwable, "Error occurred while handling image"); + }); + + compositeDisposable.add(imageQualityDisposable); + } + + /** + * Adds the corresponding upload item to the repository + * + * @param index + * @param uploadItem + */ + @Override + public void setUploadItem(int index, UploadItem uploadItem) { + repository.updateUploadItem(index, uploadItem); + } + + /** + * Fetches and sets the title and desctiption of the previous item + * + * @param indexInViewFlipper + */ + @Override + public void fetchPreviousTitleAndDescription(int indexInViewFlipper) { + UploadItem previousUploadItem = repository.getPreviousUploadItem(indexInViewFlipper); + if (null != previousUploadItem) { + view.setTitleAndDescription(previousUploadItem.getTitle().getTitleText(), previousUploadItem.getDescriptions()); + } else { + view.showMessage(R.string.previous_image_title_description_not_found, R.color.color_error); + } + } + + /** + * handles image quality verifications + * + * @param imageResult + */ + public void handleImageResult(Integer imageResult) { + if (imageResult == IMAGE_KEEP || imageResult == IMAGE_OK) { + view.onImageValidationSuccess(); + } else { + handleBadImage(imageResult); + } + } + + /** + * Handle images, say empty title, duplicate file name, bad picture(in all other cases) + * + * @param errorCode + */ + private void handleBadImage(Integer errorCode) { + Timber.d("Handle bad picture with error code %d", errorCode); + if (errorCode + >= 8) { // If location of image and nearby does not match, then set shared preferences to disable wikidata edits + repository.saveValue("Picture_Has_Correct_Location", false); + } + + switch (errorCode) { + case EMPTY_TITLE: + Timber.d("Title is empty. Showing toast"); + view.showMessage(R.string.add_title_toast, R.color.color_error); + break; + case FILE_NAME_EXISTS: + Timber.d("Trying to show duplicate picture popup"); + view.showDuplicatePicturePopup(); + break; + default: + view.showBadImagePopup(errorCode); + } + } + + /** + * notifies the user that a similar image exists + * + * @param originalFilePath + * @param possibleFilePath + */ + @Override + public void showSimilarImageFragment(String originalFilePath, String possibleFilePath) { + view.showSimilarImageFragment(originalFilePath, possibleFilePath); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java index 53d129fb3..e9765551c 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java @@ -140,4 +140,31 @@ public class DialogUtil { showSafely(activity, dialog); } + + /** + * show a dialog with just a positive button + * @param activity + * @param title + * @param message + * @param positiveButtonText + * @param positiveButtonClick + * @param cancellable + */ + public static void showAlertDialog(Activity activity, String title, String message, String positiveButtonText, final Runnable positiveButtonClick, boolean cancellable) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(title); + builder.setMessage(message); + builder.setCancelable(cancellable); + + builder.setPositiveButton(positiveButtonText, (dialogInterface, i) -> { + dialogInterface.dismiss(); + if (positiveButtonClick != null) { + positiveButtonClick.run(); + } + }); + + AlertDialog dialog = builder.create(); + showSafely(activity, dialog); + } + } diff --git a/app/src/main/res/drawable/drawable_thumbnail_image.xml b/app/src/main/res/drawable/drawable_thumbnail_image.xml new file mode 100644 index 000000000..b406e8938 --- /dev/null +++ b/app/src/main/res/drawable/drawable_thumbnail_image.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/app/src/main/res/drawable/thumbnail_not_selected.xml b/app/src/main/res/drawable/thumbnail_not_selected.xml new file mode 100644 index 000000000..8ead4b377 --- /dev/null +++ b/app/src/main/res/drawable/thumbnail_not_selected.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/thumbnail_selected.xml b/app/src/main/res/drawable/thumbnail_selected.xml new file mode 100644 index 000000000..ac6ec9335 --- /dev/null +++ b/app/src/main/res/drawable/thumbnail_selected.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_upload.xml b/app/src/main/res/layout/activity_upload.xml index 3987a53d3..7822ce593 100644 --- a/app/src/main/res/layout/activity_upload.xml +++ b/app/src/main/res/layout/activity_upload.xml @@ -1,34 +1,79 @@ - - + + + + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="@dimen/standard_gap" + > - + + + - + - + - - - - - + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_upload_bottom_card.xml b/app/src/main/res/layout/activity_upload_bottom_card.xml deleted file mode 100644 index 3afe769df..000000000 --- a/app/src/main/res/layout/activity_upload_bottom_card.xml +++ /dev/null @@ -1,199 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -