From 56d0beb22a2292f54aa7a72fda26d9b314fb3088 Mon Sep 17 00:00:00 2001 From: LachlanMajor Date: Sun, 20 Oct 2024 22:36:15 +1100 Subject: [PATCH 01/74] Fixes #5840 Custom select folder display breaks after exiting media preview (#5866) * ImageFragment.kt: notifyDataSetChanged() added to update observers about init call in handleResult() * ImageFragment.kt: unnecessary initialisation after exiting media preview was removed from passSelectedImages --- .../ui/selector/CustomSelectorActivity.kt | 4 +--- .../customselector/ui/selector/ImageFragment.kt | 12 ++++-------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index d39b69f29..3bba6f05c 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -41,7 +41,6 @@ import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.database.NotForUploadStatus import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants -import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants.SHOULD_REFRESH import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.Image @@ -237,8 +236,7 @@ class CustomSelectorActivity : val selectedImages: ArrayList = data!! .getParcelableArrayListExtra(CustomSelectorConstants.NEW_SELECTED_IMAGES)!! - val shouldRefresh = data.getBooleanExtra(SHOULD_REFRESH, false) - imageFragment?.passSelectedImages(selectedImages, shouldRefresh) + viewModel?.selectedImages?.value = selectedImages } } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index 7e522f681..efe640e82 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -279,6 +279,10 @@ class ImageFragment : filteredImages = ImageHelper.filterImages(images, bucketId) allImages = ArrayList(filteredImages) imageAdapter.init(filteredImages, allImages, TreeMap(), uploadingContributions) + viewModel?.selectedImages?.value?.let { selectedImages -> + imageAdapter.setSelectedImages(selectedImages) + } + imageAdapter.notifyDataSetChanged() selectorRV?.let { it.visibility = View.VISIBLE lastItemId?.let { pos -> @@ -382,14 +386,6 @@ class ImageFragment : selectedImages: ArrayList, shouldRefresh: Boolean, ) { - imageAdapter.setSelectedImages(selectedImages) - - val uploadingContributions = getUploadingContributions() - - if (!showAlreadyActionedImages && shouldRefresh) { - imageAdapter.init(filteredImages, allImages, TreeMap(), uploadingContributions) - imageAdapter.setSelectedImages(selectedImages) - } } /** From 014feb54e51eae5e1176efeefae8104632132422 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 21 Oct 2024 14:02:00 +0200 Subject: [PATCH 02/74] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-az/error.xml | 5 +- app/src/main/res/values-az/strings.xml | 50 ++++++--- app/src/main/res/values-da/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-lt/strings.xml | 114 ++++++++++++++++++--- app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-pa/strings.xml | 2 +- app/src/main/res/values-pms/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-zh-rHK/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- 13 files changed, 150 insertions(+), 35 deletions(-) diff --git a/app/src/main/res/values-az/error.xml b/app/src/main/res/values-az/error.xml index e698eab9d..6e9503c43 100644 --- a/app/src/main/res/values-az/error.xml +++ b/app/src/main/res/values-az/error.xml @@ -2,10 +2,11 @@ Nasazlıq Uups. Nəsə düzgün çalışmır! - Nə etdiyinizi dəqiqləşdirib, bizə bildirin və sonra e-poçtla bizə göndərin. Bu problemi həll etməyə bizə kömək edin. - Təşəkkür! + Nə etdiyinizi bizə deyin, sonra e-poçt vasitəsilə bizimlə paylaşın. Bu, bizə bunu düzəltməyə kömək edəcək! + Təşəkkürlər! diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index d5e83a2c7..1edbe43fc 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -12,34 +12,60 @@ * Şeyx Şamil --> + Commons Facebook səhifəsi + Commons Github Mənbə Kodu + Commons Loqotipi + Commons Veb-saytı + Məkan seçicidən çıxın + Göndər + Başqa təsvir əlavə et + Yeni töhfə + Kamera ilə töhfə ver + Fotolar ilə töhfə ver + Əvvəlki töhfələr qalereyasından töhfə əlavə et + Başlıqlar + Dil təsviri + Başlıq + Təsvir + Şəkil + Hamısı + Aç/Bağla + Axtarış Görünüşü + Məkanın Vəziyyəti + Günün Şəkli %1$d fayl yüklənir %1$d fayllar yüklənir + + (%1$d) + (%1$d) + + Yükləmələrə Başlanılır Ümumi Məxfilik - Vikimedia Commons + Vikianbar Tənzimləmələr - Ləqəb + İstifadəçi adı Parol Daxil ol Qeydiyyatdan keç Giriş edilir - Lütfən gözləyin… + Zəhmət olmasa, gözləyin… Daxil oldunuz! Giriş baş tutmadı! Fayl tapılmadı. Xahiş edirik başqa bir fayl üzərində cəhd edin. Doğrulama alınmadı, xahiş edirəm yenidən daxil olun Yükləmə başladı! %1$s yükləndi! - Yüklədiyini izlə + Yükləmənizə baxmaq üçün toxunun %1$s yüklənməsi başlanır %1$s yüklənir %1$s yüklənməsi başa çatdı %1$s faylının yüklənməsi alınmadı - Baxmaq üçün toxunun - Yükləmələrim - Sırada + Baxmaq üçün toxun + Son Yükləmələrim + Növbəyə alındı Uğursuz %1$d%% tamamlandı Yüklənir @@ -52,15 +78,15 @@ Açıqlama Daxil olmaq olmur — şəbəkə xətası Çox sayda uğursuz daxil olma. Xahiş edirik bir neçə dəqiqə sonra yenidən cəhd edin. - Bağışlayın, bu istifadəçi Commons-da bloklanmışdır. - İki faktorlu giriş doğrulama kodunu verməlisiniz. + Bağışlayın, bu istifadəçi Vikianbardan bloklanıb + Siz iki faktorlu autentifikasiya kodunuzu təqdim etməlisiniz. Daxil olma uğursuz oldu Yüklə Bu dəsti adlandırın - Bildirişlər + Dəyişikliklər Yüklə - Kateqoriyaları axtar - Qeyd et + Kateqoriyalarda axtar + Yadda saxla Yenilə Siyahı (Hələ yükləmə yoxdur) diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 4116080ea..2aa04017a 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -481,6 +481,7 @@ Du har ingen ulæste notifikationer Du har ingen læste notifikationer Del logs ved hjælp af + Tjek din e-mail-indbakke Vis læste Vis ulæste Der opstod en fejl under udvælgelse af billeder diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f95d4b095..992f418af 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -22,6 +22,7 @@ * JenyxGym * KATRINE1992 * Koreller +* Mahabarata * McDutchie * Melissadeba95 * Metroitendo @@ -516,6 +517,7 @@ Vous n’avez aucune notification non lue Vous n’avez aucune notification lue Partager les journaux en utilisant + Vérifiez votre boîte de réception Afficher les lus Afficher les non lus Une erreur est survenue lors de la sélection des images diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1a50e9799..f20b986f8 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -462,6 +462,7 @@ 未読の通知はありません 既読のお知らせはありません ログの共有に使うアプリ + メールをご確認ください 既読を表示 未読を見る 画像の選択中にエラーが発生しました @@ -679,7 +680,6 @@ 作者への感謝の送信エラー。 ログインが期限切れになりました。もう一度ログインしてください。 GPXファイルを開くことができるアプリケーションがありません - メールをご確認ください %d件の画像が選択されました diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 22749651c..26a9bc7f7 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -28,6 +28,7 @@ Aprašymas Paveikslėlis Visi + Perjungti aukštyn Paieškos rodinys Dienos nuotrauka @@ -57,6 +58,7 @@ Vikiteka Nustatymai Įkelti į Vikiteką + Vyksta įkėlimas Naudotojo vardas Slaptažodis Prisijunkite prie savo Commons Beta paskyros @@ -67,10 +69,13 @@ Prašome palaukti… Antraštės ir aprašymai atnaujinami Prašome palaukti... - Sėkmingai prisijungėte! - Prisijungti nepavyko! + Sėkmingai prisijungėte! + Prisijungti nepavyko! Failas nerastas. Prašome pabandyti kitą failą. - Autentifikavimas nepavyko, prašome prisijungti dar kartą + Pasiektas maksimalus pakartotinių bandymų limitas! Atšaukite įkėlimą ir bandykite dar kartą + Išjungti akumuliatoriaus optimizavimą? + Daugiau nei 3 paveikslėlių įkėlimas veikia patikimiau, kai akumuliatoriaus optimizavimas išjungtas. Išjunkite Vikitekos programėlės akumuliatoriaus optimizavimą nustatymuose, kad įkėlimas būtų sklandus. \n\nGalimi akumuliatoriaus optimizavimo išjungimo veiksmai:\n\n1 veiksmas: bakstelėkite toliau esantį mygtuką „Nustatymai“.\n\n2 veiksmas: perjunkite iš „Neoptimizuota“ į „Visos programėlės“.\n\n3 veiksmas: ieškokite „Vikiteka“ arba „fr.free.nrw.commons“.\n\n4 veiksmas: spustelėkite jį ir pasirinkite „Neoptimizuoti“.\n\n5 veiksmas: paspauskite „Atlikta“. + Autentifikavimas nepavyko. Prašome prisijungti dar kartą. Įkėlimas prasidėjo! Įkėlimas eilėje (įgalintas riboto ryšio režimas) %1$s įkelta! @@ -97,11 +102,11 @@ Pateikite šio failo antraštę Aprašymas Antraštė - Negalima prisijungti - tinklo klaida + Negalima prisijungti - tinklo klaida Per daug nesėkmingų bandymų. Pabandykite dar kartą po keleto minučių. Atsiprašome, šis vartotojas buvo užblokuotas Commons Turite pateikti savo dviejų žingsnių patvirtinimo kodą. - Prisijungti nepavyko + Prisijungti nepavyko Įkelti Pavadinkite šį rinkinį Pakeitimai @@ -123,7 +128,7 @@ Kategorija Apie Vikitekos programėlė yra atviro kodo programėlė, kurią sukūrė ir prižiūri Vikitekos bendruomenės dotacijų gavėjai ir savanoriai. „Wikimedia Foundation“ nedalyvauja kuriant, plėtojant ar prižiūrint programėlę. - Sukurkite naują <a href=\"%1$s\">GitHub pranešimą</a>, siekiant pranešti apie klaidas ir pateikti siūlymus. + Sukurkite naują <a href=\"%1$s\">GitHub pranešimą</a>, siekiant pranešti apie klaidas ir pateikti siūlymus. Privatumo politika Kūrėjai Apie @@ -178,6 +183,8 @@ Reikalinga teisė: Skaityti išorinę talpyklą. Programėle be to negali prieiti prie jūsų galerijos. Reikalingas leidimas: rašyti į išorinę saugyklą. Programėlė be to negali pasiekti jūsų fotoaparato/galerijos. Prašoma vietovės leidimo + Įrašyti vietovę nuotraukoms, kurios fotografuotos programėlėje + Įjunkite šią funkciją, kad įrašytumėte vietovę nuotraukoms, jei jūsų įrenginio kamera to nepadaro Gerai Įspėjimas Rastas pasikartojantis failo pavadinimas @@ -198,6 +205,7 @@ Prisijunkite prie beta kanalo Google Play ir gaukite išankstinę prieigą prie naujų funkcijų bei klaidų pataisymų 2FA kodas Ar tikrai norite atsijungti? + Medijos paveikslėlis nepavyko Subkategorijų nerasta Zao kalnas Lamos @@ -214,6 +222,7 @@ Apie Nustatymai Atsiliepimai + Atsiliepimai per GitHub Atsijungti Pamoka Pranešimai @@ -247,13 +256,15 @@ Žiūrėkite tinklapį dėl daugiau informacijos Praleisti Prisijungti - Ar tikrai norite praleisti prisijungimą? - Norėdami ateityje įkelti nuotraukas, turėsite prisijungti. + Ar tikrai norite praleisti prisijungimą? + Norėdami ateityje įkelti nuotraukas, turėsite prisijungti. Jei norite naudotis šia funkcija, prisijunkite Nukopijuokite vikitekstą į mainų sritį Vikitekstas buvo nukopijuotas į mainų sritį Netoliese gali tinkamai neveikti, vieta nepasiekiama. + Prieiga prie vietos uždrausta. Norėdami naudotis šia funkcija, nustatykite savo vietą rankiniu būdu. Norint rodyti netoliese esančių vietų sąrašą, reikalingas leidimas + Norint rodyti netoliese esančių paveikslėlių sąrašą, reikalingas leidimas Nurodymai Vikiduomenys Vikipedija @@ -313,14 +324,16 @@ Nuotraukos, kuriose pavaizduotos technologijos ar kultūra, yra labai laukiamos Vikitekoje. Surinkote %1$s teisingų atsakymų. Sveikiname! Norėdami atsakyti į klausimą, pasirinkite vieną iš dviejų variantų - Prisijungimo sesija baigėsi, prisijunkite dar kartą. + Prisijungimo sesija baigėsi, prašome prisijungti dar kartą. Pasidalinkite savo apkalusa su draugais! Tęsti Teisingas atsakymas Atsakymas neteisingas Ar šią ekrano kopiją galima įkelti? Dalintis programėle - Klaida gaunant netoliese esančias vietas. + Pasukti + Nepavyko įkelti netoliese esančių vietų + Šioje vietovėje nuotraukų nėra Nėra šalia esančių vietų Gaunant netoliese esančius paminklus įvyko klaida. Nėra naujausių paieškų @@ -338,6 +351,7 @@ Vaizdai per „Netoliese esančios vietos“ Lygis Vaizdai įkelti + Paveikslėliai negrąžinti Naudoti vaizdai Pasidalinkite savo pasiekimais su draugais! Jūsų lygis kyla, kai atitinkate šiuos reikalavimus. Skiltyje „statistika“ esantys elementai neįskaičiuojami į jūsų lygį. @@ -394,12 +408,22 @@ Niekada daugiau to neklausti Paprašyti vietos leidimo Jei reikia, kad būtų galima naudoti netoliese esančio pranešimų kortelės peržiūros funkciją, paprašykite leidimo nustatyti vietą. - Kažkas ne taip. Nepavyko gauti jūsų pasiekimų + Kažkas ne taip, mums nepavyko gauti pasiekimų Prisidėjote tiek daug, kad mūsų pasiekimų skaičiavimo sistema negali susidoroti. Tai yra didžiausias pasiekimas. Baigiasi: Peržiūrėkite vykstančias kampanijas + Leiskite programėlei nuskaityti vietą, jei fotoaparatas jos neįrašo. Kai kurių įrenginių kameros neįrašo vietos. Tokiais atvejais leidus programai gauti ir pridėti vietą, jūsų indėlis bus naudingesnis. Tai galite bet kada pakeisti nustatymuose + Leisti + Paslėpti + Nustatymuose įjunkite prieigą prie vietos ir bandykite dar kartą. \n\nPastaba: įkėlimas gali neturėti vietos, jei programėlė negali per trumpą laiką nuskaityti vietos iš įrenginio. + Programėlėje esančiam fotoaparatui reikalingas vietos leidimas, kad jis būtų pridėtas prie vaizdų, jei EXIF nėra vietos. Leiskite programėlei pasiekti jūsų buvimo vietą ir bandykite dar kartą.\n\nPastaba: įkėlimas gali neturėti vietos, jei programėlė negali per trumpą laiką nuskaityti vietos iš įrenginio. + Programėlė neįrašys vietos kartu su kadrais, nes neturi vietos leidimo + Programa neįrašys vietos kartu su kadrais, nes GPS išjungtas + Naudokite dokumentais pagrįstą nuotraukų rinkiklį + Naujasis „Android“ nuotraukų rinkiklis gali prarasti vietos informaciją. Įjunkite, jei atrodo, kad jį naudojate. + Išjungus tai gali suaktyvinti naująjį „Android“ nuotraukų rinkiklį. Dėl to kyla pavojus prarasti vietos informaciją.\n\nNorėdami gauti daugiau informacijos, bakstelėkite „Skaityti daugiau“. Kampanijų nebematysite. Tačiau, jei norite, galite iš naujo įjungti šį pranešimą nustatymuose. - Šiai funkcijai reikalingas tinklo ryšys, patikrinkite ryšio nustatymus. + Šiai funkcijai reikalingas tinklo ryšys. Prašome patikrinti savo ryšio nustatymus. Apdorojant vaizdą įvyko klaida. Pabandykite dar kartą! Gaunamas redagavimo prieigos raktas Kategorijos tikrinimo šablonas pridedamas @@ -418,22 +442,26 @@ Siunčiama padėka už %1$s Ar tai atitinka autorines teises? Ar tai teisingai priskirta kategorijoms? + Ar tai taikytina? Ar norėtumėte padėkoti prisidėjusiam? Spustelėkite NE, kad pasiūlytumėte šį vaizdą ištrinti, jei jis visai nenaudingas. Logotipai, ekrano kopijos, filmų plakatai dažnai pažeidžia autorines teises. Spustelėkite NE, jei norite pasiūlyti šį vaizdą ištrinti %1$s bus padrąsintas jūsų dėkingumu Oi, tai net nėra priskirta kategorijai! Šis vaizdas priklauso %1$s kategorijoms. + Tai nėra taikytina, nes Tai yra autorių teisių pažeidimas, nes Kitas vaizdas Taip, kodėl gi ne Spustelėję šį mygtuką pamatysite kitą neseniai įkeltą vaizdą iš Vikitekos + Galite peržiūrėti vaizdus, kad pagerintumėte Vikitekos kokybę.\nTrys peržiūros parametrai yra:\n\n- Ar šis vaizdas tinkamas?\nKai paliesite Ne (nepatenka į sritį), jūs prie šio paveikslėlio pridedate ištrynimo nominacijos šabloną.\n\n- Ar šis vaizdas atitinka autorių teisių taisykles?\nKai paliesite Ne (neatitinka autorių teisių taisyklių), pridedate ištrynimo nominacijos šabloną prie šio paveikslėlio.\n\n- Ar šis vaizdas teisingai suskirstytas į kategorijas?\nKai paliesite Ne (neteisingai suskirstytas į kategorijas), prie šio paveikslėlio pridedate kategorizavimo užklausos šabloną.\n\nJei viskas yra gerai, prie paveikslėlio nepridedamas joks šablonas, ir jūs turite galimybę padėkoti bendraautoriui. Nenaudojami jokie vaizdai Jokie vaizdai negrąžinti Neįkelta jokių vaizdų Neturite neskaitytų pranešimų Neturite perskaitytų pranešimų Dalinkitės žurnalus naudodami + Patikrinkite savo el. pašto dėžutę Žiūrėti perskaitytus Žiūrėti neperskaitytus Renkant vaizdus įvyko klaida @@ -478,6 +506,7 @@ Žiniasklaidos nuotrauka Atsitiktinė nuotrauka iš interneto Logotipas + Panoramos laisvės pažeidimas Nes Bandoma atnaujinti kategorijas. Kategorijos atnaujinimas @@ -498,7 +527,7 @@ Nepavyko pridėti koordinačių. Nepavyko pridėti aprašymų. Nepavyko pridėti antraštę. - Nepavyko gauti koordinačių. + Paveikslėlio koordinatės neatnaujintos Nepavyko gauti aprašymų. Redaguokite aprašymus ir antraštes Dalintis vaizdu per @@ -513,10 +542,11 @@ Reikia Nuotraukos Vietos tipas: Tiltas, muziejus, viešbutis ir t.t. - Kažkas nepavyko prisijungiant, turite iš naujo nustatyti slaptažodį !! + Kažkas nepavyko prisijungiant. Turite iš naujo nustatyti slaptažodį! MEDIJA Netoliese rasta vieta - Ar tai vietos %1$s nuotrauka? + Ar tai %1$s nuotraukos? + Ar tai %1$s nuotrauka? Žymės Nustatymai Pašalinta iš žymių @@ -524,12 +554,16 @@ Kažkas nepavyko. Nepavyko nustatyti fono paveikslėlio Nustatyti kaip fono paveikslėlį Fono paveikslėlis nustatomas. Prašome palaukti… + Sekti sistemos Tamsus Šviesus Nepavyko atidaryti vietos nustatymų. Įjunkite vietą rankiniu būdu Norėdami gauti geriausius rezultatus, pasirinkite didelio tikslumo režimą. Įjungti vietą? + Įjunkite vietos nustatymo paslaugas, kad programa parodytų jūsų dabartinę vietą Kad tinkamai veiktų, Netoliese turi būti įjungta vieta + Žemėlapio naršymui reikia vietos leidimo, kad būtų rodomi netoliese esantys paveikslėliai + Norėdami automatiškai nustatyti vietą, turite suteikti vietos leidimą. Ar nufotografavote šias dvi nuotraukas toje pačioje vietoje? Ar norite naudoti dešinėje esančio paveikslėlio platumą/ilgumą? Įkelti daugiau Vietų nerasta, pabandykite pakeisti paieškos kriterijus. @@ -617,6 +651,10 @@ Skirtingai nuo paveikslėlio kairėje, paveikslėlyje dešinėje yra Vikitekos logotipas, nurodantis, kad jis jau įkeltas. \n Palieskite ir palaikykite, kad peržiūrėtumėte vaizdą. Puiku Šis vaizdas jau buvo įkeltas į Vikiteką. + Dėl techninių priežasčių programėlė negali patikimai įkelti daugiau nei %1$d nuotraukos vienu metu. %1$d įkėlimo limitas buvo viršytas %2$d. + Paslėpti + Maksimalus: %1$d + Klaida: viršytas įkėlimo limitas Šis vaizdas bus įtrauktas į konkursą \"Wiki Loves Monuments\" (\"Wiki\" mėgsta paminklus) Rodyti paminklus Vyksta Viki myli paminklus mėnuo! @@ -626,7 +664,7 @@ Netoliese žemėlapiai turi perskaityti TELENFONO BŪSENĄ, kad tinkamai funkcionuotų Naudotojo indėlis: %s Naudotojo pasiekimai: %s - Žiūrėti naudotojo puslapį + Žiūrėti naudotojo puslapį Redaguoti vaizdus Redaguoti kategorijas Išplėstiniai nustatymai @@ -652,6 +690,8 @@ Jūsų atsiliepimas Pažymėti kaip neskirtą įkėlimui Panaikinkite žymėjimą kaip neskirto įkėlimui + Žymima kaip neįkėlimui + Naikinamas žymėjimas kaip neįkėlimui Rodyti jau padarytas nuotraukas Slepiamos jau padarytos nuotraukos Daugiau paveikslėlių nerasta @@ -660,6 +700,8 @@ Paveiklėlis pasirinktas Paveikslėlis pažymėtas kaip neskirtas įkėlimui Pranešti + Nustatyti baltą foną + Nustatyti juodą foną Pranešti apie pažeidimą Pranešti apie šį nauodotoją Pranešti apie šį turinį @@ -669,4 +711,44 @@ Norėdami atlikti šiuos veiksmus, braukite greitai ir ilgai: \n- Kairėn/dešinėn: Pereikite prie ankstesnio/kito\n- Į viršų: Pasirinkite\n- Žemyn: Pažymėti kaip neskirtą įkėlimui. Norėdami nustatyti pirmaujančiųjų sąrašo avatarą, bet kurio vaizdo trijų taškų meniu palieskite „Nustatyti kaip avatarą“. Koordinatės nėra tikslios koordinatės, bet asmuo, kuris įkėlė šią nuotrauką, mano, kad jos yra pakankamai arti. + Saugyklos leidimai atmesti + Nepavyko bendrinti šio elemento + Funkcionalumui reikalingi leidimai + Sužinokite, kaip parašyti naudingą aprašymą + Sužinokite, kaip parašyti naudingą antraštę + Pamatykite savo pasiekimus + Redaguoti paveikslėlį + Redaguoti vietą + Vieta atnaujinta! + Pašalinti vietą + Pašalinti vietos įspėjimą + Vieta daro nuotraukas naudingesnes ir lengviau randamas. Ar tikrai norite pašalinti vietą iš šios nuotraukos? + Vieta pašalinta! + Padėkoti autoriui + Klaida siunčiant padėką autoriui. + Jūsų prisijungimo sesija baigėsi. Prašome prisijungti dar kartą. + Nėra jokios programos GPX failams atidaryti + Failas sėkmingai išsaugotas + Ar norite atidaryti GPX failą? + Ar norite atidaryti KML failą? + Nepavyko išsaugoti KML failo. + Nepavyko išsaugoti GPX failo. + Išsaugomas KML failas + Išsaugomas GPX failas + Atminkite, kad visi paveikslėliai, įkeliant kelis, turi tas pačias kategorijas ir vaizdus. Jei paveikslėliuose vaizdai ir kategorijos skiriasi, prašome atlikti kelis atskirus įkėlimus. + Pastaba apie kelis įkėlimus + Praneškite apie problemą dėl šio elemento Vikiduomenims. + Įveskite keletą komentarų + Aptarimas + Parašykite ką nors apie \'%1$s\' elementą. Tai bus matoma viešai. + \'%1$s\' nebeegzistuoja, niekada nebegalima jo nufotografuoti. + \'%1$s\' yra kitoje vietoje. Toliau nurodykite teisingą vietą ir, jei įmanoma, parašykite teisingą platumą ir ilgumą. + Kita problema arba informacija (paaiškinkite toliau). + Jūsų atsiliepimai bus paskelbti šiame viki puslapyje: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile App/Feedback</a> + Ar tikrai norite atšaukti visus įkėlimus? + Atšaukiami visi įkėlimai... + Įkėlimai + Laukiama + Nepavyko + Nepavyko įkelti vietos duomenų diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index aeb8eab94..bf971f6bc 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -496,6 +496,7 @@ U heeft geen ongelezen meldingen U heeft geen gelezen meldingen Logboeken delen via + Bekijk uw e-mailinbox Bekijk gelezen Ongelezen bekijken Er is een fout opgetreden bij het kiezen van afbeeldingen diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index d4282fd95..48713104d 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -86,7 +86,7 @@ ਆਪਣੀਆਂ ਤਸਵੀਰਾਂ ਨੂੰ ਵਿਕੀਮੀਡੀਆ ਕਾਮਨਜ਼ ਵਿਚ ਜ਼ਿਆਦਾ ਲੱਭਣਯੋਗ ਬਣਾਉਣ ਲਈ ਸ਼੍ਰੇਣੀਆਂ ਜੋੜੋ।\n\nਸ਼੍ਰੇਣੀਆਂ ਜੋੜਨ ਲਈ ਟਾਈਪ ਕਰਨ ਅਰੰਭ ਕਰੋ।\nਇਸ ਕਾਰਜ ਨੂੰ ਅਣਡਿੱਠਾ ਕਰਨ ਲਈ ਇਹ ਸੁਨੇਹਾ ਥਪੇੜੋ (ਜਾਂ ਵਾਪਸੀ ਬਟਨ ਦਬਾਓ)। ਸ਼੍ਰੇਣੀਆਂ ਪਸੰਦਾਂ - ਸਾਈਨ ਅੱਪ + ਖਾਤਾ ਬਣਾਓ ਸ਼੍ਰੇਣੀ ਇਸ ਬਾਰੇ ਅਜ਼ਾਦ ਸਰੋਤ ਸਾਫ਼ਟਵੇਅਰ ਨੂੰ <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache License v2</a> ਅਧੀਨ ਜਾਰੀ ਕੀਤਾ ਗਿਆ ਹੈ diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index 788a34a4b..9c257c273 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -473,6 +473,7 @@ A l\'ha gnun-e notìfiche nen lesùe A l\'ha gnun-e notìfiche lesùe Partagé j\'argistr dovrand + Ch\'a contròla soa casela ëd pòsta eletrònica Vëdde lòn ch\'a l\'é stàit lesù Vëdde lòn ch\'a l\'é ancor nen ëstàit lesù A-i é staje n\'eror an selessionand le plance diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ca771ea98..b15d77787 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -530,6 +530,7 @@ У вас нет непрочитанных уведомлений Нет прочитанных уведомлений Поделиться лог-файлами + Проверьте свой почтовый ящик Просмотр прочитанного См. непрочитанные Произошла ошибка при загрузке изображений diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index ab203b841..d7c023e6f 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -213,7 +213,7 @@ 維基數據項目 維基百科條目 請盡可能描述媒體內容:拍攝於何處?是顯示什麼事物?有什麼脈絡?請描述對象或人物。透露出一些較不易猜測的訊息,例如是風景的話,可以是一天裡的時間。如果媒體顯示出了一些不尋常的事物,請說明不尋常原因。 + 請查看你的電子郵件信箱 學習如何編寫有用的描述 學習如何編寫有用的標題 - 請查看你的電子郵件信箱 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 59dfdd57f..e54fb9d76 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -496,6 +496,7 @@ 您有尚未讀取的通知 您沒有已讀的通知 分享日誌使用 + 請查看你的電子郵件信箱 檢視已讀 檢視未讀 選擇圖片時發生錯誤 @@ -805,5 +806,4 @@ 待處理 失敗 無法載入地點資料 - 請查看你的電子郵件信箱 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index e66f40879..74ff641c0 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -526,6 +526,7 @@ 您没有任何未读通知 您没有已读通知 分享日志于 + 请查看你的电子邮箱 查看已读 查看未读 选择图片时出错 @@ -834,5 +835,4 @@ 待处理 失败 无法加载地点数据 - 请查看你的电子邮箱 From 7b0b604834ddf2ebcfe5d23d60f5bc7d3e91446e Mon Sep 17 00:00:00 2001 From: myyyy <62922278+lzhan0121@users.noreply.github.com> Date: Tue, 22 Oct 2024 00:41:32 +1100 Subject: [PATCH 03/74] Fix: Prevent RecyclerView from resetting scroll position after returning from preview (#5873) (#5880) Resolved an issue where RecyclerView would incorrectly scroll to the top after exiting fullscreen preview. Adjusted scroll behavior to maintain position unless actioned images are filtered. Implemented observer logic adjustments to handle dataset updates more efficiently. Co-authored-by: Nicolas Raoul --- .../commons/customselector/ui/selector/ImageFragment.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index efe640e82..dbab629ff 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -285,9 +285,11 @@ class ImageFragment : imageAdapter.notifyDataSetChanged() selectorRV?.let { it.visibility = View.VISIBLE - lastItemId?.let { pos -> - (it.layoutManager as GridLayoutManager) - .scrollToPosition(ImageHelper.getIndexFromId(filteredImages, pos)) + if (switch?.isChecked == false) { + lastItemId?.let { pos -> + (it.layoutManager as GridLayoutManager) + .scrollToPosition(ImageHelper.getIndexFromId(filteredImages, pos)) + } } } } else { From ba7348f83f68ec52fc93e22086b04bc57ee18d28 Mon Sep 17 00:00:00 2001 From: Hanna Truong Date: Tue, 22 Oct 2024 23:27:40 +1100 Subject: [PATCH 04/74] Fixes issue #5841: Nearby pins: Make it easier to understand what the colors mean (#5881) * UI design for legend to explain the colors of the nearby pins * Add listener for the button to toggle the visibility of the legend (make it hideable) * Change wording for legend and make text localizable * Fixed typo * Fixed typo --------- Co-authored-by: Nicolas Raoul --- .../fragments/NearbyParentFragment.java | 15 ++++ .../res/layout/fragment_nearby_parent.xml | 27 +++++++ app/src/main/res/layout/nearby_legend.xml | 74 +++++++++++++++++++ app/src/main/res/values/strings.xml | 3 + 4 files changed, 119 insertions(+) create mode 100644 app/src/main/res/layout/nearby_legend.xml diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index 8a3c0c330..f578afc25 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -43,6 +43,7 @@ import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.widget.Button; import android.widget.Toast; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; @@ -52,6 +53,7 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog.Builder; +import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.recyclerview.widget.DividerItemDecoration; @@ -218,6 +220,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment private LatLng updatedLatLng; private boolean searchable; + private ConstraintLayout nearbyLegend; + private GridLayoutManager gridLayoutManager; private List dataList; private BottomSheetAdapter bottomSheetAdapter; @@ -302,6 +306,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment progressDialog.setCancelable(false); progressDialog.setMessage("Saving in progress..."); setHasOptionsMenu(true); + // Inflate the layout for this fragment return view; @@ -362,6 +367,16 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment } locationPermissionsHelper = new LocationPermissionsHelper(getActivity(), locationManager, this); + + // Set up the floating activity button to toggle the visibility of the legend + binding.fabLegend.setOnClickListener(v -> { + if (binding.nearbyLegendLayout.getRoot().getVisibility() == View.VISIBLE) { + binding.nearbyLegendLayout.getRoot().setVisibility(View.GONE); + } else { + binding.nearbyLegendLayout.getRoot().setVisibility(View.VISIBLE); + } + }); + presenter.attachView(this); isPermissionDenied = false; recenterToUserLocation = false; diff --git a/app/src/main/res/layout/fragment_nearby_parent.xml b/app/src/main/res/layout/fragment_nearby_parent.xml index e5002fe11..e1d82e6e7 100644 --- a/app/src/main/res/layout/fragment_nearby_parent.xml +++ b/app/src/main/res/layout/fragment_nearby_parent.xml @@ -124,6 +124,33 @@ app:srcCompat="@drawable/ic_my_location_black_24dp" app:useCompatPadding="true" /> + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b5c2dc529..f9de8e051 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -832,4 +832,7 @@ Upload your first media by tapping on the add button. Pending Failed Could not load place data + This place has no picture yet, go take one! + This place has a picture already. + Now checking whether this place has a picture. From 9c1c95f5cfe8ca34130dbeaf675f3c1e5ffbfb3c Mon Sep 17 00:00:00 2001 From: whe128 <165463363+whe128@users.noreply.github.com> Date: Wed, 23 Oct 2024 03:56:02 +1100 Subject: [PATCH 05/74] Fix for #5846: After uploading via Nearby, I am sent back to Nearby, where I am mislead into thinking that I must upload again #5846 (#5874) Co-authored-by: Nicolas Raoul --- .../java/fr/free/nrw/commons/upload/UploadActivity.java | 8 ++++++++ .../java/fr/free/nrw/commons/upload/UploadContract.java | 7 +++++++ .../java/fr/free/nrw/commons/upload/UploadPresenter.java | 7 +++++++ 3 files changed, 22 insertions(+) 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 eb180ec44..23b187340 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 @@ -320,6 +320,14 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, finish(); } + /** + * go to the uploadProgress activity to check the status of uploading + */ + @Override + public void goToUploadProgressActivity() { + startActivity(new Intent(this, UploadProgressActivity.class)); + } + /** * Show/Hide the progress dialog */ 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 index 4df778746..5f41a17c9 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.java @@ -18,6 +18,13 @@ public interface UploadContract { void returnToMainActivity(); + /** + * When submission successful, go to the loadProgressActivity to hint the user this + * submission is valid. And the user will see the upload progress in this activity; + * Fixes: Issue + */ + void goToUploadProgressActivity(); + void askUserToLogIn(); /** 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 144859432..093412c25 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 @@ -123,6 +123,9 @@ public class UploadPresenter implements UploadContract.UserActionListener { view.returnToMainActivity(); compositeDisposable.clear(); Timber.e("failed to upload: " + e.getMessage()); + + //is submission error, not need to go to the uploadActivity + //not start the uploading progress } @Override @@ -131,6 +134,10 @@ public class UploadPresenter implements UploadContract.UserActionListener { repository.cleanup(); view.returnToMainActivity(); compositeDisposable.clear(); + + //after finish the uploadActivity, if successful, + //directly go to the upload progress activity + view.goToUploadProgressActivity(); } }); } else { From f1205c19be7dccefc2b65f4d4c95bfb36c06c23c Mon Sep 17 00:00:00 2001 From: cambo14 <67790050+cambo14@users.noreply.github.com> Date: Wed, 23 Oct 2024 04:50:37 +1100 Subject: [PATCH 06/74] UploadMediaDetailAdapter: made selecting a language deselect all others (#5883) >> Made it so that selecting a language results in the hashmap storing the currently selected language(s) being cleared. Considered refactoring the hashmap storing this into a single pair storing the language positition index and its code, as only one language should ever be selected, however I am not confident that this would not introduce unintended side-effects --- .../fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java index a4e0d1029..ecddab43d 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java @@ -407,7 +407,7 @@ public class UploadMediaDetailAdapter extends recentLanguagesDao .addRecentLanguage(new Language(languageName, languageCode)); - selectedLanguages.remove(position); + selectedLanguages.clear(); selectedLanguages.put(position, languageCode); ((LanguagesAdapter) adapterView .getAdapter()).setSelectedLangCode(languageCode); @@ -497,7 +497,7 @@ public class UploadMediaDetailAdapter extends } recentLanguagesDao.addRecentLanguage(new Language(languageName, languageCode)); - selectedLanguages.remove(position); + selectedLanguages.clear(); selectedLanguages.put(position, languageCode); ((RecentLanguagesAdapter) adapterView .getAdapter()).setSelectedLangCode(languageCode); From 1e7aabad16ceda96f410a21cebbcda7c387dff81 Mon Sep 17 00:00:00 2001 From: Parneet Singh <111801812+parneet-guraya@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:28:39 +0530 Subject: [PATCH 07/74] Use new result API (#5875) * remove unused result expectancy for settings screen launch Signed-off-by: parneet-guraya * initial refactor to new result api, wip Signed-off-by: parneet-guraya * refactor camera launcher Signed-off-by: parneet-guraya * revert callback for video handling Signed-off-by: parneet-guraya * invoke callbacks when cancelled Signed-off-by: parneet-guraya * handle gallery picker result based on preference Signed-off-by: parneet-guraya * remove old method of refactoring for file picker Signed-off-by: parneet-guraya * remove legacy result handling callback Signed-off-by: parneet-guraya * request code used for handling result was never used for launching an activity, hence removed Signed-off-by: parneet-guraya * extract voice result handling into function Signed-off-by: parneet-guraya * refactor test Signed-off-by: parneet-guraya * remove unused tests Signed-off-by: parneet-guraya * cleanup Signed-off-by: parneet-guraya * fix-docs Signed-off-by: parneet-guraya * add space after , Signed-off-by: parneet-guraya --------- Signed-off-by: parneet-guraya --- .../free/nrw/commons/CommonsApplication.java | 1 - .../locations/BookmarkLocationsFragment.java | 29 ++- .../contributions/ContributionController.java | 85 +++++--- .../ContributionsListFragment.java | 34 ++- .../commons/contributions/MainActivity.java | 7 - .../ui/selector/CustomSelectorActivity.kt | 21 +- .../description/DescriptionEditActivity.kt | 36 ++-- .../nrw/commons/filepicker/Constants.java | 11 +- .../nrw/commons/filepicker/FilePicker.java | 204 +++++++----------- .../commons/media/MediaDetailFragment.java | 80 ------- .../commons/nearby/PlaceAdapterDelegate.kt | 11 +- .../fragments/CommonPlaceClickActions.kt | 16 +- .../fragments/NearbyParentFragment.java | 37 +++- .../commons/nearby/fragments/PlaceAdapter.kt | 5 + .../commons/settings/SettingsFragment.java | 12 +- .../nrw/commons/upload/UploadActivity.java | 8 - .../upload/UploadMediaDetailAdapter.java | 15 +- .../UploadMediaDetailFragment.java | 94 ++++---- .../nrw/commons/utils/PermissionUtils.java | 3 +- .../contributions/MainActivityUnitTests.kt | 14 -- .../ui/selector/CustomSelectorActivityTest.kt | 16 +- .../nrw/commons/filepicker/FilePickerTest.kt | 94 ++++---- .../media/MediaDetailFragmentUnitTests.kt | 20 +- .../commons/upload/UploadActivityUnitTests.kt | 14 -- .../UploadMediaDetailAdapterUnitTest.kt | 7 +- .../UploadMediaDetailFragmentUnitTest.kt | 20 +- 26 files changed, 407 insertions(+), 487 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index c3dde9caa..3aceb957a 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -107,7 +107,6 @@ public class CommonsApplication extends MultiDexApplication { /** * Constants begin */ - public static final int OPEN_APPLICATION_DETAIL_SETTINGS = 1001; public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using [[COM:MOA|Commons Mobile App]]"; diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java index 65d0e45a8..f5ce556c4 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java @@ -9,6 +9,7 @@ import android.view.ViewGroup; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; @@ -33,6 +34,23 @@ public class BookmarkLocationsFragment extends DaggerFragment { @Inject BookmarkLocationsDao bookmarkLocationDao; @Inject CommonPlaceClickActions commonPlaceClickActions; private PlaceAdapter adapter; + + private final ActivityResultLauncher cameraPickLauncherForResult = + registerForActivityResult(new StartActivityForResult(), + result -> { + contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> { + contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks); + }); + }); + + private final ActivityResultLauncher galleryPickLauncherForResult = + registerForActivityResult(new StartActivityForResult(), + result -> { + contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> { + contributionController.onPictureReturnedFromGallery(result, requireActivity(), callbacks); + }); + }); + private ActivityResultLauncher inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { @Override public void onActivityResult(Map result) { @@ -45,7 +63,7 @@ public class BookmarkLocationsFragment extends DaggerFragment { contributionController.locationPermissionCallback.onLocationPermissionGranted(); } else { if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { - contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher); + contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); } else { contributionController.locationPermissionCallback.onLocationPermissionDenied(getActivity().getString(R.string.in_app_camera_location_permission_denied)); } @@ -83,7 +101,9 @@ public class BookmarkLocationsFragment extends DaggerFragment { return Unit.INSTANCE; }, commonPlaceClickActions, - inAppCameraLocationPermissionLauncher + inAppCameraLocationPermissionLauncher, + galleryPickLauncherForResult, + cameraPickLauncherForResult ); binding.listView.setAdapter(adapter); } @@ -109,11 +129,6 @@ public class BookmarkLocationsFragment extends DaggerFragment { } } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - contributionController.handleActivityResult(getActivity(), requestCode, resultCode, data); - } - @Override public void onDestroy() { super.onDestroy(); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index 1251d1027..fcfd32974 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -7,6 +7,7 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.widget.Toast; +import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; @@ -64,10 +65,11 @@ public class ContributionController { * Check for permissions and initiate camera click */ public void initiateCameraPick(Activity activity, - ActivityResultLauncher inAppCameraLocationPermissionLauncher) { + ActivityResultLauncher inAppCameraLocationPermissionLauncher, + ActivityResultLauncher resultLauncher) { boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true); if (!useExtStorage) { - initiateCameraUpload(activity); + initiateCameraUpload(activity, resultLauncher); return; } @@ -75,12 +77,12 @@ public class ContributionController { () -> { if (defaultKvStore.getBoolean("inAppCameraFirstRun")) { defaultKvStore.putBoolean("inAppCameraFirstRun", false); - askUserToAllowLocationAccess(activity, inAppCameraLocationPermissionLauncher); + askUserToAllowLocationAccess(activity, inAppCameraLocationPermissionLauncher, resultLauncher); } else if (defaultKvStore.getBoolean("inAppCameraLocationPref")) { createDialogsAndHandleLocationPermissions(activity, - inAppCameraLocationPermissionLauncher); + inAppCameraLocationPermissionLauncher, resultLauncher); } else { - initiateCameraUpload(activity); + initiateCameraUpload(activity, resultLauncher); } }, R.string.storage_permission_title, @@ -94,7 +96,8 @@ public class ContributionController { * @param activity */ private void createDialogsAndHandleLocationPermissions(Activity activity, - ActivityResultLauncher inAppCameraLocationPermissionLauncher) { + ActivityResultLauncher inAppCameraLocationPermissionLauncher, + ActivityResultLauncher resultLauncher) { locationPermissionCallback = new LocationPermissionCallback() { @Override public void onLocationPermissionDenied(String toastMessage) { @@ -103,16 +106,16 @@ public class ContributionController { toastMessage, Toast.LENGTH_LONG ).show(); - initiateCameraUpload(activity); + initiateCameraUpload(activity, resultLauncher); } @Override public void onLocationPermissionGranted() { if (!locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { showLocationOffDialog(activity, R.string.in_app_camera_needs_location, - R.string.in_app_camera_location_unavailable); + R.string.in_app_camera_location_unavailable, resultLauncher); } else { - initiateCameraUpload(activity); + initiateCameraUpload(activity, resultLauncher); } } }; @@ -135,9 +138,10 @@ public class ContributionController { * @param activity Activity reference * @param dialogTextResource Resource id of text to be shown in dialog * @param toastTextResource Resource id of text to be shown in toast + * @param resultLauncher */ private void showLocationOffDialog(Activity activity, int dialogTextResource, - int toastTextResource) { + int toastTextResource, ActivityResultLauncher resultLauncher) { DialogUtil .showAlertDialog(activity, activity.getString(R.string.ask_to_turn_location_on), @@ -148,20 +152,21 @@ public class ContributionController { () -> { Toast.makeText(activity, activity.getString(toastTextResource), Toast.LENGTH_LONG).show(); - initiateCameraUpload(activity); + initiateCameraUpload(activity, resultLauncher); } ); } public void handleShowRationaleFlowCameraLocation(Activity activity, - ActivityResultLauncher inAppCameraLocationPermissionLauncher) { + ActivityResultLauncher inAppCameraLocationPermissionLauncher, + ActivityResultLauncher resultLauncher) { DialogUtil.showAlertDialog(activity, activity.getString(R.string.location_permission_title), activity.getString(R.string.in_app_camera_location_permission_rationale), activity.getString(android.R.string.ok), activity.getString(android.R.string.cancel), () -> { createDialogsAndHandleLocationPermissions(activity, - inAppCameraLocationPermissionLauncher); + inAppCameraLocationPermissionLauncher, resultLauncher); }, () -> locationPermissionCallback.onLocationPermissionDenied( activity.getString(R.string.in_app_camera_location_permission_denied)), @@ -181,7 +186,8 @@ public class ContributionController { * @param activity */ private void askUserToAllowLocationAccess(Activity activity, - ActivityResultLauncher inAppCameraLocationPermissionLauncher) { + ActivityResultLauncher inAppCameraLocationPermissionLauncher, + ActivityResultLauncher resultLauncher) { DialogUtil.showAlertDialog(activity, activity.getString(R.string.in_app_camera_location_permission_title), activity.getString(R.string.in_app_camera_location_access_explanation), @@ -190,12 +196,12 @@ public class ContributionController { () -> { defaultKvStore.putBoolean("inAppCameraLocationPref", true); createDialogsAndHandleLocationPermissions(activity, - inAppCameraLocationPermissionLauncher); + inAppCameraLocationPermissionLauncher, resultLauncher); }, () -> { ViewUtil.showLongToast(activity, R.string.in_app_camera_location_permission_denied); defaultKvStore.putBoolean("inAppCameraLocationPref", false); - initiateCameraUpload(activity); + initiateCameraUpload(activity, resultLauncher); }, null, true); @@ -204,18 +210,18 @@ public class ContributionController { /** * Initiate gallery picker */ - public void initiateGalleryPick(final Activity activity, final boolean allowMultipleUploads) { - initiateGalleryUpload(activity, allowMultipleUploads); + public void initiateGalleryPick(final Activity activity, ActivityResultLauncher resultLauncher, final boolean allowMultipleUploads) { + initiateGalleryUpload(activity, resultLauncher, allowMultipleUploads); } /** * Initiate gallery picker with permission */ - public void initiateCustomGalleryPickWithPermission(final Activity activity) { + public void initiateCustomGalleryPickWithPermission(final Activity activity, ActivityResultLauncher resultLauncher) { setPickerConfiguration(activity, true); PermissionUtils.checkPermissionsAndPerformAction(activity, - () -> FilePicker.openCustomSelector(activity, 0), + () -> FilePicker.openCustomSelector(activity, resultLauncher, 0), R.string.storage_permission_title, R.string.write_storage_permission_rationale, PermissionUtils.PERMISSIONS_STORAGE); @@ -225,12 +231,10 @@ public class ContributionController { /** * Open chooser for gallery uploads */ - private void initiateGalleryUpload(final Activity activity, + private void initiateGalleryUpload(final Activity activity, ActivityResultLauncher resultLauncher, final boolean allowMultipleUploads) { setPickerConfiguration(activity, allowMultipleUploads); - boolean openDocumentIntentPreferred = defaultKvStore.getBoolean( - "openDocumentPhotoPickerPref", true); - FilePicker.openGallery(activity, 0, openDocumentIntentPreferred); + FilePicker.openGallery(activity, resultLauncher, 0, isDocumentPhotoPickerPreferred()); } /** @@ -247,22 +251,43 @@ public class ContributionController { /** * Initiate camera upload by opening camera */ - private void initiateCameraUpload(Activity activity) { + private void initiateCameraUpload(Activity activity, ActivityResultLauncher resultLauncher) { setPickerConfiguration(activity, false); if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) { locationBeforeImageCapture = locationManager.getLastLocation(); } isInAppCameraUpload = true; - FilePicker.openCameraForImage(activity, 0); + FilePicker.openCameraForImage(activity, resultLauncher, 0); + } + + private boolean isDocumentPhotoPickerPreferred(){ + return defaultKvStore.getBoolean( + "openDocumentPhotoPickerPref", true); + } + + public void onPictureReturnedFromGallery(ActivityResult result, Activity activity, FilePicker.Callbacks callbacks){ + + if(isDocumentPhotoPickerPreferred()){ + FilePicker.onPictureReturnedFromDocuments(result, activity, callbacks); + } else { + FilePicker.onPictureReturnedFromGallery(result, activity, callbacks); + } + } + + public void onPictureReturnedFromCustomSelector(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { + FilePicker.onPictureReturnedFromCustomSelector(result, activity, callbacks); + } + + public void onPictureReturnedFromCamera(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { + FilePicker.onPictureReturnedFromCamera(result, activity, callbacks); } /** * Attaches callback for file picker. */ - public void handleActivityResult(Activity activity, int requestCode, int resultCode, - Intent data) { - FilePicker.handleActivityResult(requestCode, resultCode, data, activity, - new DefaultCallback() { + public void handleActivityResultWithCallback(Activity activity, FilePicker.HandleActivityResult handleActivityResult) { + + handleActivityResult.onHandleActivityResult(new DefaultCallback() { @Override public void onCanceled(final ImageSource source, final int type) { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index 53c91534e..509d1eb95 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -6,6 +6,7 @@ import static fr.free.nrw.commons.di.NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_ import android.Manifest.permission; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; @@ -20,6 +21,7 @@ import android.widget.LinearLayout; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions; +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -96,6 +98,30 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl private int contributionsSize; private String userName; + private final ActivityResultLauncher galleryPickLauncherForResult = + registerForActivityResult(new StartActivityForResult(), + result -> { + controller.handleActivityResultWithCallback(requireActivity(), callbacks -> { + controller.onPictureReturnedFromGallery(result, requireActivity(), callbacks); + }); + }); + + private final ActivityResultLauncher customSelectorLauncherForResult = + registerForActivityResult(new StartActivityForResult(), + result -> { + controller.handleActivityResultWithCallback(requireActivity(), callbacks -> { + controller.onPictureReturnedFromCustomSelector(result, requireActivity(), callbacks); + }); + }); + + private final ActivityResultLauncher cameraPickLauncherForResult = + registerForActivityResult(new StartActivityForResult(), + result -> { + controller.handleActivityResultWithCallback(requireActivity(), callbacks -> { + controller.onPictureReturnedFromCamera(result, requireActivity(), callbacks); + }); + }); + private ActivityResultLauncher inAppCameraLocationPermissionLauncher = registerForActivityResult( new RequestMultiplePermissions(), new ActivityResultCallback>() { @@ -111,7 +137,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl } else { if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { controller.handleShowRationaleFlowCameraLocation(getActivity(), - inAppCameraLocationPermissionLauncher); + inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); } else { controller.locationPermissionCallback.onLocationPermissionDenied( getActivity().getString( @@ -322,7 +348,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl private void setListeners() { binding.fabPlus.setOnClickListener(view -> animateFAB(isFabOpen)); binding.fabCamera.setOnClickListener(view -> { - controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher); + controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); animateFAB(isFabOpen); }); binding.fabCamera.setOnLongClickListener(view -> { @@ -330,7 +356,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl return true; }); binding.fabGallery.setOnClickListener(view -> { - controller.initiateGalleryPick(getActivity(), true); + controller.initiateGalleryPick(getActivity(), galleryPickLauncherForResult, true); animateFAB(isFabOpen); }); binding.fabGallery.setOnLongClickListener(view -> { @@ -343,7 +369,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl * Launch Custom Selector. */ protected void launchCustomSelector() { - controller.initiateCustomGalleryPickWithPermission(getActivity()); + controller.initiateCustomGalleryPickWithPermission(getActivity(), customSelectorLauncherForResult); animateFAB(isFabOpen); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 54d3e9681..a9e9ee5c6 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -438,13 +438,6 @@ public class MainActivity extends BaseActivity }); } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - Timber.d(data != null ? data.toString() : "onActivityResult data is null"); - super.onActivityResult(requestCode, resultCode, data); - controller.handleActivityResult(this, requestCode, resultCode, data); - } - @Override protected void onResume() { super.onResume(); diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index 3bba6f05c..959db52f3 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -13,6 +13,8 @@ import android.view.Window import android.widget.Button import android.widget.ImageButton import android.widget.TextView +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -146,6 +148,10 @@ class CustomSelectorActivity : private var showPartialAccessIndicator by mutableStateOf(false) + private val startForResult = registerForActivityResult(StartActivityForResult()){ result -> + onFullScreenDataReceived(result) + } + /** * onCreate Activity, sets theme, initialises the view model, setup view. */ @@ -224,17 +230,10 @@ class CustomSelectorActivity : /** * When data will be send from full screen mode, it will be passed to fragment */ - override fun onActivityResult( - requestCode: Int, - resultCode: Int, - data: Intent?, - ) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE && - resultCode == Activity.RESULT_OK - ) { + private fun onFullScreenDataReceived(result: ActivityResult){ + if (result.resultCode == Activity.RESULT_OK) { val selectedImages: ArrayList = - data!! + result.data!! .getParcelableArrayListExtra(CustomSelectorConstants.NEW_SELECTED_IMAGES)!! viewModel?.selectedImages?.value = selectedImages } @@ -509,7 +508,7 @@ class CustomSelectorActivity : selectedImages, ) intent.putExtra(CustomSelectorConstants.BUCKET_ID, bucketId) - startActivityForResult(intent, Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE) + startForResult.launch(intent) } /** diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt index 0dbdf71ae..cfd7f36b9 100644 --- a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt @@ -6,6 +6,8 @@ import android.os.Bundle import android.os.Parcelable import android.speech.RecognizerIntent import android.view.View +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import fr.free.nrw.commons.CommonsApplication @@ -70,10 +72,14 @@ class DescriptionEditActivity : private lateinit var binding: ActivityDescriptionEditBinding - private val requestCodeForVoiceInput = 1213 - private var descriptionAndCaptions: ArrayList? = null + private val voiceInputResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result: ActivityResult -> + onVoiceInput(result) + } + @Inject lateinit var descriptionEditHelper: DescriptionEditHelper @Inject lateinit var sessionManager: SessionManager @@ -115,6 +121,7 @@ class DescriptionEditActivity : savedLanguageValue, descriptionAndCaptions, recentLanguagesDao, + voiceInputResultLauncher ) uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int -> showInfoAlert( @@ -149,6 +156,15 @@ class DescriptionEditActivity : override fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) {} + private fun onVoiceInput(result: ActivityResult) { + if (result.resultCode == RESULT_OK && result.data != null) { + val resultData = result.data!!.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) + uploadMediaDetailAdapter.handleSpeechResult(resultData!![0]) + } else { + Timber.e("Error %s", result.resultCode) + } + } + /** * Adds new language item to RecyclerView */ @@ -292,22 +308,6 @@ class DescriptionEditActivity : progressDialog!!.show() } - override fun onActivityResult( - requestCode: Int, - resultCode: Int, - data: Intent?, - ) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == requestCodeForVoiceInput) { - if (resultCode == RESULT_OK && data != null) { - val result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) - uploadMediaDetailAdapter.handleSpeechResult(result!![0]) - } else { - Timber.e("Error %s", resultCode) - } - } - } - override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java index f907f0a01..4a5823252 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java @@ -4,20 +4,11 @@ public interface Constants { String DEFAULT_FOLDER_NAME = "CommonsContributions"; /** - * Provides the request codes utilised by the FilePicker + * Provides the request codes for permission handling */ interface RequestCodes { int LOCATION = 1; int STORAGE = 2; - int FILE_PICKER_IMAGE_IDENTIFICATOR = 0b1101101100; //876 - int SOURCE_CHOOSER = 1 << 15; - - int PICK_PICTURE_FROM_CUSTOM_SELECTOR = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 10); - int PICK_PICTURE_FROM_DOCUMENTS = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 11); - int PICK_PICTURE_FROM_GALLERY = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 12); - int TAKE_PICTURE = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 13); - - int RECEIVE_DATA_FROM_FULL_SCREEN_MODE = 1 << 9; } /** diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java index daa29276a..b64db24c5 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java @@ -12,6 +12,8 @@ import android.content.pm.ResolveInfo; import android.net.Uri; import android.provider.MediaStore; import android.text.TextUtils; +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; @@ -107,31 +109,25 @@ public class FilePicker implements Constants { * * @param type Custom type of your choice, which will be returned with the images */ - public static void openGallery(Activity activity, int type, boolean openDocumentIntentPreferred) { + public static void openGallery(Activity activity, ActivityResultLauncher resultLauncher, int type, boolean openDocumentIntentPreferred) { Intent intent = createGalleryIntent(activity, type, openDocumentIntentPreferred); - int requestCode = RequestCodes.PICK_PICTURE_FROM_GALLERY; - - if(openDocumentIntentPreferred){ - requestCode = RequestCodes.PICK_PICTURE_FROM_DOCUMENTS; - } - - activity.startActivityForResult(intent, requestCode); + resultLauncher.launch(intent); } /** * Opens Custom Selector */ - public static void openCustomSelector(Activity activity, int type) { + public static void openCustomSelector(Activity activity, ActivityResultLauncher resultLauncher, int type) { Intent intent = createCustomSelectorIntent(activity, type); - activity.startActivityForResult(intent, RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR); + resultLauncher.launch(intent); } /** * Opens the camera app to pick image clicked by user */ - public static void openCameraForImage(Activity activity, int type) { + public static void openCameraForImage(Activity activity, ActivityResultLauncher resultLauncher, int type) { Intent intent = createCameraForImageIntent(activity, type); - activity.startActivityForResult(intent, RequestCodes.TAKE_PICTURE); + resultLauncher.launch(intent); } @Nullable @@ -154,43 +150,6 @@ public class FilePicker implements Constants { } } - /** - * Any activity can use this method to attach their callback to the file picker - */ - public static void handleActivityResult(int requestCode, int resultCode, Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { - boolean isHandledPickedFile = (requestCode & RequestCodes.FILE_PICKER_IMAGE_IDENTIFICATOR) > 0; - if (isHandledPickedFile) { - requestCode &= ~RequestCodes.SOURCE_CHOOSER; - if (requestCode == RequestCodes.PICK_PICTURE_FROM_GALLERY || - requestCode == RequestCodes.TAKE_PICTURE || - requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS || - requestCode == RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR) { - if (resultCode == Activity.RESULT_OK) { - if (requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS && !isPhoto(data)) { - onPictureReturnedFromDocuments(data, activity, callbacks); - } else if (requestCode == RequestCodes.PICK_PICTURE_FROM_GALLERY && !isPhoto(data)) { - onPictureReturnedFromGallery(data, activity, callbacks); - } else if (requestCode == RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR) { - onPictureReturnedFromCustomSelector(data, activity, callbacks); - } else if (requestCode == RequestCodes.TAKE_PICTURE) { - onPictureReturnedFromCamera(activity, callbacks); - } - } else { - if (requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS) { - callbacks.onCanceled(FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); - } else if (requestCode == RequestCodes.PICK_PICTURE_FROM_GALLERY) { - callbacks.onCanceled(FilePicker.ImageSource.GALLERY, restoreType(activity)); - } else if (requestCode == RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR){ - callbacks.onCanceled(ImageSource.CUSTOM_SELECTOR, restoreType(activity)); - } - else { - callbacks.onCanceled(FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); - } - } - } - } - } - public static List handleExternalImagesPicked(Intent data, Activity activity) { try { return getFilesFromGalleryPictures(data, activity); @@ -243,18 +202,22 @@ public class FilePicker implements Constants { return intent; } - private static void onPictureReturnedFromDocuments(Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { - try { - Uri photoPath = data.getData(); - UploadableFile photoFile = PickedFiles.pickedExistingPicture(activity, photoPath); - callbacks.onImagesPicked(singleFileList(photoFile), FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); + public static void onPictureReturnedFromDocuments(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { + if(result.getResultCode() == Activity.RESULT_OK && !isPhoto(result.getData())){ + try { + Uri photoPath = result.getData().getData(); + UploadableFile photoFile = PickedFiles.pickedExistingPicture(activity, photoPath); + callbacks.onImagesPicked(singleFileList(photoFile), FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); - if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { - PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); + if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { + PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); + } + } catch (Exception e) { + e.printStackTrace(); + callbacks.onImagePickerError(e, FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); } - } catch (Exception e) { - e.printStackTrace(); - callbacks.onImagePickerError(e, FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); + } else { + callbacks.onCanceled(FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); } } @@ -262,14 +225,18 @@ public class FilePicker implements Constants { * onPictureReturnedFromCustomSelector. * Retrieve and forward the images to upload wizard through callback. */ - private static void onPictureReturnedFromCustomSelector(Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { - try { - List files = getFilesFromCustomSelector(data, activity); - callbacks.onImagesPicked(files, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); - } catch (Exception e) { - e.printStackTrace(); - callbacks.onImagePickerError(e, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); - } + public static void onPictureReturnedFromCustomSelector(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { + if(result.getResultCode() == Activity.RESULT_OK){ + try { + List files = getFilesFromCustomSelector(result.getData(), activity); + callbacks.onImagesPicked(files, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); + } catch (Exception e) { + e.printStackTrace(); + callbacks.onImagePickerError(e, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); + } + } else { + callbacks.onCanceled(ImageSource.CUSTOM_SELECTOR, restoreType(activity)); + } } /** @@ -292,13 +259,17 @@ public class FilePicker implements Constants { return files; } - private static void onPictureReturnedFromGallery(Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { - try { - List files = getFilesFromGalleryPictures(data, activity); - callbacks.onImagesPicked(files, FilePicker.ImageSource.GALLERY, restoreType(activity)); - } catch (Exception e) { - e.printStackTrace(); - callbacks.onImagePickerError(e, FilePicker.ImageSource.GALLERY, restoreType(activity)); + public static void onPictureReturnedFromGallery(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { + if(result.getResultCode() == Activity.RESULT_OK && !isPhoto(result.getData())){ + try { + List files = getFilesFromGalleryPictures(result.getData(), activity); + callbacks.onImagesPicked(files, FilePicker.ImageSource.GALLERY, restoreType(activity)); + } catch (Exception e) { + e.printStackTrace(); + callbacks.onImagePickerError(e, FilePicker.ImageSource.GALLERY, restoreType(activity)); + } + } else{ + callbacks.onCanceled(FilePicker.ImageSource.GALLERY, restoreType(activity)); } } @@ -324,69 +295,40 @@ public class FilePicker implements Constants { return files; } - private static void onPictureReturnedFromCamera(Activity activity, @NonNull FilePicker.Callbacks callbacks) { - try { - String lastImageUri = PreferenceManager.getDefaultSharedPreferences(activity).getString(KEY_PHOTO_URI, null); - if (!TextUtils.isEmpty(lastImageUri)) { - revokeWritePermission(activity, Uri.parse(lastImageUri)); - } - - UploadableFile photoFile = FilePicker.takenCameraPicture(activity); - List files = new ArrayList<>(); - files.add(photoFile); - - if (photoFile == null) { - Exception e = new IllegalStateException("Unable to get the picture returned from camera"); - callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); - } else { - if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) { - PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); + public static void onPictureReturnedFromCamera(ActivityResult activityResult, Activity activity, @NonNull FilePicker.Callbacks callbacks) { + if(activityResult.getResultCode() == Activity.RESULT_OK){ + try { + String lastImageUri = PreferenceManager.getDefaultSharedPreferences(activity).getString(KEY_PHOTO_URI, null); + if (!TextUtils.isEmpty(lastImageUri)) { + revokeWritePermission(activity, Uri.parse(lastImageUri)); } - callbacks.onImagesPicked(files, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); - } + UploadableFile photoFile = FilePicker.takenCameraPicture(activity); + List files = new ArrayList<>(); + files.add(photoFile); - PreferenceManager.getDefaultSharedPreferences(activity) + if (photoFile == null) { + Exception e = new IllegalStateException("Unable to get the picture returned from camera"); + callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); + } else { + if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) { + PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); + } + + callbacks.onImagesPicked(files, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); + } + + PreferenceManager.getDefaultSharedPreferences(activity) .edit() .remove(KEY_LAST_CAMERA_PHOTO) .remove(KEY_PHOTO_URI) .apply(); - } catch (Exception e) { - e.printStackTrace(); - callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); - } - } - - private static void onVideoReturnedFromCamera(Activity activity, @NonNull FilePicker.Callbacks callbacks) { - try { - String lastVideoUri = PreferenceManager.getDefaultSharedPreferences(activity).getString(KEY_VIDEO_URI, null); - if (!TextUtils.isEmpty(lastVideoUri)) { - revokeWritePermission(activity, Uri.parse(lastVideoUri)); + } catch (Exception e) { + e.printStackTrace(); + callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); } - - UploadableFile photoFile = FilePicker.takenCameraVideo(activity); - List files = new ArrayList<>(); - files.add(photoFile); - - if (photoFile == null) { - Exception e = new IllegalStateException("Unable to get the video returned from camera"); - callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_VIDEO, restoreType(activity)); - } else { - if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) { - PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); - } - - callbacks.onImagesPicked(files, FilePicker.ImageSource.CAMERA_VIDEO, restoreType(activity)); - } - - PreferenceManager.getDefaultSharedPreferences(activity) - .edit() - .remove(KEY_LAST_CAMERA_VIDEO) - .remove(KEY_VIDEO_URI) - .apply(); - } catch (Exception e) { - e.printStackTrace(); - callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_VIDEO, restoreType(activity)); + } else { + callbacks.onCanceled(FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); } } @@ -406,4 +348,8 @@ public class FilePicker implements Constants { void onCanceled(FilePicker.ImageSource source, int type); } + + public interface HandleActivityResult{ + void onHandleActivityResult(FilePicker.Callbacks callbacks); + } } 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 7336c1b40..dd0829a1b 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 @@ -1,13 +1,10 @@ package fr.free.nrw.commons.media; -import static android.app.Activity.RESULT_CANCELED; -import static android.app.Activity.RESULT_OK; import static android.view.View.GONE; import static android.view.View.VISIBLE; import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_NEEDING_CATEGORIES; import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_UNCATEGORISED; import static fr.free.nrw.commons.description.EditDescriptionConstants.LIST_OF_DESCRIPTION_AND_CAPTION; -import static fr.free.nrw.commons.description.EditDescriptionConstants.UPDATED_WIKITEXT; import static fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT; import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION; import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources; @@ -112,8 +109,6 @@ import timber.log.Timber; public class MediaDetailFragment extends CommonsDaggerSupportFragment implements CategoryEditHelper.Callback { - private static final int REQUEST_CODE = 1001; - private static final int REQUEST_CODE_EDIT_DESCRIPTION = 1002; private static final String IMAGE_BACKGROUND_COLOR = "image_background_color"; static final int DEFAULT_IMAGE_BACKGROUND_COLOR = 0; @@ -1065,81 +1060,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements return captionList; } - /** - * Get the result from another activity and act accordingly. - * @param requestCode - * @param resultCode - * @param data - */ - @Override - public void onActivityResult(final int requestCode, final int resultCode, - @Nullable final Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (requestCode == REQUEST_CODE_EDIT_DESCRIPTION && resultCode == RESULT_OK) { - final String updatedWikiText = data.getStringExtra(UPDATED_WIKITEXT); - - try { - compositeDisposable.add(descriptionEditHelper.addDescription(getContext(), media, - updatedWikiText) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(s -> { - Timber.d("Descriptions are added."); - })); - } catch (Exception e) { - if (e.getLocalizedMessage().equals(CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE)) { - final String username = sessionManager.getUserName(); - final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( - getActivity(), - requireActivity().getString(R.string.invalid_login_message), - username - ); - - CommonsApplication.getInstance().clearApplicationData( - requireActivity(), logoutListener); - } - } - - final ArrayList uploadMediaDetails - = data.getParcelableArrayListExtra(LIST_OF_DESCRIPTION_AND_CAPTION); - - LinkedHashMap updatedCaptions = new LinkedHashMap<>(); - for (UploadMediaDetail mediaDetail: - uploadMediaDetails) { - try { - compositeDisposable.add(descriptionEditHelper.addCaption(getContext(), media, - mediaDetail.getLanguageCode(), mediaDetail.getCaptionText()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(s -> { - updateCaptions(mediaDetail, updatedCaptions); - Timber.d("Caption is added."); - })); - - } catch (Exception e) { - if (e.getLocalizedMessage().equals(CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE)) { - final String username = sessionManager.getUserName(); - final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( - getActivity(), - requireActivity().getString(R.string.invalid_login_message), - username - ); - - CommonsApplication.getInstance().clearApplicationData( - requireActivity(), logoutListener); - } - } - } - binding.progressBarEdit.setVisibility(GONE); - binding.descriptionEdit.setVisibility(VISIBLE); - - } else if (requestCode == REQUEST_CODE_EDIT_DESCRIPTION && resultCode == RESULT_CANCELED) { - binding.progressBarEdit.setVisibility(GONE); - binding.descriptionEdit.setVisibility(VISIBLE); - } - } - /** * Adds caption to the map and updates captions * @param mediaDetail UploadMediaDetail diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceAdapterDelegate.kt b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceAdapterDelegate.kt index 5152ac0f7..a4ea3cd5b 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceAdapterDelegate.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceAdapterDelegate.kt @@ -1,5 +1,6 @@ package fr.free.nrw.commons.nearby +import android.content.Intent import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE @@ -17,9 +18,9 @@ import fr.free.nrw.commons.databinding.ItemPlaceBinding fun placeAdapterDelegate( bookmarkLocationDao: BookmarkLocationsDao, onItemClick: ((Place) -> Unit)? = null, - onCameraClicked: (Place, ActivityResultLauncher>) -> Unit, + onCameraClicked: (Place, ActivityResultLauncher>, ActivityResultLauncher) -> Unit, onCameraLongPressed: () -> Boolean, - onGalleryClicked: (Place) -> Unit, + onGalleryClicked: (Place, ActivityResultLauncher) -> Unit, onGalleryLongPressed: () -> Boolean, onBookmarkClicked: (Place, Boolean) -> Unit, onBookmarkLongPressed: () -> Boolean, @@ -28,6 +29,8 @@ fun placeAdapterDelegate( onDirectionsClicked: (Place) -> Unit, onDirectionsLongPressed: () -> Boolean, inAppCameraLocationPermissionLauncher: ActivityResultLauncher>, + cameraPickLauncherForResult: ActivityResultLauncher, + galleryPickLauncherForResult: ActivityResultLauncher ) = adapterDelegateViewBinding({ layoutInflater, parent -> ItemPlaceBinding.inflate(layoutInflater, parent, false) }) { @@ -44,10 +47,10 @@ fun placeAdapterDelegate( onItemClick?.invoke(item) } } - nearbyButtonLayout.cameraButton.setOnClickListener { onCameraClicked(item, inAppCameraLocationPermissionLauncher) } + nearbyButtonLayout.cameraButton.setOnClickListener { onCameraClicked(item, inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult) } nearbyButtonLayout.cameraButton.setOnLongClickListener { onCameraLongPressed() } - nearbyButtonLayout.galleryButton.setOnClickListener { onGalleryClicked(item) } + nearbyButtonLayout.galleryButton.setOnClickListener { onGalleryClicked(item, galleryPickLauncherForResult) } nearbyButtonLayout.galleryButton.setOnLongClickListener { onGalleryLongPressed() } bookmarkButtonImage.setOnClickListener { val isBookmarked = bookmarkLocationDao.updateBookmarkLocation(item) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/CommonPlaceClickActions.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/CommonPlaceClickActions.kt index f3eecf116..a4d6b14b7 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/CommonPlaceClickActions.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/CommonPlaceClickActions.kt @@ -28,14 +28,14 @@ class CommonPlaceClickActions private val activity: Activity, private val contributionController: ContributionController, ) { - fun onCameraClicked(): (Place, ActivityResultLauncher>) -> Unit = - { place, launcher -> + fun onCameraClicked(): (Place, ActivityResultLauncher>, ActivityResultLauncher) -> Unit = + { place, launcher, resultLauncher -> if (applicationKvStore.getBoolean("login_skipped", false)) { showLoginDialog() } else { Timber.d("Camera button tapped. Image title: ${place.getName()}Image desc: ${place.longDescription}") storeSharedPrefs(place) - contributionController.initiateCameraPick(activity, launcher) + contributionController.initiateCameraPick(activity, launcher, resultLauncher) } } @@ -72,14 +72,14 @@ class CommonPlaceClickActions true } - fun onGalleryClicked(): (Place) -> Unit = - { + fun onGalleryClicked(): (Place, ActivityResultLauncher) -> Unit = + {place, galleryPickLauncherForResult -> if (applicationKvStore.getBoolean("login_skipped", false)) { showLoginDialog() } else { - Timber.d("Gallery button tapped. Image title: ${it.getName()}Image desc: ${it.getLongDescription()}") - storeSharedPrefs(it) - contributionController.initiateGalleryPick(activity, false) + Timber.d("Gallery button tapped. Image title: ${place.getName()}Image desc: ${place.getLongDescription()}") + storeSharedPrefs(place) + contributionController.initiateGalleryPick(activity, galleryPickLauncherForResult, false) } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index f578afc25..7a7d5cdcb 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -49,6 +49,7 @@ import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions; import androidx.activity.result.contract.ActivityResultContracts.RequestPermission; +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -225,6 +226,31 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment private GridLayoutManager gridLayoutManager; private List dataList; private BottomSheetAdapter bottomSheetAdapter; + + private final ActivityResultLauncher galleryPickLauncherForResult = + registerForActivityResult(new StartActivityForResult(), + result -> { + controller.handleActivityResultWithCallback(requireActivity(), callbacks -> { + controller.onPictureReturnedFromGallery(result, requireActivity(), callbacks); + }); + }); + + private final ActivityResultLauncher customSelectorLauncherForResult = + registerForActivityResult(new StartActivityForResult(), + result -> { + controller.handleActivityResultWithCallback(requireActivity(), callbacks -> { + controller.onPictureReturnedFromCustomSelector(result, requireActivity(), callbacks); + }); + }); + + private final ActivityResultLauncher cameraPickLauncherForResult = + registerForActivityResult(new StartActivityForResult(), + result -> { + controller.handleActivityResultWithCallback(requireActivity(), callbacks -> { + controller.onPictureReturnedFromCamera(result, requireActivity(), callbacks); + }); + }); + private ActivityResultLauncher inAppCameraLocationPermissionLauncher = registerForActivityResult( new RequestMultiplePermissions(), new ActivityResultCallback>() { @@ -240,7 +266,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment } else { if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { controller.handleShowRationaleFlowCameraLocation(getActivity(), - inAppCameraLocationPermissionLauncher); + inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); } else { controller.locationPermissionCallback.onLocationPermissionDenied( getActivity().getString( @@ -570,7 +596,9 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment return Unit.INSTANCE; }, commonPlaceClickActions, - inAppCameraLocationPermissionLauncher + inAppCameraLocationPermissionLauncher, + galleryPickLauncherForResult, + cameraPickLauncherForResult ); binding.bottomSheetNearby.rvNearbyList.setAdapter(adapter); } @@ -2201,7 +2229,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment if (binding.fabCamera.isShown()) { Timber.d("Camera button tapped. Place: %s", selectedPlace.toString()); storeSharedPrefs(selectedPlace); - controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher); + controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); } }); @@ -2210,6 +2238,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment Timber.d("Gallery button tapped. Place: %s", selectedPlace.toString()); storeSharedPrefs(selectedPlace); controller.initiateGalleryPick(getActivity(), + galleryPickLauncherForResult, false); } }); @@ -2218,7 +2247,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment if (binding.fabCustomGallery.isShown()) { Timber.d("Gallery button tapped. Place: %s", selectedPlace.toString()); storeSharedPrefs(selectedPlace); - controller.initiateCustomGalleryPickWithPermission(getActivity()); + controller.initiateCustomGalleryPickWithPermission(getActivity(), customSelectorLauncherForResult); } }); } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/PlaceAdapter.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/PlaceAdapter.kt index 689aa7efc..e5cc92667 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/PlaceAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/PlaceAdapter.kt @@ -1,5 +1,6 @@ package fr.free.nrw.commons.nearby.fragments +import android.content.Intent import androidx.activity.result.ActivityResultLauncher import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao import fr.free.nrw.commons.nearby.Place @@ -12,6 +13,8 @@ class PlaceAdapter( onBookmarkClicked: (Place, Boolean) -> Unit, commonPlaceClickActions: CommonPlaceClickActions, inAppCameraLocationPermissionLauncher: ActivityResultLauncher>, + galleryPickLauncherForResult: ActivityResultLauncher, + cameraPickLauncherForResult: ActivityResultLauncher ) : BaseDelegateAdapter( placeAdapterDelegate( bookmarkLocationsDao, @@ -27,6 +30,8 @@ class PlaceAdapter( commonPlaceClickActions.onDirectionsClicked(), commonPlaceClickActions.onDirectionsLongPressed(), inAppCameraLocationPermissionLauncher, + cameraPickLauncherForResult, + galleryPickLauncherForResult ), areItemsTheSame = { oldItem, newItem -> oldItem.wikiDataEntityId == newItem.wikiDataEntityId }, ) diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 94e799aa2..5e631425b 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -21,6 +21,7 @@ import android.widget.TextView; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.preference.ListPreference; import androidx.preference.MultiSelectListPreference; import androidx.preference.Preference; @@ -85,6 +86,15 @@ public class SettingsFragment extends PreferenceFragmentCompat { private View separator; private ListView languageHistoryListView; private static final String GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content"; + + private final ActivityResultLauncher cameraPickLauncherForResult = + registerForActivityResult(new StartActivityForResult(), + result -> { + contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> { + contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks); + }); + }); + private ActivityResultLauncher inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { @Override public void onActivityResult(Map result) { @@ -93,7 +103,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { areAllGranted = areAllGranted && b; } if (!areAllGranted && shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { - contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher); + contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); } } }); 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 23b187340..35906c3fb 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 @@ -441,14 +441,6 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, super.onRequestPermissionsResult(requestCode, permissions, grantResults); } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) { - //TODO: Confirm if handling manual permission enabled is required - } - } - /** * Sets the flag indicating whether the upload is of a specific place. * diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java index ecddab43d..6fc8b3266 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java @@ -20,6 +20,7 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; @@ -57,27 +58,29 @@ public class UploadMediaDetailAdapter extends private int currentPosition; private Fragment fragment; private Activity activity; + private final ActivityResultLauncher voiceInputResultLauncher; private SelectedVoiceIcon selectedVoiceIcon; - private static final int REQUEST_CODE_FOR_VOICE_INPUT = 1213; private RowItemDescriptionBinding binding; public UploadMediaDetailAdapter(Fragment fragment, String savedLanguageValue, - RecentLanguagesDao recentLanguagesDao) { + RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher voiceInputResultLauncher) { uploadMediaDetails = new ArrayList<>(); selectedLanguages = new HashMap<>(); this.savedLanguageValue = savedLanguageValue; this.recentLanguagesDao = recentLanguagesDao; this.fragment = fragment; + this.voiceInputResultLauncher = voiceInputResultLauncher; } public UploadMediaDetailAdapter(Activity activity, final String savedLanguageValue, - List uploadMediaDetails, RecentLanguagesDao recentLanguagesDao) { + List uploadMediaDetails, RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher voiceInputResultLauncher) { this.uploadMediaDetails = uploadMediaDetails; selectedLanguages = new HashMap<>(); this.savedLanguageValue = savedLanguageValue; this.recentLanguagesDao = recentLanguagesDao; this.activity = activity; + this.voiceInputResultLauncher = voiceInputResultLauncher; } public void setCallback(Callback callback) { @@ -150,11 +153,7 @@ public class UploadMediaDetailAdapter extends ); try { - if (activity == null) { - fragment.startActivityForResult(intent, REQUEST_CODE_FOR_VOICE_INPUT); - } else { - activity.startActivityForResult(intent, REQUEST_CODE_FOR_VOICE_INPUT); - } + voiceInputResultLauncher.launch(intent); } catch (Exception e) { Timber.e(e.getMessage()); } 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 index 105df1837..2c4c2ecd3 100644 --- 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 @@ -18,6 +18,9 @@ import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.Toast; +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.exifinterface.media.ExifInterface; @@ -58,9 +61,24 @@ import timber.log.Timber; public class UploadMediaDetailFragment extends UploadBaseFragment implements UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener { - private static final int REQUEST_CODE = 1211; - private static final int REQUEST_CODE_FOR_EDIT_ACTIVITY = 1212; - private static final int REQUEST_CODE_FOR_VOICE_INPUT = 1213; + private UploadMediaDetailAdapter uploadMediaDetailAdapter; + + private final ActivityResultLauncher startForResult = registerForActivityResult( + new StartActivityForResult(), result -> { + onCameraPosition(result); + }); + + private final ActivityResultLauncher startForEditActivityResult = registerForActivityResult( + new StartActivityForResult(), result -> { + onEditActivityResult(result); + } + ); + + private final ActivityResultLauncher voiceInputResultLauncher = registerForActivityResult( + new StartActivityForResult(), result -> { + onVoiceInput(result); + } + ); public static Activity activity ; @@ -84,8 +102,6 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements private boolean hasUserRemovedLocation; - private UploadMediaDetailAdapter uploadMediaDetailAdapter; - @Inject UploadMediaDetailsContract.UserActionListener presenter; @@ -279,7 +295,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements */ private void initRecyclerView() { uploadMediaDetailAdapter = new UploadMediaDetailAdapter(this, - defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao); + defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao, voiceInputResultLauncher); uploadMediaDetailAdapter.setCallback(this::showInfoAlert); uploadMediaDetailAdapter.setEventListener(this); binding.rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext())); @@ -593,14 +609,14 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements * This method is called to start the image editing activity for a specific UploadItem. * It sets the UploadItem as the currently editable item, creates an intent to launch the * EditActivity, and passes the image file path as an extra in the intent. The activity - * is started with a request code, allowing the result to be handled in onActivityResult. + * is started using resultLauncher that handles the result in respective callback. */ @Override public void showEditActivity(UploadItem uploadItem) { editableUploadItem = uploadItem; Intent intent = new Intent(getContext(), EditActivity.class); intent.putExtra("image", uploadableFile.getFilePath().toString()); - startActivityForResult(intent, REQUEST_CODE_FOR_EDIT_ACTIVITY); + startForEditActivityResult.launch(intent); } /** @@ -615,6 +631,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements double defaultLongitude = -122.431297; double defaultZoom = 16.0; + final Intent locationPickerIntent; + /* Retrieve image location from EXIF if present or check if user has provided location while using the in-app camera. Use location of last UploadItem if none of them is available */ @@ -624,10 +642,11 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements .getDecLatitude(); defaultLongitude = uploadItem.getGpsCoords().getDecLongitude(); defaultZoom = uploadItem.getGpsCoords().getZoomLevel(); - startActivityForResult(new LocationPicker.IntentBuilder() + + locationPickerIntent = new LocationPicker.IntentBuilder() .defaultLocation(new CameraPosition(defaultLatitude,defaultLongitude,defaultZoom)) .activityKey("UploadActivity") - .build(getActivity()), REQUEST_CODE); + .build(getActivity()); } else { if (defaultKvStore.getString(LAST_LOCATION) != null) { final String[] locationLatLng @@ -638,27 +657,20 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements if (defaultKvStore.getString(LAST_ZOOM) != null) { defaultZoom = Double.parseDouble(defaultKvStore.getString(LAST_ZOOM)); } - startActivityForResult(new LocationPicker.IntentBuilder() + + locationPickerIntent = new LocationPicker.IntentBuilder() .defaultLocation(new CameraPosition(defaultLatitude,defaultLongitude,defaultZoom)) .activityKey("NoLocationUploadActivity") - .build(getActivity()), REQUEST_CODE); + .build(getActivity()); } + startForResult.launch(locationPickerIntent); } - /** - * Get the coordinates and update the existing coordinates. - * @param requestCode code of request - * @param resultCode code of result - * @param data intent - */ - @Override - public void onActivityResult(final int requestCode, final int resultCode, - @Nullable final Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) { + private void onCameraPosition(ActivityResult result){ + if (result.getResultCode() == RESULT_OK) { - assert data != null; - final CameraPosition cameraPosition = LocationPicker.getCameraPosition(data); + assert result.getData() != null; + final CameraPosition cameraPosition = LocationPicker.getCameraPosition(result.getData()); if (cameraPosition != null) { @@ -678,8 +690,21 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements removeLocation(); } } - if (requestCode == REQUEST_CODE_FOR_EDIT_ACTIVITY && resultCode == RESULT_OK) { - String result = data.getStringExtra("editedImageFilePath"); + } + + private void onVoiceInput(ActivityResult result) { + if (result.getResultCode() == RESULT_OK && result.getData() != null) { + ArrayList resultData = result.getData().getStringArrayListExtra( + RecognizerIntent.EXTRA_RESULTS); + uploadMediaDetailAdapter.handleSpeechResult(resultData.get(0)); + }else { + Timber.e("Error %s", result.getResultCode()); + } + } + + private void onEditActivityResult(ActivityResult result){ + if (result.getResultCode() == RESULT_OK) { + String path = result.getData().getStringExtra("editedImageFilePath"); if (Objects.equals(result, "Error")) { Timber.e("Error in rotating image"); @@ -687,24 +712,15 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements } try { if (binding != null){ - binding.backgroundImage.setImageURI(Uri.fromFile(new File(result))); + binding.backgroundImage.setImageURI(Uri.fromFile(new File(path))); } - editableUploadItem.setContentUri(Uri.fromFile(new File(result))); + editableUploadItem.setContentUri(Uri.fromFile(new File(path))); callback.changeThumbnail(indexOfFragment, - result); + path); } catch (Exception e) { Timber.e(e); } } - else if (requestCode == REQUEST_CODE_FOR_VOICE_INPUT) { - if (resultCode == RESULT_OK && data != null) { - ArrayList result = data.getStringArrayListExtra( - RecognizerIntent.EXTRA_RESULTS); - uploadMediaDetailAdapter.handleSpeechResult(result.get(0)); - }else { - Timber.e("Error %s", resultCode); - } - } } /** diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java index 9082c1f0f..828ef2338 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java @@ -54,8 +54,7 @@ public class PermissionUtils { final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); final Uri uri = Uri.fromParts("package", activity.getPackageName(), null); intent.setData(uri); - activity.startActivityForResult(intent, - CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS); + activity.startActivity(intent); } /** diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/MainActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/contributions/MainActivityUnitTests.kt index a2eb41615..780322603 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/contributions/MainActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/contributions/MainActivityUnitTests.kt @@ -340,20 +340,6 @@ class MainActivityUnitTests { method.invoke(activity, null, true) } - @Test - @Throws(Exception::class) - fun testOnActivityResult() { - val method: Method = - MainActivity::class.java.getDeclaredMethod( - "onActivityResult", - Int::class.java, - Int::class.java, - Intent::class.java, - ) - method.isAccessible = true - method.invoke(activity, 0, 0, null) - } - @Test @Throws(Exception::class) fun testOnResume() { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt index 0274fed7d..b1d66ee4d 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt @@ -1,8 +1,10 @@ package fr.free.nrw.commons.customselector.ui.selector +import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle +import androidx.activity.result.ActivityResult import fr.free.nrw.commons.OkHttpConnectionFactory import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.contributions.ContributionDao @@ -98,20 +100,20 @@ class CustomSelectorActivityTest { } /** - * Test onActivityResult function. + * Test callback when result received. */ @Test @Throws(Exception::class) - fun testOnActivityResult() { + fun testResultLauncher() { + val intent = Mockito.mock(Intent::class.java) + val activityResult = ActivityResult(Activity.RESULT_OK, intent) val func = activity.javaClass.getDeclaredMethod( - "onActivityResult", - Int::class.java, - Int::class.java, - Intent::class.java, + "onFullScreenDataReceived", + ActivityResult::class.java, ) func.isAccessible = true - func.invoke(activity, 512, -1, Mockito.mock(Intent::class.java)) + func.invoke(activity, activityResult) } /** diff --git a/app/src/test/kotlin/fr/free/nrw/commons/filepicker/FilePickerTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/filepicker/FilePickerTest.kt index 365af27f0..b7ef7878f 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/filepicker/FilePickerTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/filepicker/FilePickerTest.kt @@ -6,18 +6,19 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.net.Uri +import android.provider.MediaStore +import androidx.activity.result.ActivityResultLauncher import androidx.preference.PreferenceManager import androidx.test.core.app.ApplicationProvider +import com.nhaarman.mockitokotlin2.KArgumentCaptor +import com.nhaarman.mockitokotlin2.argumentCaptor import com.nhaarman.mockitokotlin2.verify import fr.free.nrw.commons.TestCommonsApplication -import fr.free.nrw.commons.filepicker.Constants.RequestCodes +import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers -import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.`when` @@ -48,8 +49,10 @@ class FilePickerTest { @Mock var unit: Unit? = null - @Captor - var requestCodeCaptor: ArgumentCaptor? = null + @Mock + private lateinit var mockResultLauncher: ActivityResultLauncher + + private val intentCaptor: KArgumentCaptor = argumentCaptor() private lateinit var context: Context @@ -65,15 +68,17 @@ class FilePickerTest { `when`(sharedPref.edit()).thenReturn(sharedPreferencesEditor) `when`(sharedPref.edit().putInt("type", 0)).thenReturn(sharedPreferencesEditor) val openDocumentPreferred = nextBoolean() - FilePicker.openGallery(activity, 0, openDocumentPreferred) - verify(activity).startActivityForResult( - ArgumentMatchers.any(), - requestCodeCaptor?.capture()?.toInt()!!, - ) - if(openDocumentPreferred){ - assertEquals(requestCodeCaptor?.value, RequestCodes.PICK_PICTURE_FROM_DOCUMENTS) - }else{ - assertEquals(requestCodeCaptor?.value, RequestCodes.PICK_PICTURE_FROM_GALLERY) + + FilePicker.openGallery(activity, mockResultLauncher, 0, openDocumentPreferred) + + verify(mockResultLauncher).launch(intentCaptor.capture()) + + val capturedIntent = intentCaptor.firstValue + + if (openDocumentPreferred) { + assertEquals(Intent.ACTION_OPEN_DOCUMENT, capturedIntent.action) + } else { + assertEquals(Intent.ACTION_GET_CONTENT, capturedIntent.action) } } @@ -84,12 +89,13 @@ class FilePickerTest { `when`(sharedPref.edit().putInt("type", 0)).thenReturn(sharedPreferencesEditor) val mockApplication = mock(Application::class.java) `when`(activity.applicationContext).thenReturn(mockApplication) - FilePicker.openCameraForImage(activity, 0) - verify(activity).startActivityForResult( - ArgumentMatchers.any(), - requestCodeCaptor?.capture()?.toInt()!!, - ) - assertEquals(requestCodeCaptor?.value, RequestCodes.TAKE_PICTURE) + FilePicker.openCameraForImage(activity, mockResultLauncher, 0) + + verify(mockResultLauncher).launch(intentCaptor.capture()) + + val capturedIntent = intentCaptor.firstValue + + assertEquals(MediaStore.ACTION_IMAGE_CAPTURE, capturedIntent.action) } @Test @@ -183,46 +189,20 @@ class FilePickerTest { method.invoke(mockFilePicker, mockIntent) } - @Test - fun testHandleActivityResultCaseOne() { - val mockIntent = mock(Intent::class.java) - FilePicker.handleActivityResult( - RequestCodes.FILE_PICKER_IMAGE_IDENTIFICATOR, - Activity.RESULT_OK, - mockIntent, - activity, - object : DefaultCallback() { - override fun onCanceled( - source: FilePicker.ImageSource, - type: Int, - ) { - super.onCanceled(source, type) - } - - override fun onImagePickerError( - e: Exception, - source: FilePicker.ImageSource, - type: Int, - ) { - } - - override fun onImagesPicked( - imagesFiles: List, - source: FilePicker.ImageSource, - type: Int, - ) { - } - }, - ) - } - @Test fun testOpenCustomSelectorRequestCode() { `when`(PreferenceManager.getDefaultSharedPreferences(activity)).thenReturn(sharedPref) `when`(sharedPref.edit()).thenReturn(sharedPreferencesEditor) `when`(sharedPref.edit().putInt("type", 0)).thenReturn(sharedPreferencesEditor) - FilePicker.openCustomSelector(activity, 0) - verify(activity).startActivityForResult(ArgumentMatchers.any(), requestCodeCaptor?.capture()?.toInt()!!) - assertEquals(requestCodeCaptor?.value, RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR) + FilePicker.openCustomSelector(activity, mockResultLauncher, 0) + + verify(mockResultLauncher).launch(intentCaptor.capture()) + + val capturedIntent = intentCaptor.firstValue + + assertEquals( + CustomSelectorActivity.Companion::class.java.declaringClass.name, + capturedIntent.component?.className + ) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt index 7f9e3d576..ea1d3402d 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt @@ -19,6 +19,7 @@ import android.widget.ProgressBar import android.widget.ScrollView import android.widget.Spinner import android.widget.TextView +import androidx.activity.result.ActivityResult import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import androidx.test.core.app.ApplicationProvider @@ -76,7 +77,6 @@ import java.util.Locale @Config(sdk = [21], application = TestCommonsApplication::class) @LooperMode(LooperMode.Mode.PAUSED) class MediaDetailFragmentUnitTests { - private val requestCode = 1001 private val lastLocation = "last_location_while_uploading" private lateinit var fragment: MediaDetailFragment private lateinit var fragmentManager: FragmentManager @@ -231,24 +231,6 @@ class MediaDetailFragmentUnitTests { fragment.onCreateView(layoutInflater, null, savedInstanceState) } - @Test - @Throws(Exception::class) - fun testOnActivityResultLocationPickerActivity() { - fragment.onActivityResult(requestCode, Activity.RESULT_CANCELED, intent) - } - - @Test - @Throws(Exception::class) - fun `test OnActivity Result Cancelled LocationPickerActivity`() { - fragment.onActivityResult(requestCode, Activity.RESULT_CANCELED, intent) - } - - @Test - @Throws(Exception::class) - fun `test OnActivity Result Cancelled DescriptionEditActivity`() { - fragment.onActivityResult(requestCode, Activity.RESULT_OK, intent) - } - @Test @Throws(Exception::class) fun testOnSaveInstanceState() { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt index d15fe06e5..938e595af 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt @@ -167,20 +167,6 @@ class UploadActivityUnitTests { activity.makeUploadRequest() } - @Test - @Throws(Exception::class) - fun testOnActivityResult() { - val method: Method = - UploadActivity::class.java.getDeclaredMethod( - "onActivityResult", - Int::class.java, - Int::class.java, - Intent::class.java, - ) - method.isAccessible = true - method.invoke(activity, CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS, 0, Intent()) - } - @Test @Throws(Exception::class) fun testReceiveSharedItems() { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaDetailAdapterUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaDetailAdapterUnitTest.kt index db5b20f7e..794b6e64e 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaDetailAdapterUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaDetailAdapterUnitTest.kt @@ -2,11 +2,13 @@ package fr.free.nrw.commons.upload import android.app.Dialog import android.content.Context +import android.content.Intent import android.view.View import android.widget.AdapterView import android.widget.GridLayout import android.widget.ListView import android.widget.TextView +import androidx.activity.result.ActivityResultLauncher import androidx.test.core.app.ApplicationProvider import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.times @@ -67,13 +69,16 @@ class UploadMediaDetailAdapterUnitTest { @Mock private lateinit var adapterView: AdapterView + @Mock + private lateinit var mockResultLauncher: ActivityResultLauncher + @Before fun setUp() { MockitoAnnotations.openMocks(this) uploadMediaDetails = mutableListOf(uploadMediaDetail, uploadMediaDetail) activity = Robolectric.buildActivity(UploadActivity::class.java).get() fragment = mock(UploadMediaDetailFragment::class.java) - adapter = UploadMediaDetailAdapter(fragment, "", recentLanguagesDao) + adapter = UploadMediaDetailAdapter(fragment, "", recentLanguagesDao, mockResultLauncher) context = ApplicationProvider.getApplicationContext() Whitebox.setInternalState(adapter, "uploadMediaDetails", uploadMediaDetails) Whitebox.setInternalState(adapter, "eventListener", eventListener) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt index ed76b4519..169bcd5c0 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt @@ -11,6 +11,7 @@ import android.view.View import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.activity.result.ActivityResult import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatImageButton import androidx.fragment.app.FragmentManager @@ -348,7 +349,7 @@ class UploadMediaDetailFragmentUnitTest { @Test @Throws(Exception::class) - fun testOnActivityResultOnMapIconClicked() { + fun testOnCameraPositionCallbackOnMapIconClicked() { shadowOf(Looper.getMainLooper()).idle() Mockito.mock(LocationPicker::class.java) val intent = Mockito.mock(Intent::class.java) @@ -363,13 +364,18 @@ class UploadMediaDetailFragmentUnitTest { `when`(latLng.latitude).thenReturn(0.0) `when`(latLng.longitude).thenReturn(0.0) `when`(uploadItem.gpsCoords).thenReturn(imageCoordinates) - fragment.onActivityResult(1211, Activity.RESULT_OK, intent) + val activityResult = ActivityResult(Activity.RESULT_OK, intent) + + val handleResultMethod = UploadMediaDetailFragment::class.java.getDeclaredMethod("onCameraPosition", ActivityResult::class.java) + handleResultMethod.isAccessible = true + + handleResultMethod.invoke(fragment, activityResult) Mockito.verify(presenter, Mockito.times(0)).getImageQuality(0, location, activity) } @Test @Throws(Exception::class) - fun testOnActivityResultAddLocationDialog() { + fun testOnCameraPositionCallbackAddLocationDialog() { shadowOf(Looper.getMainLooper()).idle() Mockito.mock(LocationPicker::class.java) val intent = Mockito.mock(Intent::class.java) @@ -387,7 +393,13 @@ class UploadMediaDetailFragmentUnitTest { `when`(latLng.latitude).thenReturn(0.0) `when`(latLng.longitude).thenReturn(0.0) `when`(uploadItem.gpsCoords).thenReturn(imageCoordinates) - fragment.onActivityResult(1211, Activity.RESULT_OK, intent) + + val activityResult = ActivityResult(Activity.RESULT_OK,intent) + + val handleResultMethod = UploadMediaDetailFragment::class.java.getDeclaredMethod("onCameraPosition", ActivityResult::class.java) + handleResultMethod.isAccessible = true + + handleResultMethod.invoke(fragment, activityResult) Mockito.verify(presenter, Mockito.times(1)).displayLocDialog(0, null, false) } From 1659a4ce22b7dd580d07a3bcd20344e1ed6a67c5 Mon Sep 17 00:00:00 2001 From: Parneet Singh <111801812+parneet-guraya@users.noreply.github.com> Date: Thu, 24 Oct 2024 15:37:16 +0530 Subject: [PATCH 08/74] add dependency (#5887) Signed-off-by: parneet-guraya --- app/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 43c1695da..b683b489b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -93,6 +93,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION" //Mocking + testImplementation("io.mockk:mockk:1.13.4") testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' testImplementation 'org.mockito:mockito-inline:5.2.0' testImplementation 'org.mockito:mockito-core:5.6.0' @@ -226,7 +227,7 @@ android { excludes += ['META-INF/androidx.*'] } resources { - excludes += ['META-INF/androidx.*', 'META-INF/proguard/androidx-annotations.pro'] + excludes += ['META-INF/androidx.*', 'META-INF/proguard/androidx-annotations.pro', '/META-INF/LICENSE.md', '/META-INF/LICENSE-notice.md'] } } From becc07d26b4d4396373b7a62cf5db1b61f53c10b Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 24 Oct 2024 14:02:18 +0200 Subject: [PATCH 09/74] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-bg/strings.xml | 1 + app/src/main/res/values-da/strings.xml | 3 +++ app/src/main/res/values-it/strings.xml | 3 +++ app/src/main/res/values-mk/strings.xml | 4 ++++ app/src/main/res/values-pa/strings.xml | 3 ++- app/src/main/res/values-zh-rTW/strings.xml | 3 +++ 6 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 0f765a1fb..6ee931542 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -307,4 +307,5 @@ Моля, изчакайте... напълно размазано Наблизо + Прочетете повече diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 2aa04017a..3b6822c47 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -789,4 +789,7 @@ Afventer Mislykkedes Kunne ikke indlæse steddata + Dette sted har endnu ikke noget billede, så gå hen og tag et! + Dette sted har allerede et billede. + Tjekker nu, om dette sted har et billede. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index be69fa045..f40863870 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -760,4 +760,7 @@ %d immagine selezionata %d immagini selezionate + Questo posto non ha ancora una foto, scattane una! + Questo posto ha già una foto. + Ora controlliamo se questo posto ha una foto. diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 00a8ba098..916f4f420 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -475,6 +475,7 @@ Немате непрочитани известувања Немате прочитани известувања Споделувај дневници користејќи + Проверете си ја дојдовната е-пошта Погл. прочитани Погл. непрочитани Се јави грешка при избирањето на сликите @@ -784,4 +785,7 @@ Во исчекување Неуспешно Не можев да ги вчитам податоците за место + Местово сè уште нема слика. Направете ја! + Местово веќе има слика. + Проверувам дали местово има слика. diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 48713104d..8c64900a5 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -42,6 +42,7 @@ ਵਰਤੋਂਕਾਰ ਨਾਂ ਲੰਘ-ਸ਼ਬਦ ਦਾਖ਼ਲ ਹੋਵੋ + ਪਾਰਸ਼ਬਦ ਭੁੱਲ ਗਏ? ਦਾਖ਼ਲਾ ਹੋ ਰਿਹਾ ਹੈ ਉਡੀਕੋ ਜੀ… ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... @@ -67,7 +68,7 @@ ਨੇੜੇ-ਤੇੜੇ ਮੇਰੇ ਅੱਪਲੋਡ ਸਾਂਝਾ ਕਰੋ - ਸਿਰਲੇਖ + ਸੁਰਖੀ (ਲੋੜੀਂਦੀ) ਵੇਰਵਾ ਦਾਖ਼ਲ ਹੋਣ ਵਿੱਚ ਅਸਮਰੱਥ - ਨੈੱਟਵਰਕ ਫੇਲ੍ਹ ਹੋਇਆ ਹੈ ਬਹੁਤ ਸਾਰੀਆਂ ਅਸਫ਼ਲ ਕੋਸ਼ਿਸ਼ਾਂ। ਥੋੜ੍ਹੀ ਦੇਰ ਬਾਅਦ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ। diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index e54fb9d76..cd3cf80df 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -806,4 +806,7 @@ 待處理 失敗 無法載入地點資料 + 這個地點還沒有照片,來拍一張吧! + 這個地點已有照片。 + 現在檢查這個地點是否有照片。 From 3e020ed973f4919f83d0fb0c78cf5ff8ef5ac41d Mon Sep 17 00:00:00 2001 From: Noah Vendrig Date: Fri, 25 Oct 2024 16:19:07 +1100 Subject: [PATCH 10/74] Fixes #5806 Implemented "Refresh" button to clear the cache and reload the Nearby map (#5891) * Changed files required to get the app to run correctly. Removed suspend from affected DAO files and funcs, and changed to (Kotlin v1.9.22) and (Kotlin compiler v1.5.8) * Created refresh button icon, and added it to the nearby_fragment_menu.xml (header of the nearby page). Created function refresh() in NearbyParentFragment.java to handle refresh functionality. * Replaced refresh() func with emptyCache() and reloadMap() * Attempt at reloadMap(), no testing done yet. * added changes for a possibly working emptyCache implementation (needs testing). * Tested changes as working, edited emptyCache to correctly clear cache and then reload map --------- Co-authored-by: MarcusBarta --- .idea/inspectionProfiles/Project_Default.xml | 7 --- app/build.gradle | 2 +- .../database/NotForUploadStatusDao.kt | 10 ++-- .../database/UploadedStatusDao.kt | 14 ++--- .../fr/free/nrw/commons/nearby/PlaceDao.java | 19 +++++++ .../commons/nearby/PlacesLocalDataSource.java | 4 ++ .../nrw/commons/nearby/PlacesRepository.java | 10 ++++ .../fragments/NearbyParentFragment.java | 55 +++++++++++++++++++ .../upload/categories/BaseDelegateAdapter.kt | 14 ++--- .../nrw/commons/upload/depicts/DepictsDao.kt | 8 +-- .../res/drawable/ic_refresh_24dp_nearby.xml | 18 ++++++ .../main/res/menu/nearby_fragment_menu.xml | 8 +++ build.gradle | 2 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 15 files changed, 140 insertions(+), 35 deletions(-) create mode 100644 app/src/main/res/drawable/ic_refresh_24dp_nearby.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index a5d456928..f39734eb4 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,16 +1,12 @@ \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index b683b489b..c949707c2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -381,7 +381,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion '1.3.2' + kotlinCompilerExtensionVersion '1.5.8' } namespace 'fr.free.nrw.commons' lint { diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt index b75a6e1d4..872388f40 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt @@ -15,19 +15,19 @@ abstract class NotForUploadStatusDao { * Insert into Not For Upload status. */ @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun insert(notForUploadStatus: NotForUploadStatus) + abstract fun insert(notForUploadStatus: NotForUploadStatus) /** * Delete Not For Upload status entry. */ @Delete - abstract suspend fun delete(notForUploadStatus: NotForUploadStatus) + abstract fun delete(notForUploadStatus: NotForUploadStatus) /** * Query Not For Upload status with image sha1. */ @Query("SELECT * FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") - abstract suspend fun getFromImageSHA1(imageSHA1: String): NotForUploadStatus? + abstract fun getFromImageSHA1(imageSHA1: String): NotForUploadStatus? /** * Asynchronous image sha1 query. @@ -38,7 +38,7 @@ abstract class NotForUploadStatusDao { * Deletion Not For Upload status with image sha1. */ @Query("DELETE FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") - abstract suspend fun deleteWithImageSHA1(imageSHA1: String) + abstract fun deleteWithImageSHA1(imageSHA1: String) /** * Asynchronous image sha1 deletion. @@ -49,5 +49,5 @@ abstract class NotForUploadStatusDao { * Check whether the imageSHA1 is present in database */ @Query("SELECT COUNT() FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") - abstract suspend fun find(imageSHA1: String): Int + abstract fun find(imageSHA1: String): Int } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt index 378af5b8d..03cbb176f 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt @@ -17,31 +17,31 @@ abstract class UploadedStatusDao { * Insert into uploaded status. */ @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun insert(uploadedStatus: UploadedStatus) + abstract fun insert(uploadedStatus: UploadedStatus) /** * Update uploaded status entry. */ @Update - abstract suspend fun update(uploadedStatus: UploadedStatus) + abstract fun update(uploadedStatus: UploadedStatus) /** * Delete uploaded status entry. */ @Delete - abstract suspend fun delete(uploadedStatus: UploadedStatus) + abstract fun delete(uploadedStatus: UploadedStatus) /** * Query uploaded status with image sha1. */ @Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ") - abstract suspend fun getFromImageSHA1(imageSHA1: String): UploadedStatus? + abstract fun getFromImageSHA1(imageSHA1: String): UploadedStatus? /** * Query uploaded status with modified image sha1. */ @Query("SELECT * FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) ") - abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1: String): UploadedStatus? + abstract fun getFromModifiedImageSHA1(modifiedImageSHA1: String): UploadedStatus? /** * Asynchronous insert into uploaded status table. @@ -55,7 +55,7 @@ abstract class UploadedStatusDao { * Check whether the imageSHA1 is present in database */ @Query("SELECT COUNT() FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) AND imageResult = (:imageResult) ") - abstract suspend fun findByImageSHA1( + abstract fun findByImageSHA1( imageSHA1: String, imageResult: Boolean, ): Int @@ -66,7 +66,7 @@ abstract class UploadedStatusDao { @Query( "SELECT COUNT() FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) AND modifiedImageResult = (:modifiedImageResult) ", ) - abstract suspend fun findByModifiedImageSHA1( + abstract fun findByModifiedImageSHA1( modifiedImageSHA1: String, modifiedImageResult: Boolean, ): Int diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java index 8c0a0a393..7babee3b7 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java @@ -41,4 +41,23 @@ public abstract class PlaceDao { saveSynchronous(place); }); } + + /** + * Deletes all Place objects from the database. + * + * @return A Completable that completes once the deletion operation is done. + */ + @Query("DELETE FROM place") + public abstract void deleteAllSynchronous(); + + /** + * Deletes all Place objects from the database. + * + */ + public Completable deleteAll() { + return Completable + .fromAction(() -> { + deleteAllSynchronous(); + }); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlacesLocalDataSource.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlacesLocalDataSource.java index a7f1dadcd..86a57eadc 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlacesLocalDataSource.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlacesLocalDataSource.java @@ -35,4 +35,8 @@ public class PlacesLocalDataSource { public Completable savePlace(Place place) { return placeDao.save(place); } + + public Completable clearCache() { + return placeDao.deleteAll(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlacesRepository.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlacesRepository.java index 85e964ddb..846e54fac 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlacesRepository.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlacesRepository.java @@ -3,6 +3,7 @@ package fr.free.nrw.commons.nearby; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.location.LatLng; import io.reactivex.Completable; +import io.reactivex.schedulers.Schedulers; import javax.inject.Inject; /** @@ -38,4 +39,13 @@ public class PlacesRepository { return localDataSource.fetchPlace(entityID); } + /** + * Clears the Nearby cache on an IO thread. + * + * @return A Completable that completes once the cache has been successfully cleared. + */ + public Completable clearCache() { + return localDataSource.clearCache() + .subscribeOn(Schedulers.io()); // Ensure it runs on IO thread + } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index 7a7d5cdcb..6a2e5c3a9 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -108,6 +108,7 @@ import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.SystemThemeUtils; import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.wikidata.WikidataEditListener; +import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -342,9 +343,21 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { inflater.inflate(R.menu.nearby_fragment_menu, menu); + MenuItem refreshButton = menu.findItem(R.id.item_refresh); MenuItem listMenu = menu.findItem(R.id.list_sheet); MenuItem saveAsGPXButton = menu.findItem(R.id.list_item_gpx); MenuItem saveAsKMLButton = menu.findItem(R.id.list_item_kml); + refreshButton.setOnMenuItemClickListener(new OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + try { + emptyCache(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return false; + } + }); listMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { @@ -1158,6 +1171,48 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment } } + /** + * Reloads the Nearby map + * Clears all location markers, refreshes them, reinserts them into the map. + * + */ + private void reloadMap() { + clearAllMarkers(); // Clear the list of markers + binding.map.getController().setZoom(ZOOM_LEVEL); // Reset the zoom level + binding.map.getController().setCenter(lastMapFocus); // Recenter the focus + if (locationPermissionsHelper.checkLocationPermission(getActivity())) { + locationPermissionGranted(); // Reload map with user's location + } else { + startMapWithoutPermission(); // Reload map without user's location + } + binding.map.invalidate(); // Invalidate the map + presenter.updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED); // Restart the map + Timber.d("Reloaded Map Successfully"); + } + + + /** + * Clears the Nearby local cache and then calls for the map to be reloaded + * + */ + private void emptyCache() { + // reload the map once the cache is cleared + compositeDisposable.add( + placesRepository.clearCache() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .andThen(Completable.fromAction(this::reloadMap)) + .subscribe( + () -> { + Timber.d("Nearby Cache cleared successfully."); + }, + throwable -> { + Timber.e(throwable, "Failed to clear the Nearby Cache"); + } + ) + ); + } + private void savePlacesAsKML() { final Observable savePlacesObservable = Observable .fromCallable(() -> nearbyController diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/BaseDelegateAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/BaseDelegateAdapter.kt index f1e4917a0..ce12d3915 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/BaseDelegateAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/BaseDelegateAdapter.kt @@ -10,15 +10,13 @@ abstract class BaseDelegateAdapter( areContentsTheSame: (T, T) -> Boolean = { old, new -> old == new }, ) : AsyncListDifferDelegationAdapter( object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: T, - newItem: T, - ) = areItemsTheSame(oldItem, newItem) + override fun areItemsTheSame(oldItem: T & Any, newItem: T & Any): Boolean { + return areItemsTheSame(oldItem, newItem) + } - override fun areContentsTheSame( - oldItem: T, - newItem: T, - ) = areContentsTheSame(oldItem, newItem) + override fun areContentsTheSame(oldItem: T & Any, newItem: T & Any): Boolean { + return areContentsTheSame(oldItem, newItem) + } }, *delegates, ) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt index 684400301..c20d65abf 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt @@ -22,16 +22,16 @@ abstract class DepictsDao { private val maxItemsAllowed = 10 @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun insert(depictedItem: Depicts) + abstract fun insert(depictedItem: Depicts) @Query("Select * From depicts_table order by lastUsed DESC") - abstract suspend fun getAllDepicts(): List + abstract fun getAllDepicts(): List @Query("Select * From depicts_table order by lastUsed DESC LIMIT :n OFFSET 10") - abstract suspend fun getDepictsForDeletion(n: Int): List + abstract fun getDepictsForDeletion(n: Int): List @Delete - abstract suspend fun delete(depicts: Depicts) + abstract fun delete(depicts: Depicts) /** * Gets all Depicts objects from the database, ordered by lastUsed in descending order. diff --git a/app/src/main/res/drawable/ic_refresh_24dp_nearby.xml b/app/src/main/res/drawable/ic_refresh_24dp_nearby.xml new file mode 100644 index 000000000..89f49ad9e --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_24dp_nearby.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/menu/nearby_fragment_menu.xml b/app/src/main/res/menu/nearby_fragment_menu.xml index 30b5c9dd5..fe049cde4 100644 --- a/app/src/main/res/menu/nearby_fragment_menu.xml +++ b/app/src/main/res/menu/nearby_fragment_menu.xml @@ -1,17 +1,25 @@ + + + + + diff --git a/build.gradle b/build.gradle index 003163cb8..b0bad89a5 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { maven { url "https://plugins.gradle.org/m2/" } } dependencies { - classpath 'com.android.tools.build:gradle:8.5.0' + classpath 'com.android.tools.build:gradle:8.7.0' classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION" classpath 'org.codehaus.groovy:groovy-all:2.4.15' diff --git a/gradle.properties b/gradle.properties index ecfe43a80..9ca154b75 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ org.gradle.jvmargs=-Xmx1536M org.gradle.caching=true android.enableR8.fullMode=false -KOTLIN_VERSION=1.7.20 +KOTLIN_VERSION=1.9.22 LEAK_CANARY_VERSION=2.10 DAGGER_VERSION=2.23 ROOM_VERSION=2.5.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fb6a72053..fd53d45f9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun Apr 23 18:22:54 IST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists \ No newline at end of file From 7c588918929edef6cf18467731766ef059797206 Mon Sep 17 00:00:00 2001 From: Parneet Singh <111801812+parneet-guraya@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:02:35 +0530 Subject: [PATCH 11/74] fix test (#5893) Signed-off-by: parneet-guraya --- .../test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt index 283bbf268..f980152dc 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt @@ -140,7 +140,7 @@ class ReviewHelperTest { mock().apply { whenever(title()).thenReturn(file) if (revision.isNotEmpty()) { - whenever(revisions()).thenReturn(*revision.toMutableList()) + whenever(revisions()).thenReturn(revision.toMutableList()) } val media = From bc065c8792d2804c75aa5f3af9f12a38043a160d Mon Sep 17 00:00:00 2001 From: Parneet Singh <111801812+parneet-guraya@users.noreply.github.com> Date: Sat, 26 Oct 2024 19:49:34 +0530 Subject: [PATCH 12/74] `CommonsApplication` migrate to kotlin & some lint fixes (#5879) * convert to kotlin Signed-off-by: parneet-guraya * use lateinit instead of nullable types Signed-off-by: parneet-guraya * instance property access fix Signed-off-by: parneet-guraya * refactor constants name with uppercased ones Signed-off-by: parneet-guraya * remove unused Signed-off-by: parneet-guraya * fix imports in test Signed-off-by: parneet-guraya * use mockk for kotlin to fix tests Signed-off-by: parneet-guraya --------- Signed-off-by: parneet-guraya --- .../fr/free/nrw/commons/AboutActivityTest.kt | 2 +- .../free/nrw/commons/CommonsApplication.java | 432 ------------------ .../fr/free/nrw/commons/CommonsApplication.kt | 414 +++++++++++++++++ .../free/nrw/commons/actions/ThanksClient.kt | 2 +- .../free/nrw/commons/auth/LoginActivity.java | 8 +- .../description/DescriptionEditActivity.kt | 4 +- .../nrw/commons/upload/worker/UploadWorker.kt | 2 +- .../nrw/commons/actions/ThanksClientTest.kt | 7 +- .../DescriptionEditActivityUnitTest.kt | 8 + .../nrw/commons/upload/UploadClientTest.kt | 2 +- 10 files changed, 436 insertions(+), 445 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/CommonsApplication.java create mode 100644 app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt diff --git a/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt index b5a752ef9..45ff9e49d 100644 --- a/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt +++ b/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt @@ -105,7 +105,7 @@ class AboutActivityTest { fun testLaunchTranslate() { Espresso.onView(ViewMatchers.withId(R.id.about_translate)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click()) - val langCode = CommonsApplication.getInstance().languageLookUpTable.codes[0] + val langCode = CommonsApplication.instance.languageLookUpTable!!.codes[0] Intents.intended( CoreMatchers.allOf( IntentMatchers.hasAction(Intent.ACTION_VIEW), diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java deleted file mode 100644 index 3aceb957a..000000000 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ /dev/null @@ -1,432 +0,0 @@ -package fr.free.nrw.commons; - -import static fr.free.nrw.commons.data.DBOpenHelper.CONTRIBUTIONS_TABLE; -import static org.acra.ReportField.ANDROID_VERSION; -import static org.acra.ReportField.APP_VERSION_CODE; -import static org.acra.ReportField.APP_VERSION_NAME; -import static org.acra.ReportField.PHONE_MODEL; -import static org.acra.ReportField.STACK_TRACE; -import static org.acra.ReportField.USER_COMMENT; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.content.Context; -import android.content.Intent; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.os.Build; -import android.os.Process; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.multidex.MultiDexApplication; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.imagepipeline.core.ImagePipeline; -import com.facebook.imagepipeline.core.ImagePipelineConfig; -import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table; -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao; -import fr.free.nrw.commons.category.CategoryDao; -import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler; -import fr.free.nrw.commons.concurrency.ThreadPoolService; -import fr.free.nrw.commons.contributions.ContributionDao; -import fr.free.nrw.commons.data.DBOpenHelper; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.language.AppLanguageLookUpTable; -import fr.free.nrw.commons.logging.FileLoggingTree; -import fr.free.nrw.commons.logging.LogUtils; -import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher; -import fr.free.nrw.commons.settings.Prefs; -import fr.free.nrw.commons.upload.FileUtils; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar; -import io.reactivex.Completable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.internal.functions.Functions; -import io.reactivex.plugins.RxJavaPlugins; -import io.reactivex.schedulers.Schedulers; -import java.io.File; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import javax.inject.Inject; -import javax.inject.Named; -import org.acra.ACRA; -import org.acra.annotation.AcraCore; -import org.acra.annotation.AcraDialog; -import org.acra.annotation.AcraMailSender; -import org.acra.data.StringFormat; -import timber.log.Timber; - -@AcraCore( - buildConfigClass = BuildConfig.class, - resReportSendSuccessToast = R.string.crash_dialog_ok_toast, - reportFormat = StringFormat.KEY_VALUE_LIST, - reportContent = {USER_COMMENT, APP_VERSION_CODE, APP_VERSION_NAME, ANDROID_VERSION, PHONE_MODEL, - STACK_TRACE} -) - -@AcraMailSender( - mailTo = "commons-app-android-private@googlegroups.com", - reportAsFile = false -) - -@AcraDialog( - resTheme = R.style.Theme_AppCompat_Dialog, - resText = R.string.crash_dialog_text, - resTitle = R.string.crash_dialog_title, - resCommentPrompt = R.string.crash_dialog_comment_prompt -) - -public class CommonsApplication extends MultiDexApplication { - - public static final String loginMessageIntentKey = "loginMessage"; - public static final String loginUsernameIntentKey = "loginUsername"; - - public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled"; - @Inject - SessionManager sessionManager; - @Inject - DBOpenHelper dbOpenHelper; - - @Inject - @Named("default_preferences") - JsonKvStore defaultPrefs; - - @Inject - CommonsCookieJar cookieJar; - - @Inject - CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher; - - /** - * Constants begin - */ - - public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using [[COM:MOA|Commons Mobile App]]"; - - public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com"; - - public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App Feedback"; - - public static final String REPORT_EMAIL = "commons-app-android-private@googlegroups.com"; - - public static final String REPORT_EMAIL_SUBJECT = "Report a violation"; - - public static final String NOTIFICATION_CHANNEL_ID_ALL = "CommonsNotificationAll"; - - public static final String FEEDBACK_EMAIL_TEMPLATE_HEADER = "-- Technical information --"; - - /** - * Constants End - */ - - private static CommonsApplication INSTANCE; - - public static CommonsApplication getInstance() { - return INSTANCE; - } - - private AppLanguageLookUpTable languageLookUpTable; - - public AppLanguageLookUpTable getLanguageLookUpTable() { - return languageLookUpTable; - } - - @Inject - ContributionDao contributionDao; - - public static Boolean isPaused = false; - - /** - * Used to declare and initialize various components and dependencies - */ - @Override - public void onCreate() { - super.onCreate(); - - INSTANCE = this; - ACRA.init(this); - - ApplicationlessInjection - .getInstance(this) - .getCommonsApplicationComponent() - .inject(this); - - initTimber(); - - if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) { - Set defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS); - if (null == defaultExifTagsSet) { - defaultExifTagsSet = new HashSet<>(); - } - defaultExifTagsSet.add(getString(R.string.exif_tag_location)); - defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet); - } - -// Set DownsampleEnabled to True to downsample the image in case it's heavy - ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this) - .setNetworkFetcher(customOkHttpNetworkFetcher) - .setDownsampleEnabled(true) - .build(); - try { - Fresco.initialize(this, config); - } catch (Exception e) { - Timber.e(e); - // TODO: Remove when we're able to initialize Fresco in test builds. - } - - createNotificationChannel(this); - - languageLookUpTable = new AppLanguageLookUpTable(this); - - // This handler will catch exceptions thrown from Observables after they are disposed, - // or from Observables that are (deliberately or not) missing an onError handler. - RxJavaPlugins.setErrorHandler(Functions.emptyConsumer()); - - // Fire progress callbacks for every 3% of uploaded content - System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0"); - } - - /** - * Plants debug and file logging tree. Timber lets you plant your own logging trees. - */ - private void initTimber() { - boolean isBeta = ConfigUtils.isBetaFlavour(); - String logFileName = - isBeta ? "CommonsBetaAppLogs" : "CommonsAppLogs"; - String logDirectory = LogUtils.getLogDirectory(); - //Delete stale logs if they have exceeded the specified size - deleteStaleLogs(logFileName, logDirectory); - - FileLoggingTree tree = new FileLoggingTree( - Log.VERBOSE, - logFileName, - logDirectory, - 1000, - getFileLoggingThreadPool()); - - Timber.plant(tree); - Timber.plant(new Timber.DebugTree()); - } - - /** - * Deletes the logs zip file at the specified directory and file locations specified in the - * params - * - * @param logFileName - * @param logDirectory - */ - private void deleteStaleLogs(String logFileName, String logDirectory) { - try { - File file = new File(logDirectory + "/zip/" + logFileName + ".zip"); - if (file.exists() && file.getTotalSpace() > 1000000) {// In Kbs - file.delete(); - } - } catch (Exception e) { - Timber.e(e); - } - } - - public static boolean isRoboUnitTest() { - return "robolectric".equals(Build.FINGERPRINT); - } - - private ThreadPoolService getFileLoggingThreadPool() { - return new ThreadPoolService.Builder("file-logging-thread") - .setPriority(Process.THREAD_PRIORITY_LOWEST) - .setPoolSize(1) - .setExceptionHandler(new BackgroundPoolExceptionHandler()) - .build(); - } - - public static void createNotificationChannel(@NonNull Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationManager manager = (NotificationManager) context - .getSystemService(Context.NOTIFICATION_SERVICE); - NotificationChannel channel = manager - .getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL); - if (channel == null) { - channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID_ALL, - context.getString(R.string.notifications_channel_name_all), - NotificationManager.IMPORTANCE_DEFAULT); - manager.createNotificationChannel(channel); - } - } - } - - public String getUserAgent() { - return "Commons/" + ConfigUtils.getVersionNameWithSha(this) - + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE; - } - - /** - * clears data of current application - * - * @param context Application context - * @param logoutListener Implementation of interface LogoutListener - */ - @SuppressLint("CheckResult") - public void clearApplicationData(Context context, LogoutListener logoutListener) { - File cacheDirectory = context.getCacheDir(); - File applicationDirectory = new File(cacheDirectory.getParent()); - if (applicationDirectory.exists()) { - String[] fileNames = applicationDirectory.list(); - for (String fileName : fileNames) { - if (!fileName.equals("lib")) { - FileUtils.deleteFile(new File(applicationDirectory, fileName)); - } - } - } - - sessionManager.logout() - .andThen(Completable.fromAction(() -> cookieJar.clear())) - .andThen(Completable.fromAction(() -> { - Timber.d("All accounts have been removed"); - clearImageCache(); - //TODO: fix preference manager - defaultPrefs.clearAll(); - defaultPrefs.putBoolean("firstrun", false); - updateAllDatabases(); - } - )) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(logoutListener::onLogoutComplete, Timber::e); - } - - /** - * Clear all images cache held by Fresco - */ - private void clearImageCache() { - ImagePipeline imagePipeline = Fresco.getImagePipeline(); - imagePipeline.clearCaches(); - } - - /** - * Deletes all tables and re-creates them. - */ - private void updateAllDatabases() { - dbOpenHelper.getReadableDatabase().close(); - SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); - - CategoryDao.Table.onDelete(db); - dbOpenHelper.deleteTable(db, - CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions - - try { - contributionDao.deleteAll(); - } catch (SQLiteException e) { - Timber.e(e); - } - BookmarkPicturesDao.Table.onDelete(db); - BookmarkLocationsDao.Table.onDelete(db); - Table.onDelete(db); - } - - - /** - * Interface used to get log-out events - */ - public interface LogoutListener { - - void onLogoutComplete(); - } - - /** - * This listener is responsible for handling post-logout actions, specifically invoking the LoginActivity - * with relevant intent parameters. It does not perform the actual logout operation. - */ - public static class BaseLogoutListener implements CommonsApplication.LogoutListener { - - Context ctx; - String loginMessage, userName; - - /** - * Constructor for BaseLogoutListener. - * - * @param ctx Application context - */ - public BaseLogoutListener(final Context ctx) { - this.ctx = ctx; - } - - /** - * Constructor for BaseLogoutListener - * - * @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. - * @param loginMessage Message to be displayed on the login page - * @param loginUsername Username to be pre-filled on the login page - */ - public BaseLogoutListener(final Context ctx, final String loginMessage, - final String loginUsername) { - this.ctx = ctx; - this.loginMessage = loginMessage; - this.userName = loginUsername; - } - - @Override - public void onLogoutComplete() { - Timber.d("Logout complete callback received."); - final Intent loginIntent = new Intent(ctx, LoginActivity.class); - loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - if (loginMessage != null) { - loginIntent.putExtra(loginMessageIntentKey, loginMessage); - } - if (userName != null) { - loginIntent.putExtra(loginUsernameIntentKey, userName); - } - - ctx.startActivity(loginIntent); - } - } - - /** - * This class is an extension of BaseLogoutListener, providing additional functionality or customization - * for the logout process. It includes specific actions to be taken during logout, such as handling redirection to the login screen. - */ - public static class ActivityLogoutListener extends BaseLogoutListener { - - Activity activity; - - - /** - * Constructor for ActivityLogoutListener. - * - * @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity. - * @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. - */ - public ActivityLogoutListener(final Activity activity, final Context ctx) { - super(ctx); - this.activity = activity; - } - - /** - * Constructor for ActivityLogoutListener with additional parameters for the login screen. - * - * @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity. - * @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. - * @param loginMessage Message to be displayed on the login page after logout. - * @param loginUsername Username to be pre-filled on the login page after logout. - */ - public ActivityLogoutListener(final Activity activity, final Context ctx, - final String loginMessage, final String loginUsername) { - super(activity, loginMessage, loginUsername); - this.activity = activity; - } - - @Override - public void onLogoutComplete() { - super.onLogoutComplete(); - activity.finish(); - } - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt new file mode 100644 index 000000000..9ed19d686 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt @@ -0,0 +1,414 @@ +package fr.free.nrw.commons + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.database.sqlite.SQLiteException +import android.os.Build +import android.os.Process +import android.util.Log +import androidx.multidex.MultiDexApplication +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.imagepipeline.core.ImagePipelineConfig +import fr.free.nrw.commons.auth.LoginActivity +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao +import fr.free.nrw.commons.category.CategoryDao +import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler +import fr.free.nrw.commons.concurrency.ThreadPoolService +import fr.free.nrw.commons.contributions.ContributionDao +import fr.free.nrw.commons.data.DBOpenHelper +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.language.AppLanguageLookUpTable +import fr.free.nrw.commons.logging.FileLoggingTree +import fr.free.nrw.commons.logging.LogUtils +import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher +import fr.free.nrw.commons.settings.Prefs +import fr.free.nrw.commons.upload.FileUtils +import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar +import io.reactivex.Completable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.internal.functions.Functions +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.Schedulers +import org.acra.ACRA.init +import org.acra.ReportField +import org.acra.annotation.AcraCore +import org.acra.annotation.AcraDialog +import org.acra.annotation.AcraMailSender +import org.acra.data.StringFormat +import timber.log.Timber +import timber.log.Timber.DebugTree +import java.io.File +import javax.inject.Inject +import javax.inject.Named + +@AcraCore( + buildConfigClass = BuildConfig::class, + resReportSendSuccessToast = R.string.crash_dialog_ok_toast, + reportFormat = StringFormat.KEY_VALUE_LIST, + reportContent = [ReportField.USER_COMMENT, ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME, ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL, ReportField.STACK_TRACE] +) + +@AcraMailSender(mailTo = "commons-app-android-private@googlegroups.com", reportAsFile = false) + +@AcraDialog( + resTheme = R.style.Theme_AppCompat_Dialog, + resText = R.string.crash_dialog_text, + resTitle = R.string.crash_dialog_title, + resCommentPrompt = R.string.crash_dialog_comment_prompt +) + +class CommonsApplication : MultiDexApplication() { + + @Inject + lateinit var sessionManager: SessionManager + + @Inject + lateinit var dbOpenHelper: DBOpenHelper + + @Inject + @field:Named("default_preferences") + lateinit var defaultPrefs: JsonKvStore + + @Inject + lateinit var cookieJar: CommonsCookieJar + + @Inject + lateinit var customOkHttpNetworkFetcher: CustomOkHttpNetworkFetcher + + var languageLookUpTable: AppLanguageLookUpTable? = null + private set + + @Inject + lateinit var contributionDao: ContributionDao + + /** + * Used to declare and initialize various components and dependencies + */ + override fun onCreate() { + super.onCreate() + + instance = this + init(this) + + ApplicationlessInjection + .getInstance(this) + .commonsApplicationComponent + .inject(this) + + initTimber() + + if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) { + var defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS) + if (null == defaultExifTagsSet) { + defaultExifTagsSet = HashSet() + } + defaultExifTagsSet.add(getString(R.string.exif_tag_location)) + defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet) + } + + // Set DownsampleEnabled to True to downsample the image in case it's heavy + val config = ImagePipelineConfig.newBuilder(this) + .setNetworkFetcher(customOkHttpNetworkFetcher) + .setDownsampleEnabled(true) + .build() + try { + Fresco.initialize(this, config) + } catch (e: Exception) { + Timber.e(e) + // TODO: Remove when we're able to initialize Fresco in test builds. + } + + createNotificationChannel(this) + + languageLookUpTable = AppLanguageLookUpTable(this) + + // This handler will catch exceptions thrown from Observables after they are disposed, + // or from Observables that are (deliberately or not) missing an onError handler. + RxJavaPlugins.setErrorHandler(Functions.emptyConsumer()) + + // Fire progress callbacks for every 3% of uploaded content + System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0") + } + + /** + * Plants debug and file logging tree. Timber lets you plant your own logging trees. + */ + private fun initTimber() { + val isBeta = isBetaFlavour + val logFileName = + if (isBeta) "CommonsBetaAppLogs" else "CommonsAppLogs" + val logDirectory = LogUtils.getLogDirectory() + //Delete stale logs if they have exceeded the specified size + deleteStaleLogs(logFileName, logDirectory) + + val tree = FileLoggingTree( + Log.VERBOSE, + logFileName, + logDirectory, + 1000, + fileLoggingThreadPool + ) + + Timber.plant(tree) + Timber.plant(DebugTree()) + } + + /** + * Deletes the logs zip file at the specified directory and file locations specified in the + * params + * + * @param logFileName + * @param logDirectory + */ + private fun deleteStaleLogs(logFileName: String, logDirectory: String) { + try { + val file = File("$logDirectory/zip/$logFileName.zip") + if (file.exists() && file.totalSpace > 1000000) { // In Kbs + file.delete() + } + } catch (e: Exception) { + Timber.e(e) + } + } + + private val fileLoggingThreadPool: ThreadPoolService + get() = ThreadPoolService.Builder("file-logging-thread") + .setPriority(Process.THREAD_PRIORITY_LOWEST) + .setPoolSize(1) + .setExceptionHandler(BackgroundPoolExceptionHandler()) + .build() + + val userAgent: String + get() = ("Commons/" + this.getVersionNameWithSha() + + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE) + + /** + * clears data of current application + * + * @param context Application context + * @param logoutListener Implementation of interface LogoutListener + */ + @SuppressLint("CheckResult") + fun clearApplicationData(context: Context, logoutListener: LogoutListener) { + val cacheDirectory = context.cacheDir + val applicationDirectory = File(cacheDirectory.parent) + if (applicationDirectory.exists()) { + val fileNames = applicationDirectory.list() + for (fileName in fileNames) { + if (fileName != "lib") { + FileUtils.deleteFile(File(applicationDirectory, fileName)) + } + } + } + + sessionManager.logout() + .andThen(Completable.fromAction { cookieJar.clear() }) + .andThen(Completable.fromAction { + Timber.d("All accounts have been removed") + clearImageCache() + //TODO: fix preference manager + defaultPrefs.clearAll() + defaultPrefs.putBoolean("firstrun", false) + updateAllDatabases() + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ logoutListener.onLogoutComplete() }, { t: Throwable? -> Timber.e(t) }) + } + + /** + * Clear all images cache held by Fresco + */ + private fun clearImageCache() { + val imagePipeline = Fresco.getImagePipeline() + imagePipeline.clearCaches() + } + + /** + * Deletes all tables and re-creates them. + */ + private fun updateAllDatabases() { + dbOpenHelper.readableDatabase.close() + val db = dbOpenHelper.writableDatabase + + CategoryDao.Table.onDelete(db) + dbOpenHelper.deleteTable( + db, + DBOpenHelper.CONTRIBUTIONS_TABLE + ) //Delete the contributions table in the existing db on older versions + + try { + contributionDao.deleteAll() + } catch (e: SQLiteException) { + Timber.e(e) + } + BookmarkPicturesDao.Table.onDelete(db) + BookmarkLocationsDao.Table.onDelete(db) + BookmarkItemsDao.Table.onDelete(db) + } + + + /** + * Interface used to get log-out events + */ + interface LogoutListener { + fun onLogoutComplete() + } + + /** + * This listener is responsible for handling post-logout actions, specifically invoking the LoginActivity + * with relevant intent parameters. It does not perform the actual logout operation. + */ + open class BaseLogoutListener : LogoutListener { + var ctx: Context + var loginMessage: String? = null + var userName: String? = null + + /** + * Constructor for BaseLogoutListener. + * + * @param ctx Application context + */ + constructor(ctx: Context) { + this.ctx = ctx + } + + /** + * Constructor for BaseLogoutListener + * + * @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. + * @param loginMessage Message to be displayed on the login page + * @param loginUsername Username to be pre-filled on the login page + */ + constructor( + ctx: Context, loginMessage: String?, + loginUsername: String? + ) { + this.ctx = ctx + this.loginMessage = loginMessage + this.userName = loginUsername + } + + override fun onLogoutComplete() { + Timber.d("Logout complete callback received.") + val loginIntent = Intent(ctx, LoginActivity::class.java) + loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + if (loginMessage != null) { + loginIntent.putExtra(LOGIN_MESSAGE_INTENT_KEY, loginMessage) + } + if (userName != null) { + loginIntent.putExtra(LOGIN_USERNAME_INTENT_KEY, userName) + } + + ctx.startActivity(loginIntent) + } + } + + /** + * This class is an extension of BaseLogoutListener, providing additional functionality or customization + * for the logout process. It includes specific actions to be taken during logout, such as handling redirection to the login screen. + */ + class ActivityLogoutListener : BaseLogoutListener { + var activity: Activity + + + /** + * Constructor for ActivityLogoutListener. + * + * @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity. + * @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. + */ + constructor(activity: Activity, ctx: Context) : super(ctx) { + this.activity = activity + } + + /** + * Constructor for ActivityLogoutListener with additional parameters for the login screen. + * + * @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity. + * @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. + * @param loginMessage Message to be displayed on the login page after logout. + * @param loginUsername Username to be pre-filled on the login page after logout. + */ + constructor( + activity: Activity, ctx: Context?, + loginMessage: String?, loginUsername: String? + ) : super(activity, loginMessage, loginUsername) { + this.activity = activity + } + + override fun onLogoutComplete() { + super.onLogoutComplete() + activity.finish() + } + } + + companion object { + + const val LOGIN_MESSAGE_INTENT_KEY: String = "loginMessage" + const val LOGIN_USERNAME_INTENT_KEY: String = "loginUsername" + + const val IS_LIMITED_CONNECTION_MODE_ENABLED: String = "is_limited_connection_mode_enabled" + + /** + * Constants begin + */ + const val OPEN_APPLICATION_DETAIL_SETTINGS: Int = 1001 + + const val DEFAULT_EDIT_SUMMARY: String = "Uploaded using [[COM:MOA|Commons Mobile App]]" + + const val FEEDBACK_EMAIL: String = "commons-app-android@googlegroups.com" + + const val FEEDBACK_EMAIL_SUBJECT: String = "Commons Android App Feedback" + + const val REPORT_EMAIL: String = "commons-app-android-private@googlegroups.com" + + const val REPORT_EMAIL_SUBJECT: String = "Report a violation" + + const val NOTIFICATION_CHANNEL_ID_ALL: String = "CommonsNotificationAll" + + const val FEEDBACK_EMAIL_TEMPLATE_HEADER: String = "-- Technical information --" + + /** + * Constants End + */ + + @JvmStatic + lateinit var instance: CommonsApplication + private set + + @JvmField + var isPaused: Boolean = false + + @JvmStatic + fun createNotificationChannel(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val manager = context + .getSystemService(NOTIFICATION_SERVICE) as NotificationManager + var channel = manager + .getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL) + if (channel == null) { + channel = NotificationChannel( + NOTIFICATION_CHANNEL_ID_ALL, + context.getString(R.string.notifications_channel_name_all), + NotificationManager.IMPORTANCE_DEFAULT + ) + manager.createNotificationChannel(channel) + } + } + } + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt index de716db99..af305c9c6 100644 --- a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt @@ -32,7 +32,7 @@ class ThanksClient revisionId.toString(), // Rev null, // Log csrfTokenClient.getTokenBlocking(), // Token - CommonsApplication.getInstance().userAgent, // Source + CommonsApplication.instance.userAgent, // Source ).map { mwThankPostResponse -> mwThankPostResponse.result?.success == 1 } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 0b6d1831c..3ff61e511 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -50,8 +50,8 @@ import timber.log.Timber; import static android.view.KeyEvent.KEYCODE_ENTER; import static android.view.View.VISIBLE; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; -import static fr.free.nrw.commons.CommonsApplication.loginMessageIntentKey; -import static fr.free.nrw.commons.CommonsApplication.loginUsernameIntentKey; +import static fr.free.nrw.commons.CommonsApplication.LOGIN_MESSAGE_INTENT_KEY; +import static fr.free.nrw.commons.CommonsApplication.LOGIN_USERNAME_INTENT_KEY; public class LoginActivity extends AccountAuthenticatorActivity { @@ -94,8 +94,8 @@ public class LoginActivity extends AccountAuthenticatorActivity { binding = ActivityLoginBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - String message = getIntent().getStringExtra(loginMessageIntentKey); - String username = getIntent().getStringExtra(loginUsernameIntentKey); + String message = getIntent().getStringExtra(LOGIN_MESSAGE_INTENT_KEY); + String username = getIntent().getStringExtra(LOGIN_USERNAME_INTENT_KEY); binding.loginUsername.addTextChangedListener(textWatcher); binding.loginPassword.addTextChangedListener(textWatcher); diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt index cfd7f36b9..7ed598637 100644 --- a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt @@ -258,7 +258,7 @@ class DescriptionEditActivity : username, ) - val commonsApplication = CommonsApplication.getInstance() + val commonsApplication = CommonsApplication.instance if (commonsApplication != null) { commonsApplication.clearApplicationData(this, logoutListener) } @@ -291,7 +291,7 @@ class DescriptionEditActivity : username, ) - val commonsApplication = CommonsApplication.getInstance() + val commonsApplication = CommonsApplication.instance if (commonsApplication != null) { commonsApplication.clearApplicationData(this, logoutListener) } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index fb2ca7b3a..15a049489 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -438,7 +438,7 @@ class UploadWorker( username, ) CommonsApplication - .getInstance() + .instance!! .clearApplicationData(appContext, logoutListener) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt index d409016ae..b3fb19c10 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt @@ -4,6 +4,8 @@ import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.verify import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.auth.csrf.CsrfTokenClient +import io.mockk.every +import io.mockk.mockkObject import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -29,7 +31,6 @@ class ThanksClientTest { private lateinit var commonsApplication: CommonsApplication private lateinit var thanksClient: ThanksClient - private lateinit var mockedApplication: MockedStatic /** * initial setup, test environment @@ -38,8 +39,8 @@ class ThanksClientTest { @Throws(Exception::class) fun setUp() { MockitoAnnotations.openMocks(this) - mockedApplication = Mockito.mockStatic(CommonsApplication::class.java) - `when`(CommonsApplication.getInstance()).thenReturn(commonsApplication) + mockkObject(CommonsApplication) + every { CommonsApplication.instance }.returns(commonsApplication) thanksClient = ThanksClient(csrfTokenClient, service) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditActivityUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditActivityUnitTest.kt index 00f438e1e..be3b7e8e3 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditActivityUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditActivityUnitTest.kt @@ -10,6 +10,7 @@ import android.os.Looper import android.view.LayoutInflater import android.view.View import androidx.recyclerview.widget.RecyclerView +import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.Media import fr.free.nrw.commons.R import fr.free.nrw.commons.TestCommonsApplication @@ -19,6 +20,8 @@ import fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT import fr.free.nrw.commons.settings.Prefs import fr.free.nrw.commons.upload.UploadMediaDetail import fr.free.nrw.commons.upload.UploadMediaDetailAdapter +import io.mockk.every +import io.mockk.mockkObject import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Before @@ -54,6 +57,9 @@ class DescriptionEditActivityUnitTest { @Mock private lateinit var rvDescriptions: RecyclerView + @Mock + private lateinit var commonsApplication: CommonsApplication + private lateinit var media: Media @Before @@ -82,6 +88,8 @@ class DescriptionEditActivityUnitTest { bundle.putString(Prefs.DESCRIPTION_LANGUAGE, "bn") bundle.putParcelable("media", media) intent.putExtras(bundle) + mockkObject(CommonsApplication) + every { CommonsApplication.instance }.returns(commonsApplication) activity = Robolectric.buildActivity(DescriptionEditActivity::class.java, intent).create().get() binding = ActivityDescriptionEditBinding.inflate(LayoutInflater.from(activity)) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadClientTest.kt index 97aac88fe..50130106a 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadClientTest.kt @@ -10,7 +10,7 @@ import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.whenever -import fr.free.nrw.commons.CommonsApplication.DEFAULT_EDIT_SUMMARY +import fr.free.nrw.commons.CommonsApplication.Companion.DEFAULT_EDIT_SUMMARY import fr.free.nrw.commons.auth.csrf.CsrfTokenClient import fr.free.nrw.commons.contributions.ChunkInfo import fr.free.nrw.commons.contributions.Contribution From cdc4f89da551b9be51ee17f488a6c766e6decbd0 Mon Sep 17 00:00:00 2001 From: Rohit Verma <101377978+rohit9625@users.noreply.github.com> Date: Sun, 27 Oct 2024 19:08:40 +0530 Subject: [PATCH 13/74] Database bug fix (#5902) * make database function calls suspending and update room version * replace MainScope with coroutineScope for database operations * add suspend keyword and refactor code --- .../database/NotForUploadStatusDao.kt | 10 +++++----- .../customselector/database/UploadedStatusDao.kt | 14 +++++++------- .../customselector/ui/selector/ImageLoader.kt | 4 ++-- .../fr/free/nrw/commons/nearby/PlaceDao.java | 12 +++--------- .../nrw/commons/upload/depicts/DepictsDao.kt | 16 ++++++++-------- .../nrw/commons/upload/worker/UploadWorker.kt | 4 ++-- gradle.properties | 2 +- 7 files changed, 28 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt index 872388f40..b75a6e1d4 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt @@ -15,19 +15,19 @@ abstract class NotForUploadStatusDao { * Insert into Not For Upload status. */ @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insert(notForUploadStatus: NotForUploadStatus) + abstract suspend fun insert(notForUploadStatus: NotForUploadStatus) /** * Delete Not For Upload status entry. */ @Delete - abstract fun delete(notForUploadStatus: NotForUploadStatus) + abstract suspend fun delete(notForUploadStatus: NotForUploadStatus) /** * Query Not For Upload status with image sha1. */ @Query("SELECT * FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") - abstract fun getFromImageSHA1(imageSHA1: String): NotForUploadStatus? + abstract suspend fun getFromImageSHA1(imageSHA1: String): NotForUploadStatus? /** * Asynchronous image sha1 query. @@ -38,7 +38,7 @@ abstract class NotForUploadStatusDao { * Deletion Not For Upload status with image sha1. */ @Query("DELETE FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") - abstract fun deleteWithImageSHA1(imageSHA1: String) + abstract suspend fun deleteWithImageSHA1(imageSHA1: String) /** * Asynchronous image sha1 deletion. @@ -49,5 +49,5 @@ abstract class NotForUploadStatusDao { * Check whether the imageSHA1 is present in database */ @Query("SELECT COUNT() FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") - abstract fun find(imageSHA1: String): Int + abstract suspend fun find(imageSHA1: String): Int } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt index 03cbb176f..378af5b8d 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt @@ -17,31 +17,31 @@ abstract class UploadedStatusDao { * Insert into uploaded status. */ @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insert(uploadedStatus: UploadedStatus) + abstract suspend fun insert(uploadedStatus: UploadedStatus) /** * Update uploaded status entry. */ @Update - abstract fun update(uploadedStatus: UploadedStatus) + abstract suspend fun update(uploadedStatus: UploadedStatus) /** * Delete uploaded status entry. */ @Delete - abstract fun delete(uploadedStatus: UploadedStatus) + abstract suspend fun delete(uploadedStatus: UploadedStatus) /** * Query uploaded status with image sha1. */ @Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ") - abstract fun getFromImageSHA1(imageSHA1: String): UploadedStatus? + abstract suspend fun getFromImageSHA1(imageSHA1: String): UploadedStatus? /** * Query uploaded status with modified image sha1. */ @Query("SELECT * FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) ") - abstract fun getFromModifiedImageSHA1(modifiedImageSHA1: String): UploadedStatus? + abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1: String): UploadedStatus? /** * Asynchronous insert into uploaded status table. @@ -55,7 +55,7 @@ abstract class UploadedStatusDao { * Check whether the imageSHA1 is present in database */ @Query("SELECT COUNT() FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) AND imageResult = (:imageResult) ") - abstract fun findByImageSHA1( + abstract suspend fun findByImageSHA1( imageSHA1: String, imageResult: Boolean, ): Int @@ -66,7 +66,7 @@ abstract class UploadedStatusDao { @Query( "SELECT COUNT() FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) AND modifiedImageResult = (:modifiedImageResult) ", ) - abstract fun findByModifiedImageSHA1( + abstract suspend fun findByModifiedImageSHA1( modifiedImageSHA1: String, modifiedImageResult: Boolean, ): Int diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt index 1fb5c5953..95c768c1c 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt @@ -17,7 +17,7 @@ import fr.free.nrw.commons.utils.CustomSelectorUtils import fr.free.nrw.commons.utils.CustomSelectorUtils.Companion.checkWhetherFileExistsOnCommonsUsingSHA1 import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.MainScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.Calendar import java.util.concurrent.TimeUnit @@ -65,7 +65,7 @@ class ImageLoader /** * Coroutine Scope. */ - private val scope: CoroutineScope = MainScope() + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) /** * Query image and setUp the view. diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java index 7babee3b7..9e4292114 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java @@ -37,15 +37,11 @@ public abstract class PlaceDao { */ public Completable save(final Place place) { return Completable - .fromAction(() -> { - saveSynchronous(place); - }); + .fromAction(() -> saveSynchronous(place)); } /** * Deletes all Place objects from the database. - * - * @return A Completable that completes once the deletion operation is done. */ @Query("DELETE FROM place") public abstract void deleteAllSynchronous(); @@ -53,11 +49,9 @@ public abstract class PlaceDao { /** * Deletes all Place objects from the database. * + * @return A Completable that completes once the deletion operation is done. */ public Completable deleteAll() { - return Completable - .fromAction(() -> { - deleteAllSynchronous(); - }); + return Completable.fromAction(this::deleteAllSynchronous); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt index c20d65abf..139b67d59 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt @@ -22,21 +22,21 @@ abstract class DepictsDao { private val maxItemsAllowed = 10 @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insert(depictedItem: Depicts) + abstract suspend fun insert(depictedItem: Depicts) @Query("Select * From depicts_table order by lastUsed DESC") - abstract fun getAllDepicts(): List + abstract suspend fun getAllDepicts(): List @Query("Select * From depicts_table order by lastUsed DESC LIMIT :n OFFSET 10") - abstract fun getDepictsForDeletion(n: Int): List + abstract suspend fun getDepictsForDeletion(n: Int): List @Delete - abstract fun delete(depicts: Depicts) + abstract suspend fun delete(depicts: Depicts) /** * Gets all Depicts objects from the database, ordered by lastUsed in descending order. * - * @return A list of Depicts objects. + * @return Deferred list of Depicts objects. */ fun depictsList(): Deferred> = CoroutineScope(Dispatchers.IO).async { @@ -48,7 +48,7 @@ abstract class DepictsDao { * * @param depictedItem The Depicts object to insert. */ - private fun insertDepict(depictedItem: Depicts) = + fun insertDepict(depictedItem: Depicts) = CoroutineScope(Dispatchers.IO).launch { insert(depictedItem) } @@ -59,7 +59,7 @@ abstract class DepictsDao { * @param n The number of depicts to delete. * @return A list of Depicts objects to delete. */ - private suspend fun depictsForDeletion(n: Int): Deferred> = + fun depictsForDeletion(n: Int): Deferred> = CoroutineScope(Dispatchers.IO).async { getDepictsForDeletion(n) } @@ -69,7 +69,7 @@ abstract class DepictsDao { * * @param depicts The Depicts object to delete. */ - private suspend fun deleteDepicts(depicts: Depicts) = + fun deleteDepicts(depicts: Depicts) = CoroutineScope(Dispatchers.IO).launch { delete(depicts) } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index 15a049489..144c503bb 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -41,8 +41,8 @@ import fr.free.nrw.commons.upload.UploadProgressActivity import fr.free.nrw.commons.upload.UploadResult import fr.free.nrw.commons.wikidata.WikidataEditService import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber @@ -534,7 +534,7 @@ class UploadWorker( contribution.contentUri?.let { val imageSha1 = contribution.imageSHA1.toString() val modifiedSha1 = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(contribution.localUri?.path)) - MainScope().launch { + CoroutineScope(Dispatchers.IO).launch { uploadedStatusDao.insertUploaded( UploadedStatus( imageSha1, diff --git a/gradle.properties b/gradle.properties index 9ca154b75..0aee97f4e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,7 +20,7 @@ android.enableR8.fullMode=false KOTLIN_VERSION=1.9.22 LEAK_CANARY_VERSION=2.10 DAGGER_VERSION=2.23 -ROOM_VERSION=2.5.0 +ROOM_VERSION=2.6.1 PREFERENCE_VERSION=1.1.0 CORE_KTX_VERSION=1.9.0 ADAPTER_DELEGATES_VERSION=4.3.0 From 522f1fe1922651d5a9ae11d11ec80c9a2d5a621b Mon Sep 17 00:00:00 2001 From: u7119288 <141695960+baijun6@users.noreply.github.com> Date: Mon, 28 Oct 2024 00:59:09 +1100 Subject: [PATCH 14/74] Partial fixes for errors and warnings reported by ./gradlew lint (#5885) * BaseMarker.kt: removed unneeded cast * TransformImageImpl.kt: removed unreachable code * ZoomableActivity.kt: removed Unnecessary safe call on a non-null receiver of type ZoomableDraweeView * ZoomableActivity.kt: removed Unnecessary safe call on a non-null receiver of type ZoomableDraweeView * DescriptionEditActivity.kt: removed unnecessary non-null assertion (!!) on a non-null receiver of type DescriptionEditHelper * Media.kt: Property would not be serialized into a 'Parcel'. Added '@IgnoredOnParcel' annotation to remove the warning * ZoomableActivity.kt: removed Unnecessary non-null assertion (!!) on a non-null receiver of type ZoomableDraweeView * CategoryClient.kt: removed condition 'page.categoryInfo() == null' as it's always 'false' * DescriptionEditActivity.kt: removed unnecessary safe call on a non-null receiver of type SessionManager * DescriptionEditActivity.kt: removed unnecessary safe call on a non-null receiver of type DescriptionEditHelper * WikidataFeedback.kt: removed unneeded cast * FailedUploadsFragment.kt: removed unneeded non-null assertion (!!) * PendingUploadsFragment.kt: removed unneeded non-null assertion (!!) * AchievementsFragment.java: Changed toUpperCase to toUpperCase(Locale.getDefault()) * ExploreFragment.java: Changed toUpperCase to toUpperCase(Locale.getDefault()) * FileUtils.java: Changed toUpperCase to toUpperCase(Locale.getDefault()) * AchievementsFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * ExploreFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * LocationPickerActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * MediaDetailFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * NearbyFilterSearchRecyclerViewAdapter.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * ProfileActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * RecentSearchesFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * ReviewActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * SearchActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * UploadMediaPresenter.java: Changed toUpperCase to toLowerCase(Locale.ROOT) * CategoriesMediaFragment.kt: Changed arguments!! to requireArguments() * ChildDepictionsFragment.kt: Changed arguments!! to requireArguments() * DepictedImagesFragment.kt: Changed arguments!! to requireArguments() * DepictsFragment: Changed Objects.requireNonNull(getView()) to requireViews(), Objects.requireNonNull(getActivity())) to requireActivity() * ParentCategoriesFragment.kt: Changed arguments!! to requireArguments() * ParentCategoriesFragment.kt: Changed arguments!! to requireArguments() * SubCategoriesFragment.kt: Changed arguments!! to requireArguments() * SubCategoriesFragment.kt: Changed Objects.requireNonNull(getView()) to requireViews(), Objects.requireNonNull(getActivity()) to requireActivity() * UploadMediaDetailFragment.java: Changed arguments!! to requireArguments() * WikipediaInstructionsDialogFragment.kt: Changed arguments!! to requireArguments() * BookmarkItemsDao.java: Added @SuppressLint("Range"), as -1 is expected behavior not index doesn't exist for getColumnIndex() * BookmarkLocationsDao.java: Added @SuppressLint("Range"), as -1 is expected behavior not index doesn't exist for getColumnIndex() * AndroidManifest.xml: Removed redundant label android:label="@string/app_name" * bs\strings.xml: Added missing few quantity * hr\strings.xml: Added missing few quantity * hr\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63 * Revert "hr\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63" This reverts commit 47232466ab4fc3ac91958f888bc5033fefc699a5. * cy\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63 * sr\strings.xml: Added missing few quantities for lines 35 to 70 * ro\strings.xml: Added missing few quantity and removed not needed zero for * cs\strings.xml: Added missing few many missing quantities for lines 33 - 74 * lt\strings.xml: Added missing few, many missing quantities for lines 34 - 58 * lt\strings.xml: Replaced . . . with ... * ca\strings.xml: Added missing many quantities for lines 21 - 51, replaced . . . with ... * ser\strings.xml: Added missing few quantities, replaced . . . with ... * br\strings.xml: Added missing two, few, many quantities * pt\strings.xml: Added missing many quantity, changed . . . to ... and ignored typo as it is correct for European Portuguese * it\strings.xml: changed . . . to ... * pt\strings.xml: fixed many quantity * ca\strings.xml: fixed many quantity * sr\strings.xml: fixed many quantity * cy\strings.xml: corrected quantities for "share_license_summary * fr\strings.xml: changed . . . to ... and add many quantities using the other quantity * fr\strings.xml: changed . . . to ... and add many quantities using the other quantity. Fixed some typos, added ignore for correct spellings but has warning * getColumnIndex(): added @SuppressLint("Range") as -1 is expected result for column name doesn't exist * values-b+sr+Latn\strings.xml: changed . . . to ... * Revert "values-b+sr+Latn\strings.xml: changed . . . to ..." This reverts commit 95b909c29f7f96fe0a885a22daa7fd1a3568e058. * values-b+roa+tara\strings.xml: changed . . . to ... * Revert "values-b+roa+tara\strings.xml: changed . . . to ..." This reverts commit b5db1a3e68a9abfb95b4fc3f6219b507ead16d16. * values-b+roa+tara\strings.xml: changed . . . to ... * values-b+sr+Latn\strings.xml: changed . . . to ..., add few based on other quantity. Ignored one ImpliedQuantity warning as it is correct. * it\strings.xml: changed . . . to ..., add many based on other quantity. * pt-rBR\strings.xml: changed . . . to ..., add many based on other quantity. Fixed typos, ignored warning for "one" quantity as translation didn't use number * si\strings.xml: Ignored ImpliedQuantity warning as it uses 1 not %d. Removed not needed zero quantity * si\strings.xml: Ignored ImpliedQuantity warning as it uses 1 not %d. Removed not needed zero quantity. Fixed wrong %1$d. Changed . . . to ... * mk\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use 1 * sl\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d * ru\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d * uk\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d * is\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d * strings.xml: changed . . . to ... * af\strings.xml: removed not needed zero quantity * de\strings.xml: fixed duplicate word typo * diq\strings.xml: changed - to dash (-) * hi\strings.xml: removed not needed zero * in\strings.xml: removed not needed one quantity * iw\strings.xml: removed not needed many quantity * ja\strings.xml: removed not needed one quantity * ko\strings.xml: removed not needed one quantity * ky\strings.xml: removed not needed one quantity * mr\strings.xml: removed not needed zero quantity * my\strings.xml: removed not needed one quantity * su\strings.xml: removed not needed one quantity * th\strings.xml: removed not needed one and zero quantity * zh\strings.xml: removed not needed one quantity * activity_description_edit.xml: changed android:tint to app:tint, changed layout_alignParentRight to layout_alignParentEnd * bottom_sheet_details_explore.xml: changed android:tint to app:tint, added focusable, changed to margin layout * bottom_sheet_item_layout.xml: changed android:tint to app:tint, added focusable * bottom_sheet_details_explore.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * item_place.xml.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * layout_campagin.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * layout_contribution.xml.xml: changed android:tint to app:tint * nearby_card_view.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * nearby_row_button.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * toolbar_location_picker.xml: changed android:tint to app:tint --------- Co-authored-by: Nicolas Raoul --- app/src/main/AndroidManifest.xml | 3 +- .../java/fr/free/nrw/commons/BaseMarker.kt | 2 +- .../LocationPickerActivity.java | 4 +- .../main/java/fr/free/nrw/commons/Media.kt | 2 + .../bookmarks/items/BookmarkItemsDao.java | 2 + .../locations/BookmarkLocationsDao.java | 2 + .../pictures/BookmarkPicturesDao.java | 2 + .../nrw/commons/category/CategoryClient.kt | 2 +- .../nrw/commons/category/CategoryDao.java | 2 + .../WikipediaInstructionsDialogFragment.kt | 2 +- .../description/DescriptionEditActivity.kt | 6 +- .../nrw/commons/edit/TransformImageImpl.kt | 1 - .../nrw/commons/explore/ExploreFragment.java | 7 +- .../nrw/commons/explore/SearchActivity.java | 7 +- .../media/CategoriesMediaFragment.kt | 2 +- .../parent/ParentCategoriesFragment.kt | 2 +- .../categories/sub/SubCategoriesFragment.kt | 2 +- .../child/ChildDepictionsFragment.kt | 4 +- .../media/DepictedImagesFragment.kt | 2 +- .../parent/ParentDepictionsFragment.kt | 4 +- .../recentsearches/RecentSearchesDao.java | 2 + .../RecentSearchesFragment.java | 3 +- .../commons/media/MediaDetailFragment.java | 4 +- .../nrw/commons/media/ZoomableActivity.kt | 18 ++--- ...NearbyFilterSearchRecyclerViewAdapter.java | 5 +- .../nrw/commons/nearby/WikidataFeedback.kt | 2 +- .../nrw/commons/profile/ProfileActivity.java | 5 +- .../achievements/AchievementsFragment.java | 3 +- .../recentlanguages/RecentLanguagesDao.java | 2 + .../nrw/commons/review/ReviewActivity.java | 3 +- .../commons/upload/FailedUploadsFragment.kt | 6 +- .../fr/free/nrw/commons/upload/FileUtils.java | 3 +- .../commons/upload/PendingUploadsFragment.kt | 4 +- .../categories/UploadCategoriesFragment.java | 6 +- .../upload/depicts/DepictsFragment.java | 6 +- .../UploadMediaDetailFragment.java | 2 +- .../mediaDetails/UploadMediaPresenter.java | 4 +- .../res/layout/activity_description_edit.xml | 6 +- .../layout/bottom_sheet_details_explore.xml | 12 ++-- .../res/layout/bottom_sheet_item_layout.xml | 4 +- .../main/res/layout/fragment_achievements.xml | 67 +++++-------------- app/src/main/res/layout/item_place.xml | 13 +--- app/src/main/res/layout/layout_campagin.xml | 19 ++---- .../main/res/layout/layout_contribution.xml | 4 +- app/src/main/res/layout/nearby_card_view.xml | 16 ++--- app/src/main/res/layout/nearby_row_button.xml | 20 +++--- .../res/layout/toolbar_location_picker.xml | 4 +- app/src/main/res/values-ab/strings.xml | 4 +- app/src/main/res/values-af/strings.xml | 3 +- app/src/main/res/values-anp/strings.xml | 10 +-- app/src/main/res/values-ar/strings.xml | 8 +-- app/src/main/res/values-as/strings.xml | 4 +- app/src/main/res/values-ast/strings.xml | 4 +- app/src/main/res/values-az/strings.xml | 2 +- .../main/res/values-b+roa+tara/strings.xml | 6 +- app/src/main/res/values-b+sr+Latn/strings.xml | 19 ++++-- app/src/main/res/values-ba/strings.xml | 8 +-- app/src/main/res/values-ban/strings.xml | 6 +- app/src/main/res/values-bg/strings.xml | 2 +- app/src/main/res/values-blk/strings.xml | 4 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-br/strings.xml | 6 ++ app/src/main/res/values-bs/strings.xml | 5 +- app/src/main/res/values-ca/strings.xml | 8 ++- app/src/main/res/values-ce/strings.xml | 8 +-- app/src/main/res/values-cs/strings.xml | 15 ++++- app/src/main/res/values-csb/strings.xml | 6 +- app/src/main/res/values-cy/strings.xml | 19 ++++++ app/src/main/res/values-da/strings.xml | 10 +-- app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values-diq/strings.xml | 8 +-- app/src/main/res/values-el/strings.xml | 8 +-- app/src/main/res/values-eo/strings.xml | 16 ++--- app/src/main/res/values-es/strings.xml | 38 +++++++---- app/src/main/res/values-eu/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 10 +-- app/src/main/res/values-fi/strings.xml | 10 +-- app/src/main/res/values-fr/strings.xml | 36 ++++++---- app/src/main/res/values-gcr/strings.xml | 6 +- app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 7 +- app/src/main/res/values-hr/strings.xml | 17 +++-- app/src/main/res/values-hu/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 18 +++-- app/src/main/res/values-io/strings.xml | 14 ++-- app/src/main/res/values-is/strings.xml | 8 +-- app/src/main/res/values-it/strings.xml | 15 ++++- app/src/main/res/values-iw/strings.xml | 26 +++---- app/src/main/res/values-ja/strings.xml | 5 +- app/src/main/res/values-kab/strings.xml | 8 +-- app/src/main/res/values-ko/strings.xml | 14 ++-- app/src/main/res/values-krc/strings.xml | 12 ++-- app/src/main/res/values-ku/strings.xml | 6 +- app/src/main/res/values-kum/strings.xml | 2 +- app/src/main/res/values-kus/strings.xml | 18 ++--- app/src/main/res/values-ky/strings.xml | 3 +- app/src/main/res/values-lb/strings.xml | 10 +-- app/src/main/res/values-li/strings.xml | 8 +-- app/src/main/res/values-lt/strings.xml | 21 ++++-- app/src/main/res/values-lv/strings.xml | 4 +- app/src/main/res/values-mk/strings.xml | 12 ++-- app/src/main/res/values-mni/strings.xml | 4 +- app/src/main/res/values-mnw/strings.xml | 4 +- app/src/main/res/values-mr/strings.xml | 3 +- app/src/main/res/values-my/strings.xml | 14 ++-- app/src/main/res/values-nl/strings.xml | 6 +- app/src/main/res/values-nqo/strings.xml | 20 +++--- app/src/main/res/values-oc/strings.xml | 2 +- app/src/main/res/values-pa/strings.xml | 13 ++-- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pms/strings.xml | 6 +- app/src/main/res/values-ps/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 28 +++++--- app/src/main/res/values-pt/strings.xml | 32 ++++++--- app/src/main/res/values-ro/strings.xml | 10 +-- app/src/main/res/values-ru/strings.xml | 14 ++-- app/src/main/res/values-sd/strings.xml | 4 +- app/src/main/res/values-se/strings.xml | 8 +-- app/src/main/res/values-sh/strings.xml | 4 +- app/src/main/res/values-si/strings.xml | 11 ++- app/src/main/res/values-sk/strings.xml | 14 ++-- app/src/main/res/values-sl/strings.xml | 30 ++++----- app/src/main/res/values-sr/strings.xml | 17 +++-- app/src/main/res/values-su/strings.xml | 13 +--- app/src/main/res/values-sv/strings.xml | 4 +- app/src/main/res/values-ta/strings.xml | 4 +- app/src/main/res/values-tcy/strings.xml | 4 +- app/src/main/res/values-te/strings.xml | 12 ++-- app/src/main/res/values-th/strings.xml | 7 +- app/src/main/res/values-tr/strings.xml | 6 +- app/src/main/res/values-uk/strings.xml | 6 +- app/src/main/res/values-uz/strings.xml | 8 +-- app/src/main/res/values-vec/strings.xml | 10 +-- app/src/main/res/values-xal/strings.xml | 8 +-- app/src/main/res/values-xmf/strings.xml | 6 +- app/src/main/res/values-zgh/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 17 ++--- 137 files changed, 617 insertions(+), 572 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89ed630d8..29f280c9e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -99,7 +99,6 @@ android:exported="true" android:hardwareAccelerated="false" android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" android:windowSoftInputMode="adjustResize"> @@ -122,7 +121,7 @@ android:name=".contributions.MainActivity" android:configChanges="screenSize|keyboard|orientation" android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" /> + /> diff --git a/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt b/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt index 1daadb5a1..28b01d603 100644 --- a/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt +++ b/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt @@ -46,7 +46,7 @@ class BaseMarker { val drawable: Drawable = context.resources.getDrawable(drawableResId) icon = if (drawable is BitmapDrawable) { - (drawable as BitmapDrawable).bitmap + drawable.bitmap } else { val bitmap = Bitmap.createBitmap( diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java index 8c54fd292..2f05705ba 100644 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java @@ -53,6 +53,7 @@ import fr.free.nrw.commons.utils.SystemThemeUtils; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import java.util.List; +import java.util.Locale; import javax.inject.Inject; import javax.inject.Named; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; @@ -301,7 +302,8 @@ public class LocationPickerActivity extends BaseActivity implements modifyLocationButton = findViewById(R.id.modify_location); removeLocationButton = findViewById(R.id.remove_location); showInMapButton = findViewById(R.id.show_in_map); - showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase()); + showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase( + Locale.ROOT)); shadow = findViewById(R.id.location_picker_image_view_shadow); } diff --git a/app/src/main/java/fr/free/nrw/commons/Media.kt b/app/src/main/java/fr/free/nrw/commons/Media.kt index 93efac7b2..025302cfd 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.kt +++ b/app/src/main/java/fr/free/nrw/commons/Media.kt @@ -3,6 +3,7 @@ package fr.free.nrw.commons import android.os.Parcelable import fr.free.nrw.commons.location.LatLng import fr.free.nrw.commons.wikidata.model.page.PageTitle +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import java.util.Date import java.util.Locale @@ -124,6 +125,7 @@ class Media constructor( * Gets the categories the file falls under. * @return file categories as an ArrayList of Strings */ + @IgnoredOnParcel var addedCategories: List? = null // TODO added categories should be removed. It is added for a short fix. On category update, // categories should be re-fetched instead diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java index 70c370836..6788a8290 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.bookmarks.items; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -134,6 +135,7 @@ public class BookmarkItemsDao { * @param cursor : Object for storing database data * @return DepictedItem */ + @SuppressLint("Range") DepictedItem fromCursor(final Cursor cursor) { final String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)); final String description diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java index 850b953e9..fe4f603f4 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.bookmarks.locations; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -146,6 +147,7 @@ public class BookmarkLocationsDao { return false; } + @SuppressLint("Range") @NonNull Place fromCursor(final Cursor cursor) { final LatLng location = new LatLng(cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LAT)), diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java index a56a39ba2..c214ae996 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.bookmarks.pictures; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -150,6 +151,7 @@ public class BookmarkPicturesDao { return false; } + @SuppressLint("Range") @NonNull Bookmark fromCursor(Cursor cursor) { String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_NAME)); diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt index 64463d826..992c4ed1c 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt @@ -124,7 +124,7 @@ class CategoryClient }.map { it .filter { page -> - page.categoryInfo() == null || !page.categoryInfo().isHidden + !page.categoryInfo().isHidden }.map { CategoryItem( it.title().replace(CATEGORY_PREFIX, ""), diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java index b638fc508..3cd60ac81 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.category; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -111,6 +112,7 @@ public class CategoryDao { } @NonNull + @SuppressLint("Range") Category fromCursor(Cursor cursor) { // Hardcoding column positions! return new Category( diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt b/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt index 77e52e1db..86cda2cf3 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt @@ -22,7 +22,7 @@ class WikipediaInstructionsDialogFragment : DialogFragment() { ) = DialogAddToWikipediaInstructionsBinding .inflate(inflater, container, false) .apply { - val contribution: Contribution? = arguments!!.getParcelable(ARG_CONTRIBUTION) + val contribution: Contribution? = requireArguments().getParcelable(ARG_CONTRIBUTION) tvWikicode.setText(contribution?.media?.wikiCode) instructionsCancel.setOnClickListener { dismiss() } instructionsConfirm.setOnClickListener { diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt index 7ed598637..fa4349dbf 100644 --- a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt @@ -237,7 +237,7 @@ class DescriptionEditActivity : ) { try { descriptionEditHelper - ?.addDescription( + .addDescription( applicationContext, media, updatedWikiText, @@ -250,7 +250,7 @@ class DescriptionEditActivity : ) } } catch (e: InvalidLoginTokenException) { - val username: String? = sessionManager?.userName + val username: String? = sessionManager.userName val logoutListener = CommonsApplication.BaseLogoutListener( this, @@ -268,7 +268,7 @@ class DescriptionEditActivity : for (mediaDetail in uploadMediaDetails) { try { compositeDisposable.add( - descriptionEditHelper!! + descriptionEditHelper .addCaption( applicationContext, media, diff --git a/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt b/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt index b59619691..c3db1a5a0 100644 --- a/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt +++ b/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt @@ -65,7 +65,6 @@ class TransformImageImpl : TransformImage { } catch (e: LLJTranException) { Timber.tag("Error").d(e) return null - false } if (rotated) { diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java index c66cd5163..26c8dd82b 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java @@ -22,6 +22,7 @@ import fr.free.nrw.commons.theme.BaseActivity; import fr.free.nrw.commons.utils.ActivityUtils; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import javax.inject.Inject; import javax.inject.Named; @@ -112,13 +113,13 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { mobileRootFragment = new ExploreListRootFragment(mobileArguments); mapRootFragment = new ExploreMapRootFragment(mapArguments); fragmentList.add(featuredRootFragment); - titleList.add(getString(R.string.explore_tab_title_featured).toUpperCase()); + titleList.add(getString(R.string.explore_tab_title_featured).toUpperCase(Locale.ROOT)); fragmentList.add(mobileRootFragment); - titleList.add(getString(R.string.explore_tab_title_mobile).toUpperCase()); + titleList.add(getString(R.string.explore_tab_title_mobile).toUpperCase(Locale.ROOT)); fragmentList.add(mapRootFragment); - titleList.add(getString(R.string.explore_tab_title_map).toUpperCase()); + titleList.add(getString(R.string.explore_tab_title_map).toUpperCase(Locale.ROOT)); ((MainActivity)getActivity()).showTabs(); ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); 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 7717f2deb..abb27184f 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 @@ -28,6 +28,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import timber.log.Timber; @@ -95,11 +96,11 @@ public class SearchActivity extends BaseActivity searchDepictionsFragment = new SearchDepictionsFragment(); searchCategoryFragment= new SearchCategoryFragment(); fragmentList.add(searchMediaFragment); - titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase()); + titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase(Locale.ROOT)); fragmentList.add(searchCategoryFragment); - titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase()); + titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase(Locale.ROOT)); fragmentList.add(searchDepictionsFragment); - titleList.add(getResources().getString(R.string.search_tab_title_depictions).toUpperCase()); + titleList.add(getResources().getString(R.string.search_tab_title_depictions).toUpperCase(Locale.ROOT)); viewPagerAdapter.setTabData(fragmentList, titleList); viewPagerAdapter.notifyDataSetChanged(); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/media/CategoriesMediaFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/categories/media/CategoriesMediaFragment.kt index 6de1248b4..765abd698 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/categories/media/CategoriesMediaFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/media/CategoriesMediaFragment.kt @@ -18,6 +18,6 @@ class CategoriesMediaFragment : PageableMediaFragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") + onQueryUpdated("$CATEGORY_PREFIX${requireArguments().getString("categoryName")!!}") } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/parent/ParentCategoriesFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/categories/parent/ParentCategoriesFragment.kt index 6ceccf607..c43e1c6bd 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/categories/parent/ParentCategoriesFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/parent/ParentCategoriesFragment.kt @@ -21,6 +21,6 @@ class ParentCategoriesFragment : PageableCategoryFragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") + onQueryUpdated("$CATEGORY_PREFIX${requireArguments().getString("categoryName")!!}") } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/sub/SubCategoriesFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/categories/sub/SubCategoriesFragment.kt index 19fe52beb..8fbc83039 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/categories/sub/SubCategoriesFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/sub/SubCategoriesFragment.kt @@ -20,6 +20,6 @@ class SubCategoriesFragment : PageableCategoryFragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") + onQueryUpdated("$CATEGORY_PREFIX${requireArguments().getString("categoryName")!!}") } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/child/ChildDepictionsFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/child/ChildDepictionsFragment.kt index 527536299..4f13b1be8 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/child/ChildDepictionsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/child/ChildDepictionsFragment.kt @@ -13,13 +13,13 @@ class ChildDepictionsFragment : PageableDepictionsFragment() { override val injectedPresenter get() = presenter - override fun getEmptyText(query: String) = getString(R.string.no_child_classes, arguments!!.getString("wikidataItemName")!!) + override fun getEmptyText(query: String) = getString(R.string.no_child_classes, requireArguments().getString("wikidataItemName")!!) override fun onViewCreated( view: View, savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated(arguments!!.getString("entityId")!!) + onQueryUpdated(requireArguments().getString("entityId")!!) } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/media/DepictedImagesFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/media/DepictedImagesFragment.kt index cc1b664b2..4cdb0e461 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/media/DepictedImagesFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/media/DepictedImagesFragment.kt @@ -17,6 +17,6 @@ class DepictedImagesFragment : PageableMediaFragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated(arguments!!.getString("entityId")!!) + onQueryUpdated(requireArguments().getString("entityId")!!) } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/parent/ParentDepictionsFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/parent/ParentDepictionsFragment.kt index 52a5aff5d..cf739a07d 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/parent/ParentDepictionsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/parent/ParentDepictionsFragment.kt @@ -13,13 +13,13 @@ class ParentDepictionsFragment : PageableDepictionsFragment() { override val injectedPresenter get() = presenter - override fun getEmptyText(query: String) = getString(R.string.no_parent_classes, arguments!!.getString("wikidataItemName")!!) + override fun getEmptyText(query: String) = getString(R.string.no_parent_classes, requireArguments().getString("wikidataItemName")!!) override fun onViewCreated( view: View, savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated(arguments!!.getString("entityId")!!) + onQueryUpdated(requireArguments().getString("entityId")!!) } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java index 9f12639dd..cee8a25ae 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.explore.recentsearches; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -178,6 +179,7 @@ public class RecentSearchesDao { * @return RecentSearch object */ @NonNull + @SuppressLint("Range") RecentSearch fromCursor(Cursor cursor) { // Hardcoding column positions! return new RecentSearch( diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java index cd98651f0..0db1e5539 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java @@ -15,6 +15,7 @@ import fr.free.nrw.commons.databinding.FragmentSearchHistoryBinding; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.explore.SearchActivity; import java.util.List; +import java.util.Locale; import javax.inject.Inject; @@ -90,7 +91,7 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment { private void showDeleteAlertDialog(@NonNull final Context context, final int position) { new AlertDialog.Builder(context) .setMessage(R.string.delete_search_dialog) - .setPositiveButton(getString(R.string.delete).toUpperCase(), + .setPositiveButton(getString(R.string.delete).toUpperCase(Locale.ROOT), ((dialog, which) -> setDeletePositiveButton(context, dialog, position))) .setNegativeButton(android.R.string.cancel, null) .create() 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 dd0829a1b..142d8379c 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 @@ -600,8 +600,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements // Check if the presented category is about need of category if (categoriesPresent) { for (String category : media.getCategories()) { - if (category.toLowerCase().contains(CATEGORY_NEEDING_CATEGORIES) || - category.toLowerCase().contains(CATEGORY_UNCATEGORISED)) { + if (category.toLowerCase(Locale.ROOT).contains(CATEGORY_NEEDING_CATEGORIES) || + category.toLowerCase(Locale.ROOT).contains(CATEGORY_UNCATEGORISED)) { categoriesPresent = false; } break; diff --git a/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt b/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt index d08e3048c..14b5788c2 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt @@ -219,7 +219,7 @@ class ZoomableActivity : BaseActivity() { onSwipe() } } - binding.zoomProgressBar?.let { + binding.zoomProgressBar.let { it.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE } } @@ -234,7 +234,7 @@ class ZoomableActivity : BaseActivity() { sharedPreferences.getBoolean(ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true) if (!images.isNullOrEmpty()) { - binding.zoomable!!.setOnTouchListener( + binding.zoomable.setOnTouchListener( object : OnSwipeTouchListener(this) { // Swipe left to view next image in the folder. (if available) override fun onSwipeLeft() { @@ -271,7 +271,7 @@ class ZoomableActivity : BaseActivity() { * Handles down swipe action */ private fun onDownSwiped() { - if (binding.zoomable?.zoomableController?.isIdentity == false) { + if (binding.zoomable.zoomableController?.isIdentity == false) { return } @@ -341,7 +341,7 @@ class ZoomableActivity : BaseActivity() { * Handles up swipe action */ private fun onUpSwiped() { - if (binding.zoomable?.zoomableController?.isIdentity == false) { + if (binding.zoomable.zoomableController?.isIdentity == false) { return } @@ -414,7 +414,7 @@ class ZoomableActivity : BaseActivity() { * Handles right swipe action */ private fun onRightSwiped(showAlreadyActionedImages: Boolean) { - if (binding.zoomable?.zoomableController?.isIdentity == false) { + if (binding.zoomable.zoomableController?.isIdentity == false) { return } @@ -451,7 +451,7 @@ class ZoomableActivity : BaseActivity() { * Handles left swipe action */ private fun onLeftSwiped(showAlreadyActionedImages: Boolean) { - if (binding.zoomable?.zoomableController?.isIdentity == false) { + if (binding.zoomable.zoomableController?.isIdentity == false) { return } @@ -646,7 +646,7 @@ class ZoomableActivity : BaseActivity() { .setProgressBarImage(ProgressBarDrawable()) .setProgressBarImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) .build() - with(binding.zoomable!!) { + with(binding.zoomable) { setHierarchy(hierarchy) setAllowTouchInterceptionWhileZoomed(true) setIsLongpressEnabled(false) @@ -658,10 +658,10 @@ class ZoomableActivity : BaseActivity() { .setUri(imageUri) .setControllerListener(loadingListener) .build() - binding.zoomable!!.controller = controller + binding.zoomable.controller = controller if (photoBackgroundColor != null) { - binding.zoomable!!.setBackgroundColor(photoBackgroundColor!!) + binding.zoomable.setBackgroundColor(photoBackgroundColor!!) } if (!images.isNullOrEmpty()) { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java index 5d480f4f7..b5f760c9f 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java @@ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import fr.free.nrw.commons.R; +import java.util.Locale; public class NearbyFilterSearchRecyclerViewAdapter extends RecyclerView.Adapter @@ -121,11 +122,11 @@ public class NearbyFilterSearchRecyclerViewAdapter results.count = labels.size(); results.values = labels; } else { - constraint = constraint.toString().toLowerCase(); + constraint = constraint.toString().toLowerCase(Locale.ROOT); for (Label label : labels) { String data = label.toString(); - if (data.toLowerCase().startsWith(constraint.toString())) { + if (data.toLowerCase(Locale.ROOT).startsWith(constraint.toString())) { filteredArrayList.add(Label.fromText(label.getText())); } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt b/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt index d238296d1..299ac4b6e 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt @@ -87,7 +87,7 @@ class WikidataFeedback : BaseActivity() { lat, lng, ) - } as Callable>, + }, ).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ aBoolean: Boolean? -> diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java index 9acf5b595..c6d09fdc6 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java @@ -32,6 +32,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import javax.inject.Inject; /** @@ -139,14 +140,14 @@ public class ProfileActivity extends BaseActivity { leaderboardFragment.setArguments(leaderBoardBundle); fragmentList.add(leaderboardFragment); - titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase()); + titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase(Locale.ROOT)); contributionsFragment = new ContributionsFragment(); Bundle contributionsListBundle = new Bundle(); contributionsListBundle.putString(KEY_USERNAME, userName); contributionsFragment.setArguments(contributionsListBundle); fragmentList.add(contributionsFragment); - titleList.add(getString(R.string.contributions_fragment).toUpperCase()); + titleList.add(getString(R.string.contributions_fragment).toUpperCase(Locale.ROOT)); viewPagerAdapter.setTabData(fragmentList, titleList); viewPagerAdapter.notifyDataSetChanged(); diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java index 46ea631fb..f44b7eb6d 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java @@ -27,6 +27,7 @@ import fr.free.nrw.commons.profile.ProfileActivity; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; +import java.util.Locale; import java.util.Objects; import javax.inject.Inject; import org.apache.commons.lang3.StringUtils; @@ -361,7 +362,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { + levelInfo.getMaxUniqueImages()); binding.imageFeatured.setText(String.valueOf(achievements.getFeaturedImages())); binding.qualityImages.setText(String.valueOf(achievements.getQualityImages())); - String levelUpInfoString = getString(R.string.level).toUpperCase(); + String levelUpInfoString = getString(R.string.level).toUpperCase(Locale.ROOT); levelUpInfoString += " " + levelInfo.getLevelNumber(); binding.achievementLevel.setText(levelUpInfoString); binding.achievementBadgeImage.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge, diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java index c4a4bf518..cbb8c8a1c 100644 --- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.recentlanguages; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -117,6 +118,7 @@ public class RecentLanguagesDao { * @return Language object */ @NonNull + @SuppressLint("Range") Language fromCursor(final Cursor cursor) { // Hardcoding column positions! final String languageName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)); diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java index 5eb758ada..40d743a19 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java @@ -25,6 +25,7 @@ import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; +import java.util.Locale; import javax.inject.Inject; public class ReviewActivity extends BaseActivity { @@ -241,7 +242,7 @@ public class ReviewActivity extends BaseActivity { public void showSkipImageInfo(){ DialogUtil.showAlertDialog(ReviewActivity.this, - getString(R.string.skip_image).toUpperCase(), + getString(R.string.skip_image).toUpperCase(Locale.ROOT), getString(R.string.skip_image_explanation), getString(android.R.string.ok), "", diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt index 876fb3cd3..c0e5097c0 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt @@ -63,7 +63,7 @@ class FailedUploadsFragment : } if (StringUtils.isEmpty(userName)) { - userName = sessionManager!!.getUserName() + userName = sessionManager.getUserName() } } @@ -96,8 +96,8 @@ class FailedUploadsFragment : fun initRecyclerView() { binding.failedUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context)) binding.failedUploadsRecyclerView.adapter = adapter - pendingUploadsPresenter!!.getFailedContributions() - pendingUploadsPresenter!!.failedContributionList.observe( + pendingUploadsPresenter.getFailedContributions() + pendingUploadsPresenter.failedContributionList.observe( viewLifecycleOwner, ) { list: PagedList -> adapter.submitList(list) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index b45e4b57d..8a8fa35b3 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -19,6 +19,7 @@ import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Locale; import timber.log.Timber; public class FileUtils { @@ -139,7 +140,7 @@ public class FileUtils { String fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri .toString()); mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( - fileExtension.toLowerCase()); + fileExtension.toLowerCase(Locale.getDefault())); } return mimeType; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt index 4d79bc88e..4442a64ea 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt @@ -74,8 +74,8 @@ class PendingUploadsFragment : fun initRecyclerView() { binding.pendingUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context)) binding.pendingUploadsRecyclerView.adapter = adapter - pendingUploadsPresenter!!.setup() - pendingUploadsPresenter!!.totalContributionList.observe( + pendingUploadsPresenter.setup() + pendingUploadsPresenter.totalContributionList.observe( viewLifecycleOwner, ) { list: PagedList -> contributionsSize = list.size 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 index 8503b1d05..dd264655f 100644 --- 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 @@ -372,7 +372,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate return false; }); - Objects.requireNonNull(getView()).setFocusableInTouchMode(true); + requireView().setFocusableInTouchMode(true); getView().requestFocus(); getView().setOnKeyListener((v, keyCode, event) -> { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { @@ -387,7 +387,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate }); Objects.requireNonNull( - ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + ((AppCompatActivity) requireActivity()).getSupportActionBar()) .hide(); if (getParentFragment().getParentFragment().getParentFragment() @@ -407,7 +407,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate super.onStop(); if (media != null) { Objects.requireNonNull( - ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + ((AppCompatActivity) requireActivity()).getSupportActionBar()) .show(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java index bd52a8d35..9000e513d 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java @@ -398,7 +398,7 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra return false; }); - Objects.requireNonNull(getView()).setFocusableInTouchMode(true); + requireView().setFocusableInTouchMode(true); getView().requestFocus(); getView().setOnKeyListener((v, keyCode, event) -> { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { @@ -411,7 +411,7 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra }); Objects.requireNonNull( - ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + ((AppCompatActivity) requireActivity()).getSupportActionBar()) .hide(); if (getParentFragment().getParentFragment().getParentFragment() @@ -431,7 +431,7 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra super.onStop(); if (media != null) { Objects.requireNonNull( - ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + ((AppCompatActivity) requireActivity()).getSupportActionBar()) .show(); } } 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 index 2c4c2ecd3..5581cfeb1 100644 --- 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 @@ -825,7 +825,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements @Override public void displayAddLocationDialog(final Runnable onSkipClicked) { isMissingLocationDialog = true; - DialogUtil.showAlertDialog(Objects.requireNonNull(getActivity()), + DialogUtil.showAlertDialog(requireActivity(), getString(R.string.no_location_found_title), getString(R.string.no_location_found_message), getString(R.string.add_location), 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 index 7152d4d8f..cd533401b 100644 --- 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 @@ -129,9 +129,9 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt if (place.location != null) { final String countryCode = reverseGeoCode(place.location); if (countryCode != null && WLM_SUPPORTED_COUNTRIES - .contains(countryCode.toLowerCase())) { + .contains(countryCode.toLowerCase(Locale.ROOT))) { uploadItem.setWLMUpload(true); - uploadItem.setCountryCode(countryCode.toLowerCase()); + uploadItem.setCountryCode(countryCode.toLowerCase(Locale.ROOT)); } } } diff --git a/app/src/main/res/layout/activity_description_edit.xml b/app/src/main/res/layout/activity_description_edit.xml index ed50193a2..1a8d3b8ce 100644 --- a/app/src/main/res/layout/activity_description_edit.xml +++ b/app/src/main/res/layout/activity_description_edit.xml @@ -36,11 +36,11 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:contentDescription="@string/exit_location_picker" - android:tint="@color/white" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/ic_arrow_back_white" /> + app:srcCompat="@drawable/ic_arrow_back_white" + app:tint="@color/white" /> @@ -69,7 +69,7 @@ android:id="@+id/btn_edit_submit" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" android:text="@string/submit" android:textColor="@android:color/white" /> diff --git a/app/src/main/res/layout/bottom_sheet_details_explore.xml b/app/src/main/res/layout/bottom_sheet_details_explore.xml index 1da5c7f3e..6558c9afe 100644 --- a/app/src/main/res/layout/bottom_sheet_details_explore.xml +++ b/app/src/main/res/layout/bottom_sheet_details_explore.xml @@ -31,7 +31,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" - android:layout_marginRight="50dp" + android:layout_marginEnd="50dp" android:maxLines="2" android:ellipsize="end" /> @@ -58,6 +58,7 @@ android:layout_width="@dimen/dimen_0" android:layout_height="wrap_content" android:layout_weight="1" + android:focusable="true" android:padding="@dimen/standard_gap" android:clickable="true" android:background="@drawable/button_background_selector" @@ -69,8 +70,7 @@ android:layout_gravity="center_horizontal" android:duplicateParentState="true" app:srcCompat="@drawable/ic_directions_black_24dp" - android:tint="?attr/rowButtonColor" - /> + app:tint="?attr/rowButtonColor" /> diff --git a/app/src/main/res/layout/bottom_sheet_item_layout.xml b/app/src/main/res/layout/bottom_sheet_item_layout.xml index 4f4c2c854..c569e523a 100644 --- a/app/src/main/res/layout/bottom_sheet_item_layout.xml +++ b/app/src/main/res/layout/bottom_sheet_item_layout.xml @@ -1,11 +1,13 @@ @@ -14,7 +16,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:tint="?attr/rowButtonColor" /> + app:tint="?attr/rowButtonColor" /> @@ -36,7 +35,6 @@ style="?android:textAppearanceLarge" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_marginTop="@dimen/activity_margin_horizontal" android:text="@string/level" @@ -48,13 +46,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/activity_margin_vertical" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/black" - android:layout_marginVertical="@dimen/activity_margin_vertical" /> + android:layout_marginVertical="@dimen/activity_margin_vertical" + app:tint="@color/black" /> + android:layout_marginStart="@dimen/activity_margin_horizontal" + app:tint="@color/primaryLightColor" /> @@ -189,7 +182,6 @@ style="?android:textAppearanceMedium" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:id="@+id/images_reverted_text" android:layout_marginStart="@dimen/activity_margin_horizontal" android:text="@string/image_reverts" /> @@ -200,24 +192,19 @@ android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" - android:layout_toRightOf="@+id/images_reverted_text" - android:layout_toEndOf="@+id/images_reverted_text" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" android:layout_marginLeft="@dimen/activity_margin_horizontal" - android:layout_marginStart="@dimen/activity_margin_horizontal"/> + android:layout_marginStart="@dimen/activity_margin_horizontal" app:tint="@color/primaryLightColor" /> - @@ -278,7 +265,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/images_used_by_wiki_text" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_marginTop="@dimen/achievements_activity_margin_vertical" android:text="@string/images_used_by_wiki" /> @@ -289,12 +275,10 @@ android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" - android:layout_toRightOf="@+id/images_used_by_wiki_text" - android:layout_toEndOf="@+id/images_used_by_wiki_text" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" android:layout_marginLeft="@dimen/activity_margin_horizontal" - android:layout_marginStart="@dimen/activity_margin_horizontal"/> + android:layout_marginStart="@dimen/activity_margin_horizontal" + app:tint="@color/primaryLightColor" /> @@ -353,7 +337,6 @@ android:layout_height="wrap_content" android:text="@string/statistics" style="?android:textAppearanceLarge" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_marginTop="@dimen/activity_margin_vertical" android:textAllCaps="true"/> @@ -373,9 +356,7 @@ android:id="@+id/images_nearby_info" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_toStartOf="@+id/wikidata_edits" - android:layout_toLeftOf="@+id/wikidata_edits" android:orientation="horizontal" android:gravity="center_vertical"> @@ -407,14 +388,13 @@ android:layout_height="@dimen/medium_height" android:id="@+id/images_nearby_info_icon" android:layout_marginTop="@dimen/activity_margin_horizontal" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" android:layout_gravity="top" app:layout_constraintLeft_toRightOf="@id/images_nearby_data" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" /> + app:tint="@color/primaryLightColor" /> @@ -423,16 +403,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:textAppearanceMedium" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/half_standard_height" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_centerVertical="true" tools:text="2" android:id="@+id/wikidata_edits" - android:layout_marginRight="@dimen/half_standard_height" /> + /> @@ -451,9 +429,7 @@ android:id="@+id/images_featured_info" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_toStartOf="@+id/image_featured" - android:layout_toLeftOf="@+id/image_featured" android:orientation="horizontal" android:gravity="center_vertical"> @@ -486,14 +462,13 @@ android:layout_height="@dimen/medium_height" android:id="@+id/images_featured_info_icon" android:layout_marginTop="@dimen/activity_margin_horizontal" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" app:layout_constraintLeft_toRightOf="@id/images_featured_data" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_gravity="top" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" /> + app:tint="@color/primaryLightColor" /> @@ -501,16 +476,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:textAppearanceMedium" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_centerVertical="true" tools:text="2" android:id="@+id/image_featured" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/half_standard_height" - android:layout_marginRight="@dimen/half_standard_height" /> + /> @@ -529,9 +502,7 @@ android:id="@+id/quality_images_info" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_toStartOf="@+id/quality_images" - android:layout_toLeftOf="@+id/quality_images" android:orientation="horizontal" android:gravity="center_vertical"> @@ -564,14 +535,13 @@ android:layout_height="@dimen/medium_height" android:id="@+id/quality_images_info_icon" android:layout_marginTop="@dimen/activity_margin_horizontal" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" app:layout_constraintLeft_toRightOf="@id/quality_images_data" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_gravity="top" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" /> + app:tint="@color/primaryLightColor" /> @@ -579,7 +549,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:textAppearanceMedium" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" @@ -587,9 +556,8 @@ tools:text="2" android:text="0" android:id="@+id/quality_images" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/half_standard_height" - android:layout_marginRight="@dimen/half_standard_height" /> + /> @@ -608,9 +576,7 @@ android:id="@+id/thanks_received_info" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_toStartOf="@+id/thanks_received" - android:layout_toLeftOf="@+id/thanks_received" android:orientation="horizontal" android:gravity="center_vertical"> @@ -643,14 +609,13 @@ android:layout_height="@dimen/medium_height" android:id="@+id/thanks_received_info_icon" android:layout_marginTop="@dimen/activity_margin_horizontal" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" app:layout_constraintLeft_toRightOf="@id/thanks_received_data" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_gravity="top" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" /> + app:tint="@color/primaryLightColor" /> @@ -658,16 +623,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:textAppearanceMedium" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_centerVertical="true" tools:text="2" android:id="@+id/thanks_received" android:layout_marginEnd="@dimen/half_standard_height" - android:layout_marginRight="@dimen/half_standard_height" /> + /> diff --git a/app/src/main/res/layout/item_place.xml b/app/src/main/res/layout/item_place.xml index 82e431063..9854fb58d 100644 --- a/app/src/main/res/layout/item_place.xml +++ b/app/src/main/res/layout/item_place.xml @@ -11,8 +11,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/standard_gap" - android:tint="?attr/rowButtonColor" - app:srcCompat="@drawable/ic_round_star_border_24px" /> + app:srcCompat="@drawable/ic_round_star_border_24px" + app:tint="?attr/rowButtonColor" /> @@ -54,11 +52,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignTop="@id/distance" - android:layout_marginLeft="@dimen/standard_gap" android:layout_marginStart="@dimen/standard_gap" android:layout_toEndOf="@id/icon" - android:layout_toLeftOf="@id/distance" - android:layout_toRightOf="@id/icon" android:layout_toStartOf="@id/distance" android:ellipsize="end" android:maxLines="2" @@ -71,8 +66,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignEnd="@id/distance" - android:layout_alignLeft="@id/tvName" - android:layout_alignRight="@id/distance" android:layout_alignStart="@id/tvName" android:layout_below="@id/tvName" android:layout_marginBottom="@dimen/standard_gap" diff --git a/app/src/main/res/layout/layout_campagin.xml b/app/src/main/res/layout/layout_campagin.xml index 775a6a4ec..2a2891e84 100644 --- a/app/src/main/res/layout/layout_campagin.xml +++ b/app/src/main/res/layout/layout_campagin.xml @@ -19,17 +19,14 @@ android:id="@+id/iv_campaign" android:layout_width="@dimen/dimen_40" android:layout_height="@dimen/dimen_40" - android:layout_marginLeft="@dimen/standard_gap" android:layout_marginStart="@dimen/standard_gap" android:scaleType="centerCrop" app:srcCompat="@drawable/ic_campaign" - android:tint="?attr/card_item_color" - /> + app:tint="?attr/card_item_color" /> @@ -37,15 +34,13 @@ + android:layout_marginStart="@dimen/standard_gap" + android:layout_marginEnd="@dimen/tiny_margin"> + android:visibility="visible" + app:tint="?attr/contributionsListTextSecondary" /> diff --git a/app/src/main/res/layout/nearby_card_view.xml b/app/src/main/res/layout/nearby_card_view.xml index 7161a0936..bbd43249e 100644 --- a/app/src/main/res/layout/nearby_card_view.xml +++ b/app/src/main/res/layout/nearby_card_view.xml @@ -14,7 +14,6 @@ style="@style/Widget.AppCompat.Button.Borderless" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_centerInParent="true" android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginRight="@dimen/activity_margin_horizontal" @@ -30,34 +29,28 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/content_layout" - android:layout_centerInParent="true" android:orientation="horizontal" > + android:id="@+id/progressBar" /> + app:tint="?attr/card_item_color" /> @@ -24,8 +25,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:tint="?attr/bookmarkButtonColor" - app:srcCompat="@drawable/ic_photo_camera_white_24dp" /> + app:srcCompat="@drawable/ic_photo_camera_white_24dp" + app:tint="?attr/bookmarkButtonColor" /> @@ -53,8 +55,8 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:duplicateParentState="true" - android:tint="?attr/bookmarkButtonColor" - app:srcCompat="@drawable/ic_photo_white_24dp" /> + app:srcCompat="@drawable/ic_photo_white_24dp" + app:tint="?attr/bookmarkButtonColor" /> + android:duplicateParentState="true" + app:tint="?attr/bookmarkButtonColor" /> @@ -110,8 +114,8 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:duplicateParentState="true" - android:tint="?attr/bookmarkButtonColor" - app:srcCompat="@drawable/ic_overflow" /> + app:srcCompat="@drawable/ic_overflow" + app:tint="?attr/bookmarkButtonColor" /> + app:srcCompat="@drawable/ic_arrow_back_white" + app:tint="@color/white" /> \ No newline at end of file diff --git a/app/src/main/res/values-ab/strings.xml b/app/src/main/res/values-ab/strings.xml index 9ff1b19b4..22f382f57 100644 --- a/app/src/main/res/values-ab/strings.xml +++ b/app/src/main/res/values-ab/strings.xml @@ -14,7 +14,7 @@ Аҭаларҭа Иҟаҵатәуп арегистрациа Асистемахь аҭаларҭа - Шәааԥшы ԥыҭрак... + Шәааԥшы ԥыҭрак… Аҭалара қәҿиарала имҩаԥысит! Асистемахь аҭалараан агха! Афаил ԥшаам. Даҽа фаилк шәахәаԥш. @@ -64,7 +64,7 @@ Ари шәара еилышәкаама? Ааи! Акатегориақәа - Аҭагалара... + Аҭагалара… Акагь алхӡам Иҟам ахҳәаа Идырым алицензиа diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 1da8b3101..57ba77cc9 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -22,7 +22,6 @@ %1$d lêers aan die uploaden - \@string/contributions_subtitle_zero (%1$d) (%1$d) @@ -148,7 +147,7 @@ Ja! <u>Meer inligting</u> Kategorieë - Laai ... + Laai … Niks gekies nie Geen beskrywing Geen bespreking nie diff --git a/app/src/main/res/values-anp/strings.xml b/app/src/main/res/values-anp/strings.xml index 70a01949f..e4029af9b 100644 --- a/app/src/main/res/values-anp/strings.xml +++ b/app/src/main/res/values-anp/strings.xml @@ -27,14 +27,14 @@ पासवर्ड भूलाय गेलौ की? साइन अप करौ प्रवेश होय रहलौ छौं - कृपया प्रतीक्षा करौ... - कृपया प्रतीक्षा करौ... + कृपया प्रतीक्षा करौ… + कृपया प्रतीक्षा करौ… प्रवेश विफल अपलोड आरंभ! हाल केरौ अपलोड कतारबद्ध विफल - अपलोड होय रहलौ छौं... + अपलोड होय रहलौ छौं… ठामे मँ हमरौ अपलोड साझा करौ @@ -68,7 +68,7 @@ हाँव! बेसी जानकारी श्रेणी सिनी - लोड होय रहलौ छौं... + लोड होय रहलौ छौं… कुछु चयनित नाय कोय शीर्षक नाय कोय विवरण नाय @@ -173,7 +173,7 @@ पूर्ण होलौं अगलका छवि हाँव, केन्हअ नाय - कृपया प्रतीक्षा करौ... + कृपया प्रतीक्षा करौ… प्रतिलिपि बनैलौ गेलै! लेखक स्थान diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 46ffcb7f5..b0fda6990 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -125,7 +125,7 @@ يجري الدخول الرجاء الانتظار… تحديث التسميات التوضيحية والأوصاف - يرجى الانتظار... + يرجى الانتظار… نجاح تسجيل الدخول! فشل تسجيل الدخول الملف غير موجود. فضلا اختر ملفا آخر. @@ -530,7 +530,7 @@ عرض المقروءة عرض غير المقروءة حدث خطأ أثناء التقاط الصور - الرجاء الانتظار... + الرجاء الانتظار… الصور المختارة هي صور من مصورين ورسامين ذوي مهارات عالية اختارها مجتمع ويكيميديا ​​كومنز كبعض الأفضل جودة على الموقع. الصور المرفوعة عبر الأماكن القريبة هي الصور المرفوعة عن طريق اكتشاف الأماكن على الخريطة. تتيح هذه الميزة للمحررين إرسال إشعار شكر للمستخدمين الذين يقومون بتعديلات مفيدة - باستخدام رابط شكر صغير في صفحة التاريخ أو صفحة الفرق. @@ -552,7 +552,7 @@ رفض الوصول إلى موقع الوسائط قد لا نتمكن من الحصول تلقائيًا على بيانات الموقع من الصور التي تقوم برفعها. يرجى إضافة الموقع المناسب لكل صورة قبل الإرسال ارفع الصور لويكيميديا ​​كومنز مباشرة من هاتفك. قم بتنزيل تطبيق كومنز الآن: %1$s - مشاركة التطبيق عبر... + مشاركة التطبيق عبر… معلومات الصورة لم يتم العثور على تصنيفات لم يتم العثور على الصور @@ -695,7 +695,7 @@ وضع الاتصال المحدود صور عالية الجودة الصور عالية الجودة هي رسوم بيانية أو صور فوتوغرافية تفي بمعايير جودة معينة (والتي تكون في الغالب ذات طبيعة فنية) وذات قيمة لمشروعات ويكيميديا - جاري استئناف التحميل ... + جاري استئناف التحميل … جاري إيقاف التحميل مؤقتًا .. الغاء التحميل إلغاء الرفع diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml index 960b55bda..63aa12b96 100644 --- a/app/src/main/res/values-as/strings.xml +++ b/app/src/main/res/values-as/strings.xml @@ -27,7 +27,7 @@ পাছৱৰ্ড পাহৰিলে? পঞ্জীয়ন কৰক লগইন হৈ আছে - অনুগ্ৰহ কৰি অপেক্ষা কৰক... + অনুগ্ৰহ কৰি অপেক্ষা কৰক… লগইন সফল হ\'ল! লগইন বিফল হৈছে! ফাইল পোৱা নগ\'ল। অনুগ্ৰহ কৰি আন এটা ফাইল চেষ্টা কৰক। @@ -74,7 +74,7 @@ <u>গোপনিয়তা নীতি</u> প্ৰতিক্ৰিয়া প্ৰেৰণ কৰক (ইমেইল যোগে) কোনো ইমেইল ক্লায়েন্ট ইনষ্টল কৰা নাই - প্ৰথম চিংকৰ বাবে অপেক্ষাৰত... + প্ৰথম চিংকৰ বাবে অপেক্ষাৰত… আপুনি এতিয়ালৈকে কোনো ফটো আপল\'ড কৰা নাই। পুনৰ চেষ্টা কৰক বাতিল কৰক diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index df61ed061..34212aebb 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -74,7 +74,7 @@ Aniciando sesión Espera… Actualizando pies y descripciones - Porfavor espera... + Porfavor espera… ¡Identificación correuta! ¡Falló l\'aniciu de sesión! Nun s\'alcontró\'l ficheru. Tenta con otru. @@ -480,7 +480,7 @@ Númberos de serie Software Xubi semeyes a Wikimedia Commons direutamente dende\'l to móvil. Descarga yá la app de Commons: %1$s - Compartir l\'aplicación per... + Compartir l\'aplicación per… Información de la imaxe Nun s\'alcontró nenguna categoría Nun s\'alcontraron retratos diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 1edbe43fc..d2ea468ad 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -104,7 +104,7 @@ CC BY 3.0 Əlavə məlumat Kateqoriyalar - Yüklənir... + Yüklənir… Heç biri seçilməmişdir Naməlum lisenziya Yenilə diff --git a/app/src/main/res/values-b+roa+tara/strings.xml b/app/src/main/res/values-b+roa+tara/strings.xml index 4fa660ef8..8e7764323 100644 --- a/app/src/main/res/values-b+roa+tara/strings.xml +++ b/app/src/main/res/values-b+roa+tara/strings.xml @@ -40,8 +40,8 @@ Tràse Passuord scurdate? Reggistrate - Stoche a tràse... - Aspitte... + Stoche a tràse… + Aspitte… E\' trasute! Non g\'è trasute! File non acchiate. Pruève \'n\'otre file. @@ -121,7 +121,7 @@ Permesse richieste Non ge tìne notifeche non lette Errore assute mendre ca ste pigghiave le immaggine - Aspitte... + Aspitte… Zumbe ste immaggine Autore Lènghe d\'a descrizione predefinite diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml index cd1cb09e8..b8b602d0d 100644 --- a/app/src/main/res/values-b+sr+Latn/strings.xml +++ b/app/src/main/res/values-b+sr+Latn/strings.xml @@ -5,7 +5,7 @@ * Milicevic01 * Zoranzoki21 --> - + Fejsbuk stranica Ostave Izvorni kod na Github-u Logo Ostave @@ -26,32 +26,39 @@ Slika dana %1$d datoteka se otprema + %1$d datoteke se otpremaju %1$d datoteke se otpremaju %1$d otpremanje + %1$d otpremanja %1$d otpremanja Pokretanje otpremanja Procesuiranje %d otpremanje + Procesuiranje %d otpremanja Procesuiranje %d otpremanja %d otpremanje + %d otpremanja %d otpremanja Slika će se voditi pod licencom %1$s + Slike će se voditi pod licencom %1$s Slike će se voditi pod licencom %1$s %1$d otpremanje + %1$d otpremanja %1$d otpremanja - Primanje deljenog sadržaja... Procesuiranje slike može potrajati neko vreme, u zavisnosti od veličine slike i vašeg uređaja - Primanje deljenog sadržaja... Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja + Primanje deljenog sadržaja… Procesuiranje slike može potrajati neko vreme, u zavisnosti od veličine slike i vašeg uređaja + Primanje deljenog sadržaja… Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja + Primanje deljenog sadržaja… Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja Istraga Izgled @@ -486,7 +493,7 @@ Pristup lokaciji medija je odbijen Možda nećemo moći da automatski pribavimo podatke o lokaciji iz slika koje otpremite. Dodajte odgovarajuću lokaciju za svaku sliku pre objavljivanja Otpremi fotografije na Vikimedijinu Ostavu direktno sa svog telefona. Preuzmi aplikaciju Ostave sada: %1$s - Podeli aplikaciju preko... + Podeli aplikaciju preko… Informacije o slici Nisu pronađene kategorije Otkazano otpremanje @@ -511,12 +518,13 @@ Uspešno Kategorija %1$s je dodata. + Kategorije %1$s su dodate. Kategorije %1$s su dodate. Nije moguće dodati kategorije. Ažuriraj kategoriju Uredi prikaze - Pokušavanje promena koordinata... + Pokušavanje promena koordinata… Ažuriranje koordinata Ažuriranje opisa Ažuriranje natpisa @@ -698,6 +706,7 @@ Nije moguće podeliti ovu stavku %d slika je odabrana + %d slika je odabrano %d slika je odabrano diff --git a/app/src/main/res/values-ba/strings.xml b/app/src/main/res/values-ba/strings.xml index 0fc68329f..4c33b396f 100644 --- a/app/src/main/res/values-ba/strings.xml +++ b/app/src/main/res/values-ba/strings.xml @@ -61,9 +61,9 @@ Серһүҙҙе оноттоғоҙмо? Теркәлеү Системаға инеү - Зинһар, көтөгөҙ... + Зинһар, көтөгөҙ… Аңлатмалар һәм тасуирламалар яңыртыла - Зинһар, көтөгөҙ... + Зинһар, көтөгөҙ… Системаға инеү уңышлы! Системаға инеү уңышһыҙ! Файл табылманы. Башҡа файлды эҙләп ҡарағыҙ. @@ -131,7 +131,7 @@ Фекереңде ебәр (эл.почта аша) Почта клиенты асыҡланмаған Яңыраҡ ҡулланылған категориялар - Тәүге синхронлаштырыуҙы көтөү... + Тәүге синхронлаштырыуҙы көтөү… Әлегә бер фото ла йөкләмәгәнһегеҙ Ҡабатларға Кире алыу @@ -171,7 +171,7 @@ Эйе! Ентеклерәк Категориялар - Йөкләнә башланы... + Йөкләнә башланы… Бер ни ҙә һайланмаған Тасуирламаһы юҡ Фекер алышыу юҡ diff --git a/app/src/main/res/values-ban/strings.xml b/app/src/main/res/values-ban/strings.xml index b24bc0022..1273eaf0d 100644 --- a/app/src/main/res/values-ban/strings.xml +++ b/app/src/main/res/values-ban/strings.xml @@ -61,7 +61,7 @@ Lali kruna Sandi? Daftar Ngeranjingin log - Jantos dumun... + Jantos dumun… Nganyarin sesirah miwah pidarta Jantos dumun… Mahasil manjing log! @@ -303,7 +303,7 @@ Nomor seri Piranti lunak Unggah foto nuju Wikimédia Commons langsung saking télépon ragané. Unduh aplikasi Commons mangkin: %1$s - Wedar aplikasi saking... + Wedar aplikasi saking… Pidarta Gambar Pangunggahan Kawangdé %1$s kaunggah olih: %2$s @@ -340,7 +340,7 @@ Kaanggén Paringkat Titiang Kualitas Gambar - Ngalanturang unggahan... + Ngalanturang unggahan… Ngarérénang unggahan… Wangdé Unggah Lisénsi Média diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 6ee931542..cb19d6e39 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -304,7 +304,7 @@ Преглеждане на прочетени Преглеждане на непрочетени Възникна грешка при избирането на изображенията - Моля, изчакайте... + Моля, изчакайте… напълно размазано Наблизо Прочетете повече diff --git a/app/src/main/res/values-blk/strings.xml b/app/src/main/res/values-blk/strings.xml index 2cf4aba5b..51a8ec1ba 100644 --- a/app/src/main/res/values-blk/strings.xml +++ b/app/src/main/res/values-blk/strings.xml @@ -38,7 +38,7 @@ အွောန်ႏဖေင်ꩻထိုꩻ ငဝ်းဗိဉ်ႏပလို့ꩻနဲ့? ဒင်ႏမတ်ပိုင်တိဉ် အဝ်ႏနွို့အကောက်ကျာꩻ - အိုင်ပွေားဆောင်းတဆင်ႏသြ... + အိုင်ပွေားဆောင်းတဆင်ႏသြ… နွို့အကောက်အောင်ႏလဲဉ်း! နွို့အကောက်အောင်ႏတဝ်း! မော့ꩻတဝ်းဖုဲင်၊ စံꩻထွားစံꩻသွော့ ဖုဲင်အလင်တဗာႏသြ။ @@ -97,7 +97,7 @@ မွေး! ထဲင်းယင်း သꩻတင်ꩻအချက်လက် ကဏ္ဍဖုံႏ - အဝ်ႏဒင်ႏဝွန်ႏကျာꩻ... + အဝ်ႏဒင်ႏဝွန်ႏကျာꩻ… လွိုက်ခါꩻတဝ်းမုဲင်ꩻမုဲင်ꩻ ပုင်ႏလိတ်အဝ်ႏတဝ်း အွောန်ႏနယ်ချက်အဝ်ႏတဝ်း diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 2d156c199..51502c264 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -393,7 +393,7 @@ কোনও চিত্র ব্যবহৃত হয়নি পঠিতগুলি দেখান অপঠিতগুলি দেখান - অনুগ্রহ করে অপেক্ষা করুন... + অনুগ্রহ করে অপেক্ষা করুন… অনুলিপি করা হয়েছে এই চিত্র এড়িয়ে যান প্রণেতা diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 1c7d09617..9537c45e6 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -40,6 +40,9 @@ %1$d bellgargadenn loc\'het + %1$d bellgargadenn loc\'het + %1$d bellgargadennoù loc\'het + %1$d bellgargadennoù loc\'het %1$d pellgargadennoù loc\'het @@ -51,6 +54,9 @@ gant an aotre-implijout %1$s e vo ar skeudenn-mañ + gant an aotre-implijout %1$s e vo an div skeudenn-mañ + gant an aotre-implijout %1$s e vo meur a skeudenn-mañ + gant an aotre-implijout %1$s e vo kalz a skeudenn-mañ gant an aotreoù-implijout %1$s e vo ar skeudenn-mañ Ergerzhout diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index 91860b1e1..d178ff507 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -10,19 +10,22 @@ Logo Commonsa postavlja se %1$d datoteka + postavlja se %1$d datoteke postavlja se %1$d datoteka - \@string/contributions_subtitle_zero postavljena %1$d datoteka + postavljena %1$d datoteke postavljenih datoteka: %1$d Započinjem postavljanje %1$d datoteke + Započinjem postavljanje %1$d datoteke Započinjem postavljanje %1$d datoteka/-e %1$d postavljanje + %1$d postavljanja %1$d postavljanja Slika će se voditi pod licencom %1$s diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 0c15e58c3..51330cb9d 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -20,27 +20,33 @@ Imatge del dia s\'està carregant %1$d fitxer + S\'estan carregant de %1$d fitxers s\'estan carregant %1$d fitxers (%1$d) + (%1$d) (%1$d) S\'inicien les càrregues S\'està processant %1$d càrrega + S\'estan processant %1$d càrregues S\'estan processant %1$d càrregues %d càrrega + $d càrregues %d càrregues Aquesta imatge quedarà sota llicència %1$s + Aquestes imatges quedaran sota llicència %1$s Aquestes imatges quedaran sota llicència %1$s %1$d pujada + %1$d pujades %1$d pujades Explora @@ -392,7 +398,7 @@ Model de lent Números de sèrie Programari - Comparteix l\'aplicació a través de... + Comparteix l\'aplicació a través de… Informació de la imatge No s’ha trobat cap categoria No s\'han trobat representacions diff --git a/app/src/main/res/values-ce/strings.xml b/app/src/main/res/values-ce/strings.xml index e13e8c040..e25b83e25 100644 --- a/app/src/main/res/values-ce/strings.xml +++ b/app/src/main/res/values-ce/strings.xml @@ -64,7 +64,7 @@ Викиларма Параметраш Викиларма чуйаккха - ДӀадоьдуш ду чуйаккхар... + ДӀадоьдуш ду чуйаккхар… Декъашхочун цӀе Пароль Commons Beta тӀехь хьай цӀарца чугӀо @@ -146,7 +146,7 @@ ЦӀе: Сиднейн операн театр ХӀаъ! Категореш - Чуйолуш... + Чуйолуш… ХӀума хаьржина йац Куьг доцуш Хаамаш бац @@ -297,7 +297,7 @@ Серийн лоьмар Программан кхачам Файл йолу меттиган тӀекхача бакъо ца ло - Йекъа программа, гӀоьнца... + Йекъа программа, гӀоьнца… Суьртан информаци Цхьа а категори ца карийна. Цхьа а хаам ца карийна. @@ -362,7 +362,7 @@ ДӀайаьккхина закладки йукъара Цхьа хӀума галдаьлла. Фонан сурт хӀотто аьтто ца баьлла Фонан сурт санна хӀоттайе - Фонан сурт дӀахӀоттош ду... + Фонан сурт дӀахӀоттош ду… Системин нисдаран гӀирс Бодане Сирла diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 4d49ee632..fb4ee05ef 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -32,31 +32,44 @@ Obrázek dne %1$d soubor se nahrává + %1$d soubory se nahrávají + %1$d souborů se nahrává %1$d souborů se nahrává - \@string/contributions_subtitle_zero (%1$d) + (%1$d) + (%1$d) (%1$d) Spouští se nahrávání %1$d souboru + Spouští se nahrávání %1$d souborů + Spouští se nahrávání %1$d souborů Spouští se nahrávání %1$d souborů %1$d nahrávání + %1$d nahrávání + %1$d nahrávání %1$d nahrávání Tento obrázek bude zveřejněn pod licencí %1$s + Tyto obrázky budou zveřejněny pod licencí %1$s + Tyto obrázky budou zveřejněny pod licencí %1$s Tyto obrázky budou zveřejněny pod licencí %1$s %1$d nahrání + %1$d nahrávání + %1$d nahrávání %1$d nahrání Probíhá příjem sdíleného obsahu. Zpracování obrázku může chvíli trvat v závislosti na velikosti obrázku a vašem zařízení + Probíhá příjem sdíleného obsahu. Zpracovávání obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení + Probíhá příjem sdíleného obsahu. Zpracovávání obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení Probíhá příjem sdíleného obsahu. Zpracování obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení Objevit diff --git a/app/src/main/res/values-csb/strings.xml b/app/src/main/res/values-csb/strings.xml index 623a48c8c..7cdfb1382 100644 --- a/app/src/main/res/values-csb/strings.xml +++ b/app/src/main/res/values-csb/strings.xml @@ -29,7 +29,7 @@ Wlogùjë mie Wregistrëjë sã Logòwanié - Proszã żdac... + Proszã żdac… Ùdałi logòwanié! Logòwanié nie darzëło sã! Felënk lopka. Proszã spróbòwac znowa. @@ -78,7 +78,7 @@ Sélôj òpinijã (przez e-mail) Felënk wjinstalowónegò e-mailowégò klienta Slédno ùżëwóne kategòrëje - Żdanié na pierszą synchronizacëjã... + Żdanié na pierszą synchronizacëjã… Nie môsz jesz wladowónych òdjimków Próbùjë znowa Òprzestóń @@ -99,7 +99,7 @@ Przëmiôr wladënka: Jo! Kategòrëje - Wladënk... + Wladënk… Felënk nacéchòwaniô Felënk òpisënka Nieznónô licencëja diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 8c4b4a652..50df9b6d8 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -21,25 +21,44 @@ Popeth Llun y Dydd + %1$d ffeil yn uwchlwytho %1$d ffeil yn uwchlwytho + %1$d ffeil yn uwchlwytho + %1$d ffeil yn uwchlwytho + %1$d ffeil yn uwchlwytho %1$d ffeil yn uwchlwytho \@string/contributions_subtitle_zero (%1$d) + (%1$d) + (%1$d) + (%1$d) (%1$d) Cychwyn Uwchlwytho + Dechrau %1$d uwchlwythiad Cychwyn %1$d uwchlwythiad + Dechrau %1$d uwchlwythiad + Dechrau %1$d uwchlwythiad + Dechrau %1$d uwchlwythiad Cychwyn uwchlwytho %1$d ffeil + %1$d uwchlwythiad %1$d uwchlwythiad + %1$d uwchlwythiad + %1$d uwchlwythiad + %1$d uwchlwythiad %1$d uwchlwythiad + Ni chaiff unrhyw ddelweddau eu trwyddedu dan %1$s Caiff y ddelwedd hon ei thrwyddedu yn ôl termau\'r drwydded %1$s + Caiff y delweddau hyn eu trwyddedu dan %1$s + Caiff y delweddau hyn eu trwyddedu dan %1$s + Caiff y delweddau hyn eu trwyddedu dan %1$s Caiff y delweddau hyn eu trwyddedu dan %1$s Archwilio diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 3b6822c47..80a82afb5 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -507,7 +507,7 @@ Adgang til medieplacering nægtet Vi kan muligvis ikke automatisk indhente placeringsdata fra billeder, du uploader. Tilføj den passende placering for hvert billede, før du indsender Upload billeder til Wikimedia Commons direkte fra din telefon. Download Commons-appen nu: %1$s - Del app via... + Del app via… Billedoplysninger Ingen kategorier blev fundet Ingen afbildninger fundet @@ -642,9 +642,9 @@ Begrænset forbindelsestilstand Kvalitetsbilleder Kvalitetsbilleder er tegninger eller fotografier, der opfylder visse kvalitetsstandarder (som for det meste er af teknisk karakter) og er værdifulde for Wikimedia-projekter - Genoptager upload... - Sætter upload på pause... - Annullerer upload... + Genoptager upload… + Sætter upload på pause… + Annullerer upload… Annuller upload Du har aktiveret begrænset forbindelsestilstand. Alle uploads er sat på pause og genoptages, når du deaktiverer denne tilstand. Begrænset forbindelsestilstand aktiveret! @@ -784,7 +784,7 @@ Andet problem eller anden information (forklar venligst nedenfor). Din feedback bliver slået op på følgende wiki-side: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Er du sikker på, at du vil annullere alle uploads? - Annullerer alle uploads... + Annullerer alle uploads… Uploads Afventer Mislykkedes diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a6471c1fe..404304021 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -516,7 +516,7 @@ Ungelesene ansehen Beim Auswählen der Bilder ist ein Fehler aufgetreten Bitte warten … - Vorgestellte Bilder sind Bilder von professionellen Fotografen und Zeichnern, die die Gemeinschaft von Wikimedia Commons als diejenigen mit der höchsten Qualität auf der Website ausgewählt hat. + Vorgestellte Bilder sind Bilder von professionellen Fotografen und Zeichnern, die Gemeinschaft von Wikimedia Commons als diejenigen mit der höchsten Qualität auf der Website ausgewählt hat. Über Orte in der Nähe hochgeladene Bilder sind die Bilder, die von entdeckten Orten auf der Karte hochgeladen wurden. Diese Funktion erlaubt es Autoren, eine Dankeschön-Benachrichtigung an Benutzer zu senden, die nützliche Bearbeitungen durchgeführt haben – durch die Benutzung eines kleinen Dankeschön-Links in der Versionsgeschichte oder Unterschiedsseite. Auf Folgemedien kopieren @@ -611,7 +611,7 @@ zu den Lesezeichen hinzugefügt Etwas ist schiefgelaufen. Das Hintergrundbild konnte nicht eingestellt werden Als Hintergrundbild festlegen - Hintergrundbild wird festgelegt. Bitte warten... + Hintergrundbild wird festgelegt. Bitte warten… Systemeinstellung Dunkel Hell diff --git a/app/src/main/res/values-diq/strings.xml b/app/src/main/res/values-diq/strings.xml index 840b0198d..ebb3cfbe4 100644 --- a/app/src/main/res/values-diq/strings.xml +++ b/app/src/main/res/values-diq/strings.xml @@ -62,8 +62,8 @@ Parola, xo vira kerde? Qeyd be Kewno cı - Kerem kerên, bıpawên... - Kerem ke, bıpawe... + Kerem kerên, bıpawên… + Kerem ke, bıpawe… Cıkewtış hewl bi. Nidekeweya de Dosya nêvineya. Dosyê da bine bıcerebnê. @@ -93,7 +93,7 @@ Şınasnayış Bınnuşte Xırabiya kewten-network xeta - Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2-3 deqey ra tepeya reyna bıcerrebnên. + Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2–3 deqey ra tepeya reyna bıcerrebnên. Qısur mewni rê, Karber commons dı bloqe biyo. Kodê kamiya raştkerdışi dıfaktorın gani cı kewê. Nidekeweya de @@ -298,7 +298,7 @@ Pêhesnayışê toyê wendışi çıniyê Wendışi bıvêne Nêwendeyan bıvêne - Kerem kerên, bıpawên... + Kerem kerên, bıpawên… Nê resımi raviyarnê Nuştekar Heqa telifi diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e3675ef0a..e4b597fb1 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -91,7 +91,7 @@ Σύνδεση Ξεχάσατε τον κωδικό πρόσβασης σας; Εγγραφή - Γίνεται σύνδεση... + Γίνεται σύνδεση… Παρακαλούμε αναμείνετε… Ενημέρωση λεζάντων και περιγραφών Παρακαλούμε αναμείνετε… @@ -204,7 +204,7 @@ Ναι! Περισσότερες πληροφορίες Κατηγορίες - Φόρτωση σε εξέλιξη... + Φόρτωση σε εξέλιξη… Καμία επιλεγμένη Χωρίς λεζάντα Χωρίς περιγραφή @@ -521,7 +521,7 @@ Δεν επιτρέπεται η πρόσβαση στην τοποθεσία πολυμέσων Ενδέχεται να μην μπορούμε να λάβουμε αυτόματα δεδομένα τοποθεσίας από φωτογραφίες που ανεβάζετε. Προσθέστε την κατάλληλη τοποθεσία για κάθε εικόνα πριν την υποβολή Ανεβάστε φωτογραφίες στα Wikimedia Commons απευθείας από το τηλέφωνό σας. Κάντε λήψη της εφαρμογής Commons τώρα: %1$s - Κοινή χρήση εφαρμογής μέσω... + Κοινή χρήση εφαρμογής μέσω… Πληροφορίες Εικόνας Δεν βρέθηκαν Κατηγορίες Δεν βρέθηκαν απεικονίσεις @@ -798,7 +798,7 @@ Άλλο πρόβλημα ή πληροφορίες (παρακαλούμε εξηγήστε παρακάτω). Τα σχόλιά σας δημοσιεύονται στην ακόλουθη σελίδα wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Εφαρμογή για κινητά/Σχόλια</a> Είστε βέβαιοι ότι θέλετε να ακυρώσετε όλες τις μεταφορτώσεις; - Ακύρωση όλων των μεταφορτώσεων... + Ακύρωση όλων των μεταφορτώσεων… Μεταφορτώσεις Σε εκκρεμότητα Απέτυχε diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 69673afbe..323c823b2 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -78,7 +78,7 @@ Ĉu pasvorto forgesita? Registriĝi Ensalutado - Bonvolu atendi... + Bonvolu atendi… Ĝisdatiganta subtekstojn kaj priskribojn Bonvolu atendi… Ensalutado sukcesis @@ -150,7 +150,7 @@ Sendi viajn komentojn (per retpoŝto) Neniu retpoŝtilo instalita Laste uzitaj kategorioj - Atendas la unuan Sinkronigado... + Atendas la unuan Sinkronigado… Vi ankoraŭ ne alŝutis fotojn. Reprovi Nuligi @@ -190,7 +190,7 @@ Jes! <u>Ekscii pli</u> Kategorioj - Ŝargado... + Ŝargado… Neniu elektita Neniu substeksto Sen priskribo @@ -482,7 +482,7 @@ Vidu legitajn Vidi nelegitojn Eraro okazis dum elektado de bildoj - Bonvolu atendi... + Bonvolu atendi… Elstaraj bildoj estas tiuj bildoj far tre spertaj fotografistoj kaj ilustristoj, kiujn la komunumo de Vikimedia Komunejo elektis kiel iujn de la plej alta kvalito en la retejo. Bildoj Alŝutitaj per Apudaj lokoj estas bildoj alŝutitaj per trovado de lokoj sur la mapo. Tiu funkcio ebligas sendi Dankantan sciigon al farinto de utila redakto – per malgranda dankiga ligilo ĉe la paĝo de historio aŭ diferenco. @@ -504,7 +504,7 @@ Aliro al loko de plurmediaĵo malakceptita Ni eble ne povos aŭtomate akiri pri-lokajn datumojn de bildoj, kiujn vi alŝutas. Bonvolu aldoni la taŭgan lokon por ĉiu bildo antaŭ ol sendi Alŝutu fotojn al Vikimedia Komunejo rekte de via telefono. Elŝutu la Komunejan aplikaĵon nun: %1$s - Diskonigi aplikaĵon per... + Diskonigi aplikaĵon per… Informo pri Bildo Neniu Kategorio troviĝis Neniu bildo-priskribo trovita @@ -636,9 +636,9 @@ Modo por limigita konekto Kvalitaj Bildoj Kvalitaj bildoj estas diagramoj aŭ fotoj kiuj kontentigas certajn normojn pri kvalito (kiuj estas plejparte teknikaj) kaj estas valoraj por Vikimediaj projektoj. - Rekomencante alŝuton... - Paŭzante alŝuton... - Nuligante alŝuton... + Rekomencante alŝuton… + Paŭzante alŝuton… + Nuligante alŝuton… Ĉesigi alŝutadon Vi aktivigis Modon por limigita konekto. Ĉiuj alŝutoj estas paŭzitaj kaj rekomencos post kiam vi malŝaltos ĉi modon. Modo por limigita konekto estas aktivigita. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4e90f6864..218953470 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -51,7 +51,7 @@ * Vivaelcelta * Wizardeck --> - + Página de Facebook de Commons Código fuente de Commons en GitHub Logo de Commons @@ -75,31 +75,38 @@ Foto del día Cargando %1$d archivo + Cargando %1$d archivos Cargando %1$d archivos (%1$d) + (%1$d) (%1$d) Comenzando las subidas Procesando %d carga + Procesando %d cargas Procesando %d cargas %d carga + %1 cargas %1 cargas Esta imagen se publicará bajo la licencia %1$s + Estas imágenes se publicarán bajo la licencia %1$s Estas imágenes se publicarán bajo la licencia %1$s %1$d Subida + %1$d Subidas %1$d Subidas Recepción de contenido compartido. El procesamiento de la imagen puede tardar cierto tiempo, dependiendo del tamaño de la imagen y de tu dispositivo + Recepción de contenido compartido. El procesamiento de las imágenes puede tardar cierto tiempo, dependiendo del tamaño de las imágenes y de tu dispositivo Recepción de contenido compartido. El procesamiento de las imágenes puede tardar cierto tiempo, dependiendo del tamaño de las imágenes y de tu dispositivo Explorar @@ -335,7 +342,7 @@ Omitir tutorial Internet no disponible Error al recuperar las notificaciones - Hubo un error al recuperar la imágen a revisar. Toca refrescar para intentarlo de nuevo. + Hubo un error al recuperar la imágen a revisar. Toca refrescar para intentarlo de nuevo. No se encontró ninguna notificación Traducir Idiomas @@ -477,7 +484,7 @@ Permitir Descartar Por favor, activa el acceso a la ubicación desde Configuración y vuelva a intentarlo. \n\nNota: Es posible que la subida no tenga datos de la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. - La cámara dentro de la aplicación necesita el permiso a la ubicación para adjuntarla a sus imágenes en caso de que la ubicación no esté disponible en EXIF. Por favor, permita que la aplicación acceda a su ubicación e inténtelo de nuevo.\n\nNota: Es posible que la subida no tenga los datos de la la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. + La cámara dentro de la aplicación necesita el permiso a la ubicación para adjuntarla a sus imágenes en caso de que la ubicación no esté disponible en EXIF. Por favor, permita que la aplicación acceda a su ubicación e inténtelo de nuevo.\n\nNota: Es posible que la subida no tenga los datos de la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. La aplicación no registrará la ubicación junto con las tomas debido a la falta del permiso de la ubicación. La aplicación no registrará la ubicación junto con las tomas porque el GPS está apagado Utilizar el selector de fotografías basado en documentos @@ -505,8 +512,8 @@ ¿Está correctamente categorizado? ¿Está dentro de los objetivos del proyecto? ¿Quieres agradecer al colaborador? - Toca en NO para nominar esta imágen para ser borrada si no es para nada útil. - Los logotipos, las capturas de pantalla y los pósteres de películas son habitualmente infracciones a los derechos de autor.\n Toca NO para nominar esta imágen para borrado + Toca en NO para nominar esta imágen para ser borrada si no es para nada útil. + Los logotipos, las capturas de pantalla y los pósteres de películas son habitualmente infracciones a los derechos de autor.\n Toca NO para nominar esta imágen para borrado Tu apreciación animara a %1$s ¡Oh, esto ni siquiera esta categorizado! Esta imagen esta dentro de %1$s categorías. @@ -524,7 +531,7 @@ Compartir registros usando Ver leídas Ver no leidas - Ocurrió un error mientras se elegían imagenes + Ocurrió un error mientras se elegían imágenes Un momento… Las imágenes destacadas son creaciones de talentosos fotógrafos e ilustradores que la comunidad de Wikimedia Commons ha reconocido como las de mayor calidad del sitio. Las imágenes subidas vía Lugares Cercanos son las imágenes que han sido subidas al descubrir lugares en el mapa. @@ -547,7 +554,7 @@ Acceso a la ubicación del archivo multimedia denegado Es posible que no podamos obtener automáticamente los datos de ubicación de las imágenes que suba. Añada la ubicación adecuada a cada imagen antes de enviarla Sube fotos a Wikimedia Commons directamente desde tu celular. Descarga la aplicación de Commons ahora: %1$s - Compartir la aplicación vía... + Compartir la aplicación vía… Información de la imagen No se encontró ninguna categoría No se encontraron representaciones @@ -574,6 +581,7 @@ Éxito Se añade %1$s categoría. + Se añaden %1$s categorías. Se añaden %1$s categorías. No se pudieron añadir las categorías. @@ -582,6 +590,7 @@ Editar las descripciones %1$s Se añade la descripción. + Descripción %1$s se añadieron. Descripción %1$s se añadieron. No se pueden añadir descripciones. @@ -599,7 +608,7 @@ Las coordenadas de la imagen no están actualizadas. No se puede obtener descripciones. Editar descripciones y leyendas - Compartir imagen via + Compartir imagen via Todavía no has hecho ninguna contribución. %s Aún no ha realizado ninguna contribución Cuenta creada @@ -624,7 +633,7 @@ añadido a marcadores Algo salió mal. No se pudo establecer el fondo de pantalla Colocar como fondo de pantalla - Estableciendo el fondo de pantalla. Por favor espere... + Estableciendo el fondo de pantalla. Por favor espere… Seguir sistema Oscuro Claro @@ -682,9 +691,9 @@ Modo de conexión limitada Imágenes de calidad Las imágenes de calidad son diagramas o fotografías que cumplen determinados estándares de calidad (mayormente de carácter técnico) y que son valiosas para proyectos de Wikimedia - Reanudando carga... - Pausando carga... - Cancelando carga... + Reanudando carga… + Pausando carga… + Cancelando carga… Cancelar carga Has habilitado el modo de conexión limitada. Todas las cargas están pausadas y se reanudarán cuando deshabilites este modo. El modo de conexión limitada está encendido. @@ -811,7 +820,8 @@ Guardar archivo GPX %d imagen seleccionada - %d imagenes seleccionadas + %d imágenes seleccionadas + %d imágenes seleccionadas Recuerde que todas las imágenes en una carga múltiple tienen la misma categoría y representación. Si las imágenes no comparten representación y categoría, haga varias cargas por separado. Nota sobre cargas múltiples @@ -819,7 +829,7 @@ Por favor, escriba algunos comentarios. Discusión Escriba algo sobre el elemento \'%1$s\'. Será visible públicamente. - Cancelando todas las subidas... + Cancelando todas las subidas… Subidas Pendiente Falló diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index ff75cbc7f..3dd463b34 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -154,7 +154,7 @@ Mesedez, igo bakarrik zuk ateratako edo sortutako irudiak: Naturako elementuak (loreak, animaliak, mendiak) Objektu erabilgarriak (bizikletak, tren geltokiak) - Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat...) + Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat…) Mesedez EZ igo: Autorretratuak edo zure lagunen argazkiak Internetetik jaitsitako irudiak diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 841160581..4cdd2b87a 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -83,7 +83,7 @@ رمز عبور خودتان را فراموش کرده‌اید؟ ثبت نام واردشدن - شکیبا باشید... + شکیبا باشید… ورود موفق! ورود ناموفق! پرونده یافت نشد لطفاً پرونده دیگری را امتحان کنید. @@ -122,7 +122,7 @@ تغییرها بارگذاری جستجوی رده‌ها - جستجو برای موضوعی که در پروندهٔ شما به‌نمایش کشیده‌شده است (مثلا کوه، تاج محل، ...) + جستجو برای موضوعی که در پروندهٔ شما به‌نمایش کشیده‌شده است (مثلا کوه، تاج محل، …) ذخیره تازه کردن فهرست @@ -411,7 +411,7 @@ شما هیچ اعلان خوانده‌شده‌ای ندارید نمایش دیده‌شده مشاهده خوانده نشده ها - لطفاً صبر کنید... + لطفاً صبر کنید… نمونه تصاویری که برای بازگذاری مناسب نیستند از این تصویر صرف نظر کن مدیریت تگ‌های EXIF @@ -423,7 +423,7 @@ مدل لنز شماره سریال نرم‌افزار - اشتراک از طریق... + اشتراک از طریق… اطلاعات عکس هیچ رده‌ای یافت نشد بارگذاری لغو شد @@ -455,7 +455,7 @@ به بوکمارک‌ها افزوده شد مشکل به وجود آمد. به عنوان پس‌زمینه انتخاب نشد. انتخاب به عنوان پس‌زمینه - قرار دادن پس‌زمینه. لطفاً صبر کنید... + قرار دادن پس‌زمینه. لطفاً صبر کنید… سامانه را دنبال کنید تیره روشن diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 312ebc84c..26328a3e2 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -80,7 +80,7 @@ Kirjaudutaan Odota… Päivitetään kuvatekstejä ja kuvauksia - Odota... + Odota… Kirjautuminen onnistui! Kirjautuminen epäonnistui! Tiedostoa ei löytynyt. Yritä toista tiedostoa. @@ -481,7 +481,7 @@ Sarjanumerot Ohjelmisto Lähetä valokuvia suoraan Wikimedia Commonsiin puhelimestasi. Lataa Commons-appi nyt: %1$s - Jaa sovellus... + Jaa sovellus… Kuvan tiedot Luokkia ei löytynyt Kuvauksia ei löytynyt @@ -546,7 +546,7 @@ Lisätty kirjanmerkkeihin Jotain meni väärin. Ei voitu asettaa taustakuvaksi. Aseta taustakuvaksi - Asetetaan taustakuvaksi. Odota... + Asetetaan taustakuvaksi. Odota… Käytä järjestelmän Tumma Vaalea @@ -594,8 +594,8 @@ Rajoitettu yhteistila pois päältä. Jonossa olevat lähetykset kopioidaan nyt. Rajoitettu yhteystila Laatukuvat - Jatketaan lähettämistä... - Keskeytetään lähetys... + Jatketaan lähettämistä… + Keskeytetään lähetys… Peruutetaan tallennusta… Peruuta tallennus Rajoitettu yhteystila on päällä. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 992f418af..995e4041b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -46,7 +46,7 @@ * Wladek92 * Y-M D --> - + Page Facebook de Commons Code source Github de Commons Logo de Commons @@ -70,31 +70,38 @@ Image du jour %1$d fichier en cours de téléversement + %1$d fichiers en cours de téléversement %1$d fichiers en cours de téléversement (%1$d) + (%1$d) (%1$d) Démarrage des téléversements %d téléversement en cours + %d téléversements en cours %d téléversements en cours %d téléversement + %d téléversements %d téléversements Cette image sera sous licence %1$s. + Ces images seront sous licence %1$s. Ces images seront sous licence %1$s. %1$d téléversement + %1$d téléversements %1$d téléversements - 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 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. 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. Explorer @@ -113,9 +120,9 @@ Mot de passe oublié ? S’inscrire Connexion - Veuillez patienter... + Veuillez patienter… Mise à jour des légendes et des descriptions - Veuillez patienter... + Veuillez patienter… Connexion réussie ! Échec de la connexion ! Fichier non trouvé. Veuillez en essayer un autre. @@ -185,7 +192,7 @@ Envoyer vos commentaires (par courriel) Aucun client de courriel installé Catégories récemment utilisées - En attente de première synchronisation... + En attente de première synchronisation… Vous n’avez encore téléchargé aucune photo. Réessayer Annuler @@ -225,7 +232,7 @@ Oui ! Davantage d’informations Catégories - Chargement en cours... + Chargement en cours… Aucune catégorie sélectionnée Aucune légende Aucune description @@ -521,7 +528,7 @@ Afficher les lus Afficher les non lus Une erreur est survenue lors de la sélection des images - Veuillez patienter... + Veuillez patienter… Les images en vedette sont des images de photographes et d’illustrateurs très doués que la communauté de Wikimédia Commons a choisies comme étant de la meilleure qualité pour le site. Les images téléversées par « Lieux à proximité » sont les images téléversées lors de la découverte de lieux sur la carte. Cette fonctionnalité 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. @@ -543,7 +550,7 @@ Accès à l’emplacement du média refusé Nous ne pourrons pas obtenir automatiquement les données de localisation des images que vous téléchargez. Veuillez ajouter l’emplacement approprié pour chaque image avant de la soumettre. Téléversez des photos sur Wikimedia Commons directement depuis votre téléphone. Téléchargez l’application Commons maintenant : %1$s - Partager l’application via... + Partager l’application via… Informations sur l’image Aucune catégorie trouvée Aucun élément représenté trouvé @@ -570,6 +577,7 @@ Succès La catégorie %1$s est ajoutée. + Les catégories %1$s sont ajoutées. Les catégories %1$s sont ajoutées. Impossible d’ajouter des catégories. @@ -578,6 +586,7 @@ Modifier les éléments représentés L’élément représenté %1$s est ajouté. + Les éléments représentés %1$s sont ajoutés. Les éléments représentés %1$s sont ajoutés. Impossible d’ajouter des éléments représentés. @@ -620,7 +629,7 @@ Ajouté aux favoris Un problème est survenu. Impossible d’installer le fond d’écran. Définir comme fond d’écran - Installation du fond d’écran. Veuillez patienter... + Installation du fond d’écran. Veuillez patienter… Suivre le système Sombre Clair @@ -678,9 +687,9 @@ Mode de connexion limitée Images de qualité Les images de qualité sont des diagrammes ou des photographies qui respectent certains standards de qualité (qui sont, par nature, essentiellement techniques) et sont précieuses pour les projets Wikimedia. - Reprise du téléversement... - Mise en pause du téléversement... - Annulation du téléversement... + Reprise du téléversement… + Mise en pause du téléversement… + Annulation du téléversement… Annuler le téléversement Vous avez activé le mode de connexion limitée. Tous les téléversements sont suspendus et reprendront une fois ce mode désactivé. Le mode de connexion limitée est actif. @@ -809,6 +818,7 @@ Fichier GPX enregistré %d image sélectionnée + %d images sélectionnées %d images sélectionnées Souvenez-vous que toutes les images dans une importation multiple prennent les mêmes catégories et descriptions. Si les images de partagent pas les descriptions et catégories, veuillez effectuer plusieurs importations séparées. @@ -822,7 +832,7 @@ Autre problème ou information (merci d\'expliquer ci-dessous). Vos commentaires sont publiés sur la page wiki suivante : <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Êtes-vous sûr de vouloir annuler tous les téléchargements ? - Annulation de tous les téléchargements... + Annulation de tous les téléchargements… Téléversements En attente Échec diff --git a/app/src/main/res/values-gcr/strings.xml b/app/src/main/res/values-gcr/strings.xml index b0ec66423..4659eecf1 100644 --- a/app/src/main/res/values-gcr/strings.xml +++ b/app/src/main/res/values-gcr/strings.xml @@ -38,9 +38,9 @@ Ou bliyé ou Kodsigré ? Enskri oukò Konnègsyon - Souplé antann... + Souplé antann… Mizajou di léjann-yan ké dèskripsyon-yan - Souplé antann... + Souplé antann… Konnègsyon bon ! Konnègsyon pabon ! Fiché pa trouvé. Souplé éséyé ké rounòt. @@ -96,7 +96,7 @@ Enren ! Plis lenfòrmasyon Katégori-ya - Chajman ka fèt... + Chajman ka fèt… Pyès katégori sélègsyonnen Pyès léjann Pyès dèskripsyon diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 1740c1890..e11716a51 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -452,7 +452,7 @@ Modelo de lente Números de serie Software - Compartir a aplicación vía... + Compartir a aplicación vía… Información da imaxe Non se atoparon categorías Cancelouse a carga diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 50a04319b..237583853 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -39,7 +39,6 @@ %1$d फ़ाइलें अपलोड हो रहीं - \@string/contributions_subtitle_zero (%1$d) (%1$d) @@ -70,8 +69,8 @@ पासवर्ड भूल गये? खाता बनायें लॉग इन हो रहा है - कृपया प्रतीक्षा करें... - कृपया प्रतीक्षा करें... + कृपया प्रतीक्षा करें… + कृपया प्रतीक्षा करें… लॉग इन सफल! लॉग इन विफल! फ़ाइल नहीं मिली, कृपया अन्य फ़ाइल से प्रयास करें। @@ -350,7 +349,7 @@ रद्द करें वार्ता क्या आप वाकई सभी अपलोड रद्द करना चाहते हैं? - सभी अपलोड रद्द किये जा रहे हैं... + सभी अपलोड रद्द किये जा रहे हैं… अपलोड लंबित विफल हुआ diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index d2d731c39..414f0dd40 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -15,19 +15,22 @@ Slika dana Postavlja se %1$d datoteka + Postavlja se %1$d datoteke Postavljaju se %1$d datoteke - \@string/contributions_subtitle_zero %1$d postavljena datoteka + %1$d postavljena datoteke %1$d postavljene datoteke Započeto %1$d postavljanje + Započinjem %1$d postavljanja Započeta %1$d postavljanja %1$d postavljanje + %1$d postavljanja %1$d postavljanja Ova će slika biti licencirana pod %1$s @@ -46,7 +49,7 @@ Zaboravljena zaporka? Otvori račun Prijava - Molimo pričekajte ... + Molimo pričekajte … Prijava uspješna! Prijava neuspješna! Datoteka nije pronađena. Molimo probajte drugu. @@ -104,7 +107,7 @@ Pošaljite povratnu informaciju (putem elektroničke pošte) Klijent za elektroničku poštu nije instaliran Nedavno rabljene kategorije - Pričekajte za prvu sinkronizaciju... + Pričekajte za prvu sinkronizaciju… Nemate još postavljenih slika. Pokušaj ponovo Odustani @@ -144,7 +147,7 @@ Da! Više informacija Kategorije - Učitavanje... + Učitavanje… Ništa nije odabrano Nema opisa Nepoznata licencija @@ -193,7 +196,7 @@ Stranica datoteke na Zajedničkom poslužitelju Stavka na Wikidati Članak na Wikipediji - Opišite medij što je više moguće: gdje je napravljen, što prikazuje,... Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. + Opišite medij što je više moguće: gdje je napravljen, što prikazuje,… Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. Mogući problemi s ovom slikom: Slika je pretamna. Slika je mutna. @@ -281,7 +284,7 @@ Promijenio/la sam mišljenje, ne želim da više bude javno vidljivo Toliko ste pridonijeli projektu da se naš sustav za računanje postignuća ne može nositi s time. To je vrhunsko postignuće. Došlo je do pogrješke tijekom obradbe slike. Molimo Vas, pokušajte ponovo! - Molimo Vas, pričekajte ... + Molimo Vas, pričekajte … Preskoči ovu sliku Zadani jezik za opis Pokušavanje ažuriranja kategorija. @@ -293,7 +296,7 @@ Dodano u oznake Nešto je pošlo po zlu. Ne možemo postaviti pozadinu Postavi kao pozadinu - Postavljanje pozadine. Molimo, pričekajte... + Postavljanje pozadine. Molimo, pričekajte… Zadano Tamno Svijetlo diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index aefc17d9d..eb3438674 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -441,7 +441,7 @@ Sorozatszámok Szoftver Képek feltöltése Wikimedia Commons-ba közvetlenül a telefonodról. Töltsd le a Commons applikációt most: %1$s - Alkalmazás megosztása ezzel... + Alkalmazás megosztása ezzel… Képinformáció Nem található kategória Megszakított feltöltés @@ -474,7 +474,7 @@ Híd, múzeum, szálloda, stb. A belépés nem sikerült, kérj új jelszót. Beállítás háttérképnek - Beállítás háttérképnek. Kérem várjon... + Beállítás háttérképnek. Kérem várjon… Rendszerbeállítás követése Sötét Világos diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 219fa4521..8fff554e3 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -61,7 +61,6 @@ %1$d Unggahan - Sedang menerima konten yang dibagikan. Memproses gambar mungkin memerlukan waktu lebih lama tergantung pada ukuran gambar dan perangkat Anda Sedang menerima konten yang dibagikan. Memproses gambar mungkin memerlukan waktu lebih lama tergantung pada ukuran gambar dan perangkat Anda Jelajahi @@ -82,7 +81,7 @@ Memasuki log Silakan tunggu… Memperbarui takarir dan deskripsi - Mohon tunggu... + Mohon tunggu… Berhasil masuk log! Gagal masuk log! Berkas tidak ditemukan. Silakan coba berkas lain. @@ -191,7 +190,7 @@ Ya! Informasi selengkapnya Kategori - Memuat... + Memuat… Tidak ada yang dipilih Tanpa takarir Tidak ada keterangan @@ -497,7 +496,7 @@ Akses lokasi media ditolak Kami mungkin tidak dapat memperoleh data lokasi secara otomatis dari gambar yang Anda unggah. Harap tambahkan lokasi yang sesuai untuk setiap gambar sebelum mengirimkannya Mengunggah foto ke Wikimedia Commons secara langsung dari telepon Anda. Unduh aplikasi Commons sekarang: %1$s - Bagikan aplikasi lewat... + Bagikan aplikasi lewat… Info Gambar Kategori tidak ditemukan Penggambaran tidak ditemukan @@ -523,7 +522,6 @@ Pembaruan kategori Berhasil - Kategori %1$s ditambahkan. Kategori %1$s ditambahkan. Tidak bisa menambahkan kategori. @@ -569,7 +567,7 @@ Ditambahkan ke pembatas Terjadi kesalahan. Tidak bisa menetapkan wallpaper Jadikan Wallpaper - Sedang menetapkan Wallpaper. Tolong tunggu... + Sedang menetapkan Wallpaper. Tolong tunggu… Ikuti sistem Gelap Terang @@ -625,9 +623,9 @@ Mode Koneksi Terbatas Gambar Berkualitas Gambar berkualitas adalah diagram atau foto yang memenuhi standar kualitas tertentu (yang sifatnya teknis) dan berharga bagi proyek Wikimedia - Melanjutkan unggahan... - Menunda unggahan... - Membatalkan pengunggahan... + Melanjutkan unggahan… + Menunda unggahan… + Membatalkan pengunggahan… Batalkan pengunggahan Anda menyalakan mode koneksi terbatas. Semua pengunggahan ditunda dan akan dilanjutkan begitu Anda mematikan mode ini. Mode sambungan terbatas sedang menyala. @@ -743,7 +741,7 @@ %d gambar dipilih Bicara - Membatalkan semua unggahan... + Membatalkan semua unggahan… Unggahan Menunggu Gagal diff --git a/app/src/main/res/values-io/strings.xml b/app/src/main/res/values-io/strings.xml index 51fe16441..994b1c3d3 100644 --- a/app/src/main/res/values-io/strings.xml +++ b/app/src/main/res/values-io/strings.xml @@ -70,9 +70,9 @@ Ka tu obliviis tua pasovorto? Enirar Eniranta - Voluntez vartar... + Voluntez vartar… Aktualiganta etiketi e deskripturi - Voluntez vartar... + Voluntez vartar… Eniro sucesoza! Eniro faliis! Arkivo ne trovita. Voluntez probar altr arkivo. @@ -142,7 +142,7 @@ Sendez komenti (per e-posto) Nula kliento di e-posto instalesis Kategorii recente uzita - Vartanta unesma sinkronigo... + Vartanta unesma sinkronigo… Vu ankore ne sendis fotografuri. Riprobar Nuligar @@ -180,7 +180,7 @@ Yes! Plusa informo Kategorii - Karganta... + Karganta… Nulo selektesis Nula deskripto-texto Nula deskripto @@ -410,7 +410,7 @@ Vu ne lektis irga avizo Vidar lektita Vidar ne-lektata - Vartez... + Vartez… Kopiita Exempli pri bona imaji por sendar a Commons Saltez ca imajo @@ -472,7 +472,7 @@ Ajusti Adjuntita marko-rubandi Uzar kom skreno-kovrilo - Kreanta skreno-kovrilo. Voluntez vartar... + Kreanta skreno-kovrilo. Voluntez vartar… Koloro obskura Koloro klara Charjez pluse @@ -500,7 +500,7 @@ Uzita Mea rango Imaji di qualeso - Nuliganta sendajo... + Nuliganta sendajo… Cesar kargajo Lektez pluse En omna idiomi diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 417652953..ac64fbf2c 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -3,7 +3,7 @@ * Sveinki * Sveinn í Felli --> - + Commons Facebook-síðan Grunnkóði Commons á Github Táknmerki Commons @@ -51,7 +51,7 @@ %1$d innsendingar - Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndarinnar og gerð tækisins þíns + Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndarinnar og gerð tækisins þíns Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndaanna og gerð tækisins þíns Uppgötva @@ -138,7 +138,7 @@ Senda umsögn (með tölvupósti) Ekkert tölvupóstforrit er uppsett Nýlega notaðir flokkar - Bíð eftir fyrstu samstillingu... + Bíð eftir fyrstu samstillingu… Þú ert ekki ennþá búin(n) að senda inn neinar myndir. Reyna aftur Hætta við @@ -477,7 +477,7 @@ Hugbúnaður Aðgangi að staðsetningu gagnamiðla hafnað Sendu myndir inn á Wikimedia Commons beint úr símanum þínum. Sæktu Commons-appið núna: %1$s - Deila forriti með... + Deila forriti með… Upplýsingar í mynd Engir flokkar fundust Engar myndlýsingar fundust diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f40863870..e9aa8934e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -46,31 +46,38 @@ Foto del giorno %1$d file in caricamento + %1$d file in caricamento %1$d file in caricamento (%1$d) + (%1$d) (%1$d) Avvio del caricamento Elaborando %d caricamento + Elaborando %d caricamenti Elaborando %d caricamenti %d caricamento + %d caricamenti %d caricamenti Questa immagine sarà rilasciata in base alla licenza %1$s + Queste immagini saranno rilasciate in base alla licenza %1$s Queste immagini saranno rilasciate in base alla licenza %1$s %1$d caricamento + %1$d caricamenti %1$d caricamenti Ricezione di contenuti condivisi. L\'elaborazione dell\'immagine potrebbe richiedere del tempo a seconda delle dimensioni dell\'immagine e del dispositivo + Ricezione di contenuti condivisi. L\'elaborazione delle immagini potrebbe richiedere del tempo a seconda delle dimensioni delle immagini e del dispositivo Ricezione di contenuti condivisi. L\'elaborazione delle immagini potrebbe richiedere del tempo a seconda delle dimensioni delle immagini e del dispositivo Esplora @@ -516,7 +523,7 @@ Accesso alla posizione multimediale negato Potremmo non essere in grado di ottenere automaticamente i dati sulla posizione dalle immagini caricate. Si prega di aggiungere la posizione appropriata per ciascuna immagine prima di inviarla Carica foto su Wikimedia Commons direttamente dal tuo telefono. Scarica subito l\'app Commons: %1$s - Condividi applicazione tramite... + Condividi applicazione tramite… Informazioni sull\'immagine Nessuna categoria trovata Nessuna definizione trovata @@ -543,6 +550,7 @@ Successo Categoria %1$s aggiunta. + Categorie %1$s aggiunte. Categorie %1$s aggiunte. Non è stato possibile aggiungere le categorie. @@ -575,7 +583,7 @@ Esiste Necessita della fotografia Tipo di luogo: - Ponte, museo, albergo, ecc... + Ponte, museo, albergo, ecc… Si è verificato un errore durante l\'accesso. Devi reimpostare la password! MEDIA CLASSI FIGLIE @@ -588,7 +596,7 @@ Aggiungi ai preferiti Qualcosa è andato storto. Non è stato possibile impostare lo sfondo schermo Imposta come sfondo - Impostazione di sfondo in corso... + Impostazione di sfondo in corso… Segui sistema Scuro Chiaro @@ -758,6 +766,7 @@ Sessione scaduta. Accedi nuovamente. %d immagine selezionata + %d immagini selezionate %d immagini selezionate Questo posto non ha ancora una foto, scattane una! diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 0b512102b..4b8c51f6c 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -45,44 +45,37 @@ מועלה קובץ אחד מועלים %1$d קבצים - מועלים %1$d קבצים מועלים %1$d קבצים (%1$d) (%1$d) - (%1$d) (%1$d) ההעלאות מתחילות עיבוד העלאה עיבוד d% העלאות - עיבוד d% העלאות עיבוד d% העלאות העלאה אחת %d העלאות - %d העלאות %d העלאות התמונה הזאת תפורסם ברישיון %1$s התמונות האלה תפורסמנה ברישיון %1$s - התמונות האלה תפורסמנה ברישיון %1$s התמונות האלה תפורסמנה ברישיון %1$s העלאה אחת %1$d העלאות - %1$d העלאות %1$d העלאות מתקבל תוכן שיתופי. עיבוד התמונה עשוי לארוך זמן מה כתלות בגודל התמונה והמכשיר שלך מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך - מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך לחקור @@ -101,9 +94,9 @@ שכחת את הסיסמה? רישום כניסה לחשבון - נא להמתין... + נא להמתין… עדכון כיתובים ותיאורים - נא להמתין... + נא להמתין… הכניסה הצליחה! הכניסה נכשלה! הקובץ לא נמצא. נא לנסות קובץ אחר. @@ -213,7 +206,7 @@ כן! מידע נוסף קטגוריות - בטעינה... + בטעינה… לא נבחר דבר אין כיתוב אין תיאור @@ -508,7 +501,7 @@ הצגת התראות שנקראו הצגת התראות שלא נקראו אירעה שגיאה בעת בחירת תמונות - נא להמתין... + נא להמתין… תמונות מובילות הן תמונות של צלמים ומאיירים מיומנים אותם בחרה קהילת ויקישיתוף בזכות איכות התוצר שהם תורמים לאתר. תמונות שהועלו דרך מקומות בסביבה הן התמונות שנשלחות על ידי גילוי מקומות במפה. תכונה זו מאפשרת לעורכים לשלוח מסרי תודה למשתמשים שביצעו עריכות מועילות - על ידי שימוש בקישור תודה בדף ההיסטוריה או בדף ההבדלים. @@ -530,7 +523,7 @@ הגישה למקום המדיה נדחתה ייתכן שלא נוכל לאתר את נתוני המקום מתמונות שהעלית. נא להוסיף את המקום המתאים לכל תמונה בטרם הגשתה כדי להעלות תמונות לוויקינתונים של ויקימדיה ישר מהטלפון שלך. אתם מוזמנים להוריד את היישום של ויקינתונים עכשיו: %1$s - שיתוף היישום דרך... + שיתוף היישום דרך… פרטי תמונה לא נמצאו קטגוריות לא נמצאו מוצגים @@ -558,7 +551,6 @@ נוספה קטגוריה. נוספו %1$s קטגוריות. - נוספו %1$s קטגוריות. נוספו %1$s קטגוריות. לא ניתן להוסיף קטגוריות. @@ -568,7 +560,6 @@ נוסף מוצג %1$s נוספו המוצגים %1$s - נוספו המוצגים %1$s נוספו המוצגים %1$s לא היה אפשר להוסיף מוצגים. @@ -611,7 +602,7 @@ נוסף לסימניות משהו השתבש. לא היה אפשר להגדיר את הטפט להגדיר בתור טפט - הגדרת טפט. נא להמתין... + הגדרת טפט. נא להמתין… מערכת מעקב כהה בהירה @@ -671,7 +662,7 @@ תמונות איכות הן תרשימים או תמונות שעומדות בתקני איכות מסוימים (שמטבעם בעיקר טכניים) והן בעלות ערך למיזמי ויקימדיה ההעלאה ממשיכה… ההעלאה מושהית… - ביטול ההעלאה... + ביטול ההעלאה… ביטול ההעלאה הפעלת מצב חיבור מוגבל. כל ההעלאות מושהות ותמשכנה לאחר השבתת המצב הזה. מצב חיבור מוגבל פעיל. @@ -801,7 +792,6 @@ נבחרה תמונה אחת נבחרו שתי תמונות - נבחרו %d תמונות נבחרו %d תמונות נא לזכור שכשמועלות כמה תמונות, כולן מקבלות את אותן הקטגוריות והמוצגים. אם התמונות אינן חולקות מוצגים וקטגוריות, נא לעשות כמה העלאות נפרדות. @@ -815,7 +805,7 @@ בעיה אחרת או מידע אחר (נא להסביר הלאה). המשוב שלך מתפרסם בדף הוויקי הבא: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> האם ברצונך באמת לבטל את כל ההעלאות? - ביטול כל ההעלאות... + ביטול כל ההעלאות… העלאות ממתינות נכשלו diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f20b986f8..f60bb30dd 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -44,7 +44,6 @@ %1$d 件のファイルをアップロード中 - (%1$d) (%1$d) アップロードを開始中です @@ -55,14 +54,12 @@ %d 件のアップロード - この画像は%1$sライセンスのもとにアップロードされます これらの画像は%1$sライセンスのもとにアップロードされます %1$d 件のアップロード - 共有コンテンツを受信中です。 この画像の投稿の処理には、サイズやご使用の機器により時間がかかる事があります 共有コンテンツの受信中です。投稿画像の処理には、サイズやご使用の機器により時間がかかる事があります 探索 @@ -560,7 +557,7 @@ ブックマークに追加 問題が発生しました。壁紙を設定できませんでした。 壁紙として設定 - 壁紙を設定中。お待ちください... + 壁紙を設定中。お待ちください… システムのまま ダーク ライト diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 40eb01629..eb90e4a23 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -47,8 +47,8 @@ Qqen Tettuḍ awal uffir? Jerred - Tuqqna... - Rǧu... + Tuqqna… + Rǧu… Tuqqna tedda! Tqqna ur teddi ara! Ulac afaylu. Ɛreḍ wayeḍ ma ulac aɣilif. @@ -100,7 +100,7 @@ Azen tikti (s yimayl) Ulac amsaɣ n yimayl ibedden Taggayin yettwasqedcenmelmi kan - Araǧu n umtawi amezwaru... + Araǧu n umtawi amezwaru… Ur tsuliḍ ara yakan tiwlafin. Ɛref̣ tikelt-nniḍen Sefsex @@ -130,7 +130,7 @@ Tɣileḍ igarrez? Ih! Taggayin - Asali... + Asali… Ula d yiwet ur tettwafren Ulac aglam Turagt tarussint diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index b729838b9..3703d373f 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -43,28 +43,22 @@ 검색 뷰 오늘의 이미지 - %1$d개의 파일을 올리는 중 %1$d개의 파일을 올리는 중 - (%1$d) (%1$d) 파일 올리기 - %1$d장의 업로드를 처리하는 중입니다 %1$d장의 업로드를 처리하는 중입니다 - %d개 업로드 %d개 업로드 - 이 그림은 %1$s에 따라 사용이 허가됩니다 이 그림은 %1$s에 따라 사용이 허가됩니다 - %1$d개 업로드 %1$d개 업로드 찾아보기 @@ -85,7 +79,7 @@ 로그인 중 기다려 주세요… 캡션 및 설명를 업데이트하는 중 - 기다려 주십시오... + 기다려 주십시오… 로그인 성공! 로그인 실패! 파일을 찾을 수 없습니다. 다른 파일을 사용해 주십시오. @@ -456,7 +450,7 @@ 읽은 항목 보기 읽지 않은 항목 보기 이미지 선택 도중 오류가 발생했습니다 - 기다려 주십시오... + 기다려 주십시오… 다음 미디어로 복사 복사했습니다 공용에 업로드할 좋은 이미지의 예 @@ -471,7 +465,7 @@ 렌즈 모델 일련 번호 소프트웨어 - 앱 공유... + 앱 공유… 이미지 정보 분류가 없습니다 서술이 발견되지 않았습니다 @@ -529,7 +523,7 @@ 북마크에 추가됨 무언가 잘못되었습니다. 배경화면을 설정하지 못했습니다 배경화면으로 설정 - 배경화면을 설정 중입니다. 기다려 주십시오... + 배경화면을 설정 중입니다. 기다려 주십시오… 어두운 밝은 위치 설정을 열지 못했습니다. 위치를 수동으로 켜주세요 diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index b97684821..be63e9db5 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -143,7 +143,7 @@ Оюмунгу билдир (эл. почта бла) Почта клиент къурулмагъанды Кёб болмай хайырланнган категорияла - Биринчи синхронизацияны сакълаб турады... + Биринчи синхронизацияны сакълаб турады… Алкъын джюкленнген фотосуратыгъыз джокъду. Джангыдан сына Ызына ал @@ -498,7 +498,7 @@ Медиа локациягъа джетишиу уналмады Джюклеген суратладан локация билгилени автомат халда алмазгъа боллукъбуз. Тилейбиз, джибериуден алгъа хар сурат ючюн келишген локацияны къошугъуз Фотосуратланы телефонугъуздан туура Викигёзеннге джюклегиз. Гёзен Къошакъны энди эндиригиз: %1$s - Къошакъны буну бла юлюшле... + Къошакъны буну бла юлюшле… Сурат Информация Категорияла табылмадыла Танытыула табылмадыла @@ -575,7 +575,7 @@ Китаб белгилеге къошулду Не эсе да терс кетди. Къабыргъа къагъыт къурулалмады Къабыргъа къагъыт эт - Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз... + Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз… Системаны джарашдыр Къарангы Джарыкъ @@ -633,9 +633,9 @@ Чекленнген Байланыу Режим Агъачлары Мийик Суратла Агъачлы суратла, белгили агъач стандартларына (асламысыны техника халы болады) келишген эмда Викимедиа проектле ючюн багъалы болгъан диаграммала неда фотосуратладыла - Джюклениу андан ары бардырылады... - Джюклениу туракъланады... - Джюклениу ызына алынады... + Джюклениу андан ары бардырылады… + Джюклениу туракъланады… + Джюклениу ызына алынады… Джюклеуню Ызына Ал Чекли байланыу режимни джандырдыгъыз. Бютеу джюклениуле туракълатыллыкъдыла эмда бу режимни джукълатсагъыз, тохтагъан джерден башларыкъдыла. Чекленнген байланыу режим джандырылгъанды. diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index 506e9e4b4..d9d5b65b9 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -70,8 +70,8 @@ Te şîfreya xwe ji bîr kir? Xwe tomar bike Têdikeve - Ji kerema xwe piçek bisekine ... - Xêra xwe hinek bisekine... + Ji kerema xwe piçek bisekine … + Xêra xwe hinek bisekine… Têketin bi ser ket! Têketin bi ser neket! Dosye nehat dîtin. Ji kerema xwe re dosyeyek din biceribîne. @@ -183,7 +183,7 @@ Wêneyên Barkirî Wêneyê din Belê, çima na - Ji kerema xwe piçek bisekine ... + Ji kerema xwe piçek bisekine … Wêne tevlî Wîkîpediyayê bike Tu dixwazî vê wêneyê tevlî gotara Wîkîpediyayê ya bi zimanê %1$s bikî? Pişrast bike diff --git a/app/src/main/res/values-kum/strings.xml b/app/src/main/res/values-kum/strings.xml index 8112afea6..ab657b354 100644 --- a/app/src/main/res/values-kum/strings.xml +++ b/app/src/main/res/values-kum/strings.xml @@ -49,7 +49,7 @@ Юклев уьлгю: Дюр! Категориялар - Юклев... + Юклев… Бир зат сайланмагъан Тасвири ёкъ Пикирлешивлер ёкъ diff --git a/app/src/main/res/values-kus/strings.xml b/app/src/main/res/values-kus/strings.xml index 99fb8c1f7..02abd4ea1 100644 --- a/app/src/main/res/values-kus/strings.xml +++ b/app/src/main/res/values-kus/strings.xml @@ -62,9 +62,9 @@ Fʋ tami fʋ paaswɛɛtɛ? Yɔ\'ɔgin kpɛn\' Kpɛn\'ɛdnɛ - M bɛlimnɛ gu\'usim... + M bɛlimnɛ gu\'usim… Maligim maal pian\'azut nɛ pa\'alʋg nam - M bɛlimnɛ gu\'usim... + M bɛlimnɛ gu\'usim… Kpɛn\'ɛb nyaŋya Kpɛn\'ɛb gʋ\'ʋŋya M Pʋ nyɛ faal la. M bɛlimnɛ tiakim faal si\'a. @@ -169,7 +169,7 @@ Ɛɛn! Labaya bɛdigʋ Buudi kɔn\'ɔb-kɔn\'ɔb - Bɛ tʋʋma ni... + Bɛ tʋʋma ni… Pʋ gaŋ si\'ela Pian\'azug kae Pa\'alʋg kae @@ -400,7 +400,7 @@ Gɔsim dinɛ ka fʋ karim sa Gɔsim dinɛ ka fʋ nam pʋ karim Daʋŋʋ kidig footonam la nɔkirin - M bɛlimnɛ gu\'usim... + M bɛlimnɛ gu\'usim… Footo banɛ ka fʋ kpɛn\'ɛsi dɔlis zin\'ibanɛ be yamma anɛ footo banɛ ka fʋ kpɛn\'ɛs ka di yinɛ fʋn nyɛ di map ni la. Yaam paas media banɛ bɛ tuon Yaaiya @@ -418,7 +418,7 @@ Serial Numbers Software Pʋ bas suor ye fʋ kpɛn\' midia zin\'iginɛ - Pʋdigim app la dɔlis... + Pʋdigim app la dɔlis… Footo labaar Pʋ paam buudinama Pʋ nyɛ nwɛnnɛm si\'aa. @@ -492,7 +492,7 @@ Ba zaŋi paas bookmarknamin Daʋŋsi\'a naam. Pʋ nyaŋi maal nibdaa footo la Maalimi fʋ nindaa footo la - Maanɛ nindaa footo. M bɛlimnɛ gu\'usim... + Maanɛ nindaa footo. M bɛlimnɛ gu\'usim… Dɔl sistɛm la Lik Nɛɛsim @@ -538,9 +538,9 @@ Bas suor ye di tʋm saŋa bi\'ela! Atʋm bi\'ela zi\'esim Footo sʋma - Lɛm pin\'in kpɛn\'ɛsʋg... - Gu\'om kpɛn\'ɛsʋg... - Basid kpɛn\'ɛsʋg... + Lɛm pin\'in kpɛn\'ɛsʋg… + Gu\'om kpɛn\'ɛsʋg… + Basid kpɛn\'ɛsʋg… Basim kpɛn\'ɛsʋg Bas suor ye di tʋm saŋa bi\'ela. Nwɛnnɛm nam diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index 2eb2fcf2f..8b2ab6b95 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -21,7 +21,6 @@ %1$d файл жүктөлүүдө - Азырынча жүктөөлөр жок 1 жүктөө %1$d жүктөө @@ -137,7 +136,7 @@ Жүктөөнү жокко чыгаруу Артка баскычын колдонуу менен бул жүктөө жокко чыгарылат жана сиз ийгиликти жоготосуз Жүктөөнү улантуу - Күтө туруңуз... + Күтө туруңуз… Аталыш Сыпаттама Элементтер diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index 9d69efabb..d99e269ab 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -61,7 +61,7 @@ Aloggen Waart wgl. … Beschrëftungen a Beschreiwungen aktualiséieren - Waart wgl. ... + Waart wgl. … Umeldung huet geklappt! D\'Aloggen huet net funktionéiert! Fichier net fonnt. Probéiert wgl. en anere Fichier. @@ -349,7 +349,7 @@ Déi geliese weisen Déi net geliese weisen Feeler beim Eraussiche vun de Biller - Waart wgl. ... + Waart wgl. … Kopéiert Beispiller vu gudde Biller fir op Commons eropzelueden Beispiller fir Biller, déi een net eropluede sollt @@ -361,7 +361,7 @@ Seriennummeren Software Luet Fotoen direkt vun Ärem Handy op Wikimedia Commons erop. Luet d\'Commons-App elo erof: %1$s - App deelen iwwer... + App deelen iwwer… Bildinformatiounen Keng Kategorie fonnt. Eroplueden ofgebrach @@ -411,7 +411,7 @@ Bei d\'Lieszeechen derbäigesat Et ass Eppes schif gaangen. D\'Hannergrondbild konnt net agestallt ginn Als Hannergrondbild festleeën - Hannergrondbild gëtt agestallt. Waart wgl... + Hannergrondbild gëtt agestallt. Waart wgl… System suivéieren Däischter Hell @@ -454,7 +454,7 @@ Limitéierte Verbindungsmodus Qualitéitsbiller Qualitéitsbiller sinn Diagrammen oder Fotoen, déi gewësse Qualitéitscritèren erfëllen (déi haaptsächlech vun technescher Natur sinn) a wäertvoll fir Wikimedia-Projete sinn. - Eropluede gëtt ofgebrach.... + Eropluede gëtt ofgebrach…. Eroplueden ofbriechen Kategoriesäit weisen Sprooch vum Interface vum Benotzer vun der App diff --git a/app/src/main/res/values-li/strings.xml b/app/src/main/res/values-li/strings.xml index f477ed8f0..1720bfbcb 100644 --- a/app/src/main/res/values-li/strings.xml +++ b/app/src/main/res/values-li/strings.xml @@ -33,8 +33,8 @@ Melj dich aan Wachwaord vergaete? Teiken dich in - Aan \'nt melje... - Wach estebleef... + Aan \'nt melje… + Wach estebleef… Aanmelje gelök! Aanmelje mislök! Bestandj neet gevónje. Perbeer \'n anger bestandj. @@ -88,7 +88,7 @@ Sjik feedback (mitten e-mail) Geine e-mailcliënt geïnstalleerd Recèntelik gebroekde categorieje - Oppe ieëste synchronisatie \'nt wachte... + Oppe ieëste synchronisatie \'nt wachte… Doe höbs nag gein plaetjes geüpload. Perbeer oppernuuj Braek aaf @@ -127,7 +127,7 @@ Versteis se \'t? Jao! Categorieje - \'nt laje... + \'nt laje… Geine gekaoze Gein besjrieving Ónbekande licentie diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 26a9bc7f7..cb7bebe41 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -33,20 +33,27 @@ Dienos nuotrauka %1$d keliamas failas + %1$d keliami failai + %1$d failų keliamas %1$d keliami failai + %1$d įkėlimas + %1$d įkėlimai + %1$d įkėlimų - \@string/contributions_subtitle_zero - 1 įkėlimas Įkėlimai pradedami Pradedamas %1$d įkėlimas + Pradedami %1$d įkėlimai + Pradedami %1$d įkėlimų Pradedami %1$d įkėlimai %1$d įkėlimas + %1$d įkėlimai + %1$d įkėlimų %1$d įkėlimai Šio paveikslėlio licencija bus %1$s @@ -68,7 +75,7 @@ Jungiamasi Prašome palaukti… Antraštės ir aprašymai atnaujinami - Prašome palaukti... + Prašome palaukti… Sėkmingai prisijungėte! Prisijungti nepavyko! Failas nerastas. Prašome pabandyti kitą failą. @@ -172,7 +179,7 @@ Taip! Daugiau informacijos Kategorijos - Kraunasi... + Kraunasi… Niekas nepasirinkta Nėra antraštės Nėra aprašymo @@ -465,7 +472,7 @@ Žiūrėti perskaitytus Žiūrėti neperskaitytus Renkant vaizdus įvyko klaida - Prašome palaukti... + Prašome palaukti… Rinktinės nuotraukos yra aukštos kvalifikacijos fotografų ir iliustratorių vaizdai, kuriuos Vikiteka bendruomenė pasirinko kaip svetainėje aukščiausios kokybės. Vaizdai, įkelti per Netoliese esančias vietas, yra vaizdai, kurie įkeliami atrandant vietas žemėlapyje. Ši funkcija leidžia redaktoriams siųsti padėkos pranešimą naudotojams, kurie atlieka naudingus pakeitimus, naudojant nedidelę padėkos nuorodą istorijos puslapyje arba skirtumų puslapyje. @@ -486,7 +493,7 @@ Prieiga prie medijos vietos uždrausta Gali būti, kad negalėsime automatiškai gauti vietos duomenų iš jūsų įkeltų nuotraukų. Prieš pateikdami kiekvienai nuotraukai pridėkite tinkamą vietą Įkelkite nuotraukas į Vikiteką tiesiai iš savo telefono. Atsisiųskite Vikitekos programėlę dabar: %1$s - Dalintis programą per ... + Dalintis programą per … Vaizdo informacija Kategorijų nerasta Vaizdų nerasta @@ -746,7 +753,7 @@ Kita problema arba informacija (paaiškinkite toliau). Jūsų atsiliepimai bus paskelbti šiame viki puslapyje: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile App/Feedback</a> Ar tikrai norite atšaukti visus įkėlimus? - Atšaukiami visi įkėlimai... + Atšaukiami visi įkėlimai… Įkėlimai Laukiama Nepavyko diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 7a6d9e362..9038eec9d 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -21,7 +21,7 @@ Reģistrēties Pieslēdzas Lūdzu, uzgaidiet… - Lūdzu, uzgaidi... + Lūdzu, uzgaidi… Ieiešana veiksmīga Pieteikšanās neizdevās. Autentifikācija neizdevās! @@ -163,7 +163,7 @@ Nākamais attēls Skatīt arhivētos Skatīt nelasītos - Lūdzu, uzgaidiet... + Lūdzu, uzgaidiet… Izlaist šo attēlu Autors Autortiesības diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 916f4f420..c496505ae 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -4,7 +4,7 @@ * Violetova * Vlad5250 --> - + Ризницата на Фејсбук Изворен код на Ризницата на Github Лого на Ризницата @@ -52,7 +52,7 @@ %1$d подигања - Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликата и вашиот уред + Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликата и вашиот уред Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликите и вашиот уред Истражи @@ -73,7 +73,7 @@ Најава Почекајте… Поднова на толкувања и описи - Почекајте... + Почекајте… Најавата е успешна! Најавата не успеа! Не ја пронајдов податотеката. Пробајте со друга. @@ -479,7 +479,7 @@ Погл. прочитани Погл. непрочитани Се јави грешка при избирањето на сликите - Почекајте... + Почекајте… Избраните слики се дела на високообучени фотографи и илустратори кои заедницата ги избрала за да бидат истакнати како едни од најдобрите слики на Ризницата. Сликите подигнати преку „Околни места“ се оние подигнати при откривање на места на картата. Ова им дава можност на уредниците да им испраќаат благодарници на корисниците што вршат полезни уредувања. Ова се прави стискајќи на малата врска за заблагодарување во страницата за историја или разлики. @@ -501,7 +501,7 @@ Одибиен пристапот до местоположбата на сликата Можеби нема да можеме автоматски да ги добиеме податоците за местоположба од сликите што ги подигате. Ставете ја соодветната местоположба за секоја слика пред да подигате Подигајте слики непосредно на Ризницата од телефон. Преземете го прилогот на Ризницата сега: %1$s - Сподели преку... + Сподели преку… Инфо за сликата Не пронајдов ниедна категорија Не пронајдов ниедно прикажување @@ -780,7 +780,7 @@ Друг проблем или информација (објаснете подолу). Вашите мислења се објавуваат на следнава викистраница: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Дали сигурно сакате да ги откажете сите подигања? - Ги откажувам сите подигања... + Ги откажувам сите подигања… Подигања Во исчекување Неуспешно diff --git a/app/src/main/res/values-mni/strings.xml b/app/src/main/res/values-mni/strings.xml index de888dcbc..0d8e029a4 100644 --- a/app/src/main/res/values-mni/strings.xml +++ b/app/src/main/res/values-mni/strings.xml @@ -18,7 +18,7 @@ ꯈꯨꯠꯌꯦꯛ ꯄꯤꯈꯠꯂꯨ ꯃꯅꯨꯡ ꯆꯪꯁꯤꯟꯂꯤ ꯉꯥꯏꯍꯥꯛ ꯉꯥꯏꯕꯤꯌꯨ - ꯉꯥꯏꯍꯥꯛꯇꯪ ꯉꯥꯏꯕꯤꯈꯣ... + ꯉꯥꯏꯍꯥꯛꯇꯪ ꯉꯥꯏꯕꯤꯈꯣ… ꯃꯥꯏꯄꯥꯛꯅꯥ ꯆꯪꯁꯤꯜꯂꯦ ꯫ ꯆꯪꯁꯤꯟꯕ ꯃꯥꯏꯄꯥꯛꯇꯔꯦ! ꯐꯥꯏꯜ ꯊꯤꯕꯥ ꯐꯪꯗꯔꯦ ꯫ ꯆꯥꯟꯕꯤꯗꯨꯅꯥ ꯑꯇꯣꯞꯄ ꯑꯃꯥ ꯇꯧꯕꯤꯔꯣ ꯫ @@ -59,7 +59,7 @@ ꯍꯣꯏ! ꯑꯍꯦꯟꯕ ꯋꯥꯔꯣꯜ ꯃꯆꯥꯈꯥꯏꯕꯁꯤꯡ - ꯆꯤꯡꯂꯤ/ꯎꯪꯂꯤ..... + ꯆꯤꯡꯂꯤ/ꯎꯪꯂꯤ….. ꯑꯃꯠꯇ ꯈꯟꯗꯦ ꯑꯀꯨꯞꯄ ꯃꯔꯣꯜ ꯌꯥꯎꯗꯦ ꯈꯟꯅ-ꯅꯩꯅꯕ ꯂꯩꯇꯦ diff --git a/app/src/main/res/values-mnw/strings.xml b/app/src/main/res/values-mnw/strings.xml index a6c18bca3..27a76b0a7 100644 --- a/app/src/main/res/values-mnw/strings.xml +++ b/app/src/main/res/values-mnw/strings.xml @@ -45,7 +45,7 @@ ဝိုတ်စ မအက္ခရ်ပၞုက် ပတိုန် စၟတ်သမ္တီ လုပ်လံက်အေန် ဒၟံင် - ပဂုန်တုဲ မင်မွဲလစုတ်... + ပဂုန်တုဲ မင်မွဲလစုတ်… လုက်အေန် အာစိုပ်ဒတုဲ! လံက်အေန် လီုလာ်! ဝှာင် ဟွံဂွံဆဵု၊ ပဂုန်တုဲ ဂၠာဲ ဝှာင်တၞဟ်။ @@ -148,7 +148,7 @@ ယွံ! ဆက်လဴ ပရူတင်ဂၞင် ကဏ္ဍဂမၠိုင် - ပတိုန်ဒၟံင်... + ပတိုန်ဒၟံင်… ဟွံမဲကဵု ပရေၚ်ရုဲစှ် ဟွံမဲကဵု က္ဍိုပ်လိက် ဟွံမဲကဵု ဗမံက်ထ္ၜး diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 546b43f4f..965594585 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -17,7 +17,6 @@ %1$d संचिका अपभारीत होत आहे - अद्याप अपभारणे नाहीत एक अपभारण %1$d अपभारणे @@ -94,7 +93,7 @@ प्रतिसाद पाठवा (विपत्राद्वारे) कोणतेही ईमेल क्लायंट स्थापित नाहीत अलीकडे वापरलेले वर्ग - प्रथम संकालनाची प्रतीक्षा करीत आहे ... + प्रथम संकालनाची प्रतीक्षा करीत आहे … आपण अद्याप काहीच चित्रे अपभारीत केली नाहीत. पुन्हा प्रयत्न करा रद्द करा diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 1fce0c0da..e5dd0f3be 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -19,20 +19,16 @@ အားလုံး ယနေ့အတွက် အထူးဓာတ်ပုံ - ဖိုင် %1$d ခု တင်နေသည် ဖိုင် %1$d ခု တင်နေသည် အပ်ပလုဒ်များ စတင်ခြင်း - %1$d ခု တင်ထားသည် %1$d ခု တင်ထားသည် - ဤရုပ်ပုံသည် %1$ အောက်တွင် လိုင်စင်သတ်မှတ်ထးပါမည် ဤရုပ်ပုံများသည် %1$ အောက်တွင် လိုင်စင်သတ်မှတ်ထးပါမည် - %1$d အက်ပလုပ် %1$d အက်ပလုပ်များ ရှာဖွေစူးစမ်းပါ @@ -49,9 +45,9 @@ အကောင့်ဝင်ရန် စကားဝှက် မေ့နေပါသလား မှတ်ပုံတင်ရန် - လော့ဂ်အင် ဝင်ရောက်နေသည်... - ခေတ္တစောင့်ပါ... - ကျေးဇူးပြု၍ ခဏစောင့်ပါ... + လော့ဂ်အင် ဝင်ရောက်နေသည်… + ခေတ္တစောင့်ပါ… + ကျေးဇူးပြု၍ ခဏစောင့်ပါ… လော့အင် အောင်မြင်သည် လော့အင် မအောင်မြင်ပါ ဖိုင်မတွေ့ပါ၊ အခြးဖိုင်တစ်ခု စမ်းကြည့်ပါ။ @@ -133,7 +129,7 @@ ဟုတ်ကဲ့ သတင်းအချက်အလက် ပို၍ ကဏ္ဍများ - ဝန်ဆွဲတင်နေသည်... + ဝန်ဆွဲတင်နေသည်… ဘာမှရွေးချယ်မထားပါ ပုံစာ မရှိ ဖော်ပြချက် မရှိ @@ -315,7 +311,7 @@ မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ ရုပ်ပုံများကိုရွေးနေစဉ် အမှားဖြစ်ပွားခဲ့ပါသည် - ကျေးဇူးပြု၍ ခဏစောင့်ပါ... + ကျေးဇူးပြု၍ ခဏစောင့်ပါ… နမူနာရုပ်ပုံများ အက်ပလုပ်တင်ရန် မဟုတ်ပါ ဤရုပ်ပုံအား ကျော်သွားမည် ဒေါင်းလုဒ် မအောင်မြင်ပါ။ ပြင်ပသိုလှောင်မှုခွင့်ပြုချက်မရှိဘဲ ဖိုင်ဒေါင်းလုဒ်မဆွဲနိုင်ပါ။ diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index bf971f6bc..3b4bf30dc 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -522,7 +522,7 @@ Toegang tot medialocatie geweigerd Het is mogelijk dat we niet automatisch locatiegegevens kunnen verkrijgen van foto\'s die u uploadt. Voeg de locatie bij elke foto toe voordat u die upload Upload foto\'s rechtstreeks vanaf uw telefoon naar Wikimedia Commons. Download de Commons-app nu: %1$s - App delen via... + App delen via… Afbeeldingsinfo Geen categorieën gevonden Geen beschrijvingen gevonden @@ -599,7 +599,7 @@ Als bladwijzer toegevoegd Er is iets fout gegaan. Kan de achtergrond niet instellen Instellen als achtergrond - Wordt ingesteld als achtergrond. Een ogenblik geduld... + Wordt ingesteld als achtergrond. Een ogenblik geduld… Systeem volgen Donker Licht @@ -659,7 +659,7 @@ Kwaliteitsafbeeldingen zijn diagrammen of foto\'s die voldoen aan bepaalde kwaliteitsnormen (die meestal technisch van aard zijn) en waardevol zijn voor Wikimedia-projecten Uploaden hervatten… Uploaden onderbreken… - Uploaden wordt geannuleerd... + Uploaden wordt geannuleerd… Uploaden Annuleren U hebt de beperkte verbindingsmodus ingeschakeld. Alle uploads worden gepauzeerd en worden hervat zodra u deze modus uitschakelt. Beperkte verbindingsmodus is ingeschakeld. diff --git a/app/src/main/res/values-nqo/strings.xml b/app/src/main/res/values-nqo/strings.xml index 7e11ea03a..62e01d4d5 100644 --- a/app/src/main/res/values-nqo/strings.xml +++ b/app/src/main/res/values-nqo/strings.xml @@ -51,9 +51,9 @@ ߌ ߓߘߊ߫ ߢߌ߬ߣߊ߬ ߌ ߟߊ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߠߊ߫؟ ߖߊ߬ߕߋ߬ߘߊ ߟߊߞߊ߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߦߋ߫ ߛߋ߲߬ߠߊ߫ - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… ߝߍ߬ߛߓߍߟߌ ߣߌ߫ ߞߊ߲߬ߛߓߍߟߌ ߟߊߞߎߘߦߊ ߦߴߌ ߘߐ߫ - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߛߎߘߊ߲߫߹ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߗߌߙߏ߲߫߹ ߞߐߕߐ߮ ߡߊ߫ ߛߐ߬ߘߐ߲߬. ߘߏ߫ ߜߘߍ߫ ߡߊߝߍߣߍ߲߫ ߖߊ߰ߣߌ߲߫. @@ -69,7 +69,7 @@ ߊ߬ ߛߐ߲߬ߞߌ߲߫ ߞߵߊ߬ ߦߋ߫ ߊ߬ ߛߐ߲߬ߞߌ߲߫ ߞߵߊ߬ ߦߋ߫ ߒ ߠߊ߫ ߟߊ߬ߦߟߍ߬ߣߍ߲߬ ߞߐ߯ߟߕߊ ߟߎ߬ - ߘߞߐ߬ߣߐ߲߬ߠߌ߲ ߘߐ߫... + ߘߞߐ߬ߣߐ߲߬ߠߌ߲ ߘߐ߫… ߊ߬ ߓߘߊ߫ ߗߌߙߏ߲߫ %1$d%% ߓߘߊ߫ ߘߝߊ߫ ߟߊ߬ߦߟߍ߬ߟߌ ߦߋ߫ ߛߋ߲߬ߠߊ߫ @@ -148,7 +148,7 @@ ߐ߲߬ߐ߲߬ߐ߲߫߹ ߞߎ߲߬ߠߊ߬ߝߎ߬ߟߋ߲߬ ߜߘߍ ߟߎ߬ ߦߌߟߡߊ ߟߎ߬ - ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫... + ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫… ߊ߬ ߡߊ߫ ߓߊߕߐ߬ߡߐ߲߬ ߝߍ߬ߛߓߍߟߌ߫ ߕߍ߫ ߦߋ߲߬ ߞߊ߲߬ߛߓߍߟߌ߫ ߕߴߦߋ߲߬ @@ -408,7 +408,7 @@ ߘߐ߬ߞߊ߬ߙߊ߲߬ߣߍ߲ ߠߎ߬ ߦߋ߫ ߘߐ߬ߞߊ߬ߙߊ߲߬ߓߊߟߌ ߟߎ߬ ߦߋ߫ ߝߎ߬ߕߎ߲߬ߕߌ ߓߌ߬ߟߊ߬ߣߍ߲߫ ߊ߬ ߘߐ߫ ߞߵߌ ߕߏ߫ ߖߌ߬ߦߊ߬ߓߍ ߓߊߕߐ߬ߡߐ߲ ߞߊ߲߬. - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… ߓߘߊ߫ ߓߊߓߌ߬ߟߊ߬ ߖߌ߬ߦߊ߬ߓߍ߬ ߢߌ߬ߡߊ߬ ߟߊߦߟߍ߬ߕߊ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ ߖߌ߬ߦߊ߬ߓߍ߬ ߖߎ߰ ߟߊߦߟߍ߬ߓߊߟߌ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ @@ -418,7 +418,7 @@ ߘߌ߲߬ߞߌߙߊ ߖߌ߬ߦߊ߬ߕߊ߬ߟߊ߲ ߛߎ߮ߦߊ ߛߎ߲ߝߘߍ - ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬... + ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬… ߖߌ߬ߦߊ߬ߓߍ ߞߌ߬ߓߊ߬ߙߏ߬ߦߊ ߦߌߟߡߊߙߋ߲߫ ߕߴߦߋ߲߬ ߘߊ߲߬ߠߊ߬ߕߍ߰ߟߌ ߡߊ߫ ߛߐ߬ߘߐ߲߬ @@ -486,7 +486,7 @@ ߊ߬ ߓߌ߬ߟߊ߬ ߟߊ߬ߡߊ ߘߐ߫ ߞߏ ߘߏ߫ ߓߍ߲߬ߣߍ߫ ߕߎ߲߬ ߕߍ߫. ߘߊ߲߬ߘߊ߲߬ߥߟߊ ߕߍ߫ ߛߐ߲߬ ߘߐߓߍ߲߬ ߠߊ߫. ߊ߬ ߓߌ߬ߟߊ߬ ߘߊ߬ߣߊ߲߬ߥߟߊ ߟߊ߫. - ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫... + ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫… ߞߊ߲ߞߋ ߟߊߓߊ߬ߕߏ߬ ߘߌ߬ߓߌ ߦߋߟߋ߲ @@ -533,9 +533,9 @@ ߟߊߓߊ߯ߙߊߣߍ߲ ߒ ߠߊ߫ ߛߝߊ ߖߌ߬ߦߊ߬ߓߍ ߛߎ߯ߦߊ - ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫... - ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫... - ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫... + ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫… + ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫… + ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫… ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߓߌ߬ߟߊ߬ ߡߋߘߌߦߊ ߝߊߙߊ߲ߝߊ߯ߛߌ ߦߌߟߡߊ߫ ߞߐߜߍ ߘߐߜߍ߫ diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index c097898e9..eab67e076 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -81,7 +81,7 @@ Mandar vòstres comentaris (per corrièl) Cap de client de corrièl pas installat Categorias utilizadas recentament - Espèra de primièra sincronizacion... + Espèra de primièra sincronizacion… Avètz pas encara telecargat cap de fòto. Tornar ensajar Anullar diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 8c64900a5..d0ee73396 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -8,7 +8,7 @@ * Sony dandiwal * ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ --> - + ਕਾਮਨਜ਼ ਮਾਰਕਾ ਇੱਕ ਹੋਰ ਵੇਰਵਾ ਸ਼ਾਮਲ ਕਰੋ ਨਵਾਂ ਯੋਗਦਾਨ ਸ਼ਾਮਲ ਕਰੋ @@ -17,11 +17,10 @@ ਸਾਰੇ ਦਿਨ ਦੀ ਤਸਵੀਰ - ੧ ਫ਼ਾਈਲ ਚੜ੍ਹਾਈ ਜਾ ਰਹੀ ਹੈ + ੧ ਫ਼ਾਈਲ ਚੜ੍ਹਾਈ ਜਾ ਰਹੀ ਹੈ %1$d ਫ਼ਾਈਲਾਂ ਚੜ੍ਹਾਈਆਂ ਜਾ ਰਹੀਆਂ ਹਨ - \@string/contributions_subtitle_zero %1$d upload %1$d ਅੱਪਲੋਡ @@ -30,7 +29,7 @@ %1$d ਸ਼ੁਰੂ ਹੋ ਰਹੇ ਹਨ - &d ਅੱਪਲੋਡ + %1$d ਅੱਪਲੋਡ %1$d ਅੱਪਲੋਡਾਂ ਇਹ ਤਸਵੀਰ ਦਾ %1$s ਹੇਠ ਲਸੰਸ ਜਾਰੀ ਕੀਤੀ ਜਾਵੇਗਾ @@ -45,7 +44,7 @@ ਪਾਰਸ਼ਬਦ ਭੁੱਲ ਗਏ? ਦਾਖ਼ਲਾ ਹੋ ਰਿਹਾ ਹੈ ਉਡੀਕੋ ਜੀ… - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… ਦਾਖ਼ਲ ਹੋਣਾ ਸਫ਼ਲ! ਦਾਖ਼ਲ ਹੋਣਾ ਅਸਫ਼ਲ! ਫ਼ਾਇਲ ਦੀ ਖੋਜ ਨਹੀਂ ਹੋ ਸਕੀ। ਕਿਰਪਾ ਕਰਕੇ ਹੋਰ ਫ਼ਾਇਲ ਖੋਜੋ। @@ -129,7 +128,7 @@ ਹਾਂ! ਹੋਰ ਜਾਣਕਾਰੀ ਸ਼੍ਰੇਣੀਆਂ - ਲੱਦ ਰਿਹਾ ਹੈ... + ਲੱਦ ਰਿਹਾ ਹੈ… ਕੋਈ ਵੀ ਨਹੀਂ ਚੁਣਿਆ ਕੋਈ ਵੇਰਵਾ ਨਹੀਂ ਕੋਈ ਗੱਲਬਾਤ ਨਹੀਂ @@ -201,7 +200,7 @@ ਇਜਾਜ਼ਤ ਦਿਓ ਖ਼ਾਰਜ ਕਰੋ ਧੰਨਵਾਦ ਭੇਜਣਾ: ਸਫਲ ਹੋਇਆ - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… ਉਤਾਰਾ ਕੀਤਾ ਟਿਕਾਣਾ ਲਿਖਤ ਛਾਪੋ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index dcd8ea284..09132f40e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -508,7 +508,7 @@ Zobacz przeczytane Wyświetl nieprzeczytane Wystąpił błąd podczas pobierania zdjęć - Proszę czekać... + Proszę czekać… Polecane zdjęcia to zdjęcia wysoko wykwalifikowanych fotografów i ilustratorów, które społeczność Wikimedia Commons wybrała jako jedne z najwyższych jakości na stronie. Obrazy przesłane przez Pobliskie miejsca to obrazy, które są przesyłane przez odkrywanie miejsc na mapie. Ta funkcja umożliwia redaktorom wysyłanie powiadomień z podziękowaniem do użytkowników, którzy dokonują przydatnych zmian - za pomocą małego linku z podziękowaniem na stronie historii lub na stronie diff. diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index 9c257c273..b7449e957 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -477,7 +477,7 @@ Vëdde lòn ch\'a l\'é stàit lesù Vëdde lòn ch\'a l\'é ancor nen ëstàit lesù A-i é staje n\'eror an selessionand le plance - Ch\'a l\'abia passiensa... + Ch\'a l\'abia passiensa… Le fòto an evidensa a son ëd plance fàite da dij fotògraf e ilustrator motobin àbij che la comunità ëd Wikipedia Commons a l\'ha sernù tra cole ëd qualità pi àuta an sël sit. Le plance carià dai pòst ëd prossimità a son le plance carià con la dëscuverta dij pòst an sla carta. Costa fonsionalità a përmet ai contributor ëd mandé na notìfica d\'aringrassiament a j\'utent ch\'a fan dle modìfiche ùtij - an dovrand na cita liura d\'aringrassiament an sla pàgina dla stòria o cola dle diferense. @@ -499,7 +499,7 @@ Acess a la locassion dël mojen arfudà I podoma pa oten-e an automàtich ij dàit ëd localisassion dle plance che chiel a caria. Për piasì, ch\'a giontà la posission apropià për tute le plance prima ëd mandeje Ch\'a caria dle fòto su Wikimedia Commons diretaman da sò teléfon. Ch\'a dëscaria l\'aplicassion Commons adess: %1$s - Partagé l\'aplicassion via... + Partagé l\'aplicassion via… Anformassion an sla plancia Gnun-e categorìe trovà Gnun-e descrission trovà @@ -776,7 +776,7 @@ Àutr problema o anformassion (për piasì, ch\'a spiega sì-sota). Ij sò sugeriment a saran giontà a coste pàgine wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> É-lo sigur ëd vorèj anulé tuti ij cariament? - Anulament ëd tuti ij cariament... + Anulament ëd tuti ij cariament… Cariament An atèisa Falì diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 4f17da26f..461cb6b1d 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -65,7 +65,7 @@ CC BY 3.0 هو وېشنيزې - رابرسېرېږي... + رابرسېرېږي… هېڅ هم نه دی ټاکل شوی څرگندونه نشته نامعلوم جواز diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 3779a8a51..b0dd3b016 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -28,7 +28,7 @@ * Tuliouel * YuriNikolai --> - + Página do Commons no Facebook Código fonte do Commons no Github Logotipo do Commons @@ -51,32 +51,39 @@ Estado do local Imagem do Dia - carregando arquivo + carregando arquivo + carregando %1$d arquivos carregando %1$d arquivos (%1$d) + (%1$d) (%1$d) Iniciando carregamentos Processando %d carregamento + Processando %d carregamentos Processando %d carregamentos %d carregamento + %d carregamentos %d carregamentos Esta imagem será licenciada sob %1$s + Estas imagens serão licenciadas sob %1$s Estas imagens serão licenciadas sob %1$s %1$d carregamento + %1$d carregamentos %1$d carregamentos - Recebendo conteúdo compartilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da imagem e do seu dispositivo + Recebendo conteúdo compartilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da imagem e do seu dispositivo + Recebendo conteúdo compartilhado. O processamento das imagens pode levar algum tempo, dependendo do tamanho das imagens e do dispositivo Recebendo conteúdo compartilhado. O processamento das imagens pode levar algum tempo, dependendo do tamanho das imagens e do dispositivo Explorar @@ -95,9 +102,9 @@ Esqueceu a senha? Cadastre-se Efetuar login - Por favor, aguarde... + Por favor, aguarde… Atualizando legendas e descrições - Por favor, aguarde... + Por favor, aguarde… Login bem sucedido Falha na identificação Arquivo não encontrado. Tente outro arquivo. @@ -161,7 +168,7 @@ Sobre O Wikimedia Commons é um aplicativo de código aberto criado e mantido por beneficiários e voluntários da comunidade Wikimedia. A Wikimedia Foundation não está envolvida na criação, desenvolvimento ou manutenção do aplicativo. Criar uma nova <a href=\"%1$s\">publicação no GitHub</a> para informar erros e sugestões. - Politica de privacidade + Política de privacidade Créditos Sobre Enviar comentários (por e-mail) @@ -248,7 +255,7 @@ Ponte de Arco-Íris Tulipa Bem-vindo à Wikipédia - Direitos de autor são bem vindo + Direitos de autor são bem-vindo Ópera de Sydney Cancelar Abrir @@ -306,7 +313,7 @@ Commons Avalie-nos Perguntas frequentes - Guia de usuario + Guia de usuário Pular Tutorial A Internet não está disponível Erro ao tentar obter as notificações @@ -521,7 +528,7 @@ Acesso à localização da mídia negado É possível que não possamos obter automaticamente os dados de localização das imagens que você carregar. Por favor adicione a localização adequada para cada imagem antes de envia-las Carregue fotos na wiki Wikimedia Commons, diretamente do seu celular. Baixe o aolicativo Commons agora: %1$s - Compartilhar aplicativo via... + Compartilhar aplicativo via… Informação da imagem Nenhuma categoria encontrada Nenhuma representação encontrada @@ -548,6 +555,7 @@ Sucesso A categoria %1$s foi adicionada. + As categorias %1$s foram adicionadas. As categorias %1$s foram adicionadas. Não foi possível adicionar categorias. @@ -556,6 +564,7 @@ Editar representações O elemento retratado %1$s está adicionado. + Os elementos retratados %1$s estão adicionados. Os elementos retratados %1$s estão adicionados. Não foi possível adicionar os elementos retratados. @@ -767,6 +776,7 @@ Salvar arquivo GPX %d imagem selecinada + %d imagens selecionadas %d imagens selecionadas Escreva algo sobre o item %1$s. Isso será visivel publicamente. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index bad9dc500..19a52d72f 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -20,7 +20,7 @@ * Unamane * Vitorvicentevalente --> - + Página da wiki Commons no Facebook Código-fonte da wiki Commons no Github Logótipo da wiki Commons @@ -44,31 +44,38 @@ Imagem do Dia a carregar %1$d ficheiro + a carregar muitos %1$d ficheiros a carregar %1$d ficheiros (%1$d) + (%1$d) (%1$d) A iniciar carregamentos A processar %d carregamento + A processar %d carregamentos A processar %d carregamentos %d carregamento + %d carregamentos %d carregamentos Esta imagem será licenciada com a %1$s + Estas imagens serão licenciadas com a %1$s Estas imagens serão licenciadas com a %1$s %1$d carregamento + %1$d carregamentos %1$d carregamentos - A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo + A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo + A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo A receber conteúdo partilhado. O processamento das imagens pode demorar algum tempo, dependendo do tamanho das mesmas e do seu dispositivo Explorar @@ -156,8 +163,8 @@ Política de privacidade Créditos Sobre - Enviar comentários (por correio eletrónico) - Não foi instalado nenhum cliente de correio eletrónico + Enviar comentários (por correio eletrónico) + Não foi instalado nenhum cliente de correio eletrónico Categorias usadas recentemente A aguardar pela primeira sincronização… Não carregou ainda nenhuma foto. @@ -276,7 +283,7 @@ Gravar as fotografias tiradas com a câmara da aplicação no armazenamento do seu dispositivo Inicie sessão na sua conta Enviar ficheiro de registos - Enviar o ficheiro de registos aos programadores por correio eletrónico para ajudar a corrigir problemas da aplicação. Nota: os registos podem conter informações identificativas + Enviar o ficheiro de registos aos programadores por correio eletrónico para ajudar a corrigir problemas da aplicação. Nota: os registos podem conter informações identificativas Não foi encontrado nenhum navegador da Internet para abrir o URL Erro! Não foi possível encontrar o URL Nomear para eliminação @@ -491,7 +498,7 @@ Ver lidas Ver não lidas Ocorreu um erro ao escolher imagens - Aguarde, por favor... + Aguarde, por favor… As fotografias destacadas são imagens de fotógrafos e ilustradores altamente qualificados, que a comunidade da wiki Wikimedia Commons escolheu como as de melhor qualidade do \'\'site\'\'. As imagens carregadas via \"Locais próximos\" são as imagens que são carregadas descobrindo locais do mapa. Esta funcionalidade permite que os editores enviem uma notificação de agradecimento aos utilizadores que fizerem edições úteis - usando uma pequena hiperligação de agradecimento na página do historial ou na de diferenças. @@ -513,7 +520,7 @@ Acesso à localização de multimédia negado Podemos não conseguir obter automaticamente os dados de localização das fotografias que carregar. Adicione a localização apropriada de cada fotografia antes de a enviar, por favor Carregue fotografias na wiki Wikimedia Commons, diretamente do seu telemóvel. Descarregue a aplicação Commons agora: %1$s - Partilhar aplicação por... + Partilhar aplicação por… Informação da imagem Não foi encontrada nenhuma categoria Não foi encontrada nenhuma representação @@ -540,6 +547,7 @@ Êxito A categoria %1$s foi adicionada. + As categorias %1$s foram adicionadas. As categorias %1$s foram adicionadas. Não foi possível adicionar categorias. @@ -548,6 +556,7 @@ Editar elementos retratados O elemento retratado %1$s está adicionado. + Os elementos retratados %1$s estão adicionados. Os elementos retratados %1$s estão adicionados. Não foi possível adicionar os elementos retratados. @@ -589,7 +598,7 @@ Adicionado aos marcadores Ocorreu um problema. Não foi possível definir a imagem de fundo Definir como imagem de fundo - A definir a imagem de fundo. Aguarde, por favor... + A definir a imagem de fundo. Aguarde, por favor… Seguir sistema Escuro Claro @@ -645,8 +654,8 @@ Modo de ligação limitada Imagens de qualidade As imagens de qualidade são diagramas ou fotografias que satisfazem certos padrões de qualidade (principalmente de natureza técnica) e são valiosos para projetos da Wikimedia - A retomar carregamento... - A pausar carregamento... + A retomar carregamento… + A pausar carregamento… A cancelar o carregamento… Cancelar carregamento Ativou o modo de ligação limitada. Todos os carregamentos foram colocados em pausa e serão retomados quando desativar este modo. @@ -709,7 +718,7 @@ Não foi encontrada nenhuma localização Que tal adicionar o local onde a imagem foi tirada?\nOs dados de localização ajudam os editores da wiki a encontrarem a sua fotografia, tornando-a muito mais útil.\nObrigado! Adicionar localização - Remova desta mensagem de correio todas as informações que não se sinta à vontade em partilhar publicamente, por favor. Adicionalmente, esteja consciente de que o seu endereço de correio eletrónico, com o qual está a fazer esta publicação, e o nome e imagem de perfil a ele associados, serão visíveis pelo público geral. + Remova desta mensagem de correio todas as informações que não se sinta à vontade em partilhar publicamente, por favor. Adicionalmente, esteja consciente de que o seu endereço de correio eletrónico, com o qual está a fazer esta publicação, e o nome e imagem de perfil a ele associados, serão visíveis pelo público geral. Detalhes As realizações só estão disponíveis na versão de produção; consulte a documentação para programadores, por favor. A tabela de classificação só está disponível na versão prod. Consulte a documentação do desenvolvedor. @@ -760,6 +769,7 @@ Erro no envio de agradecimento ao autor. %d imagem selecionada + %d imagens selecionadas %d imagens selecionadas diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 0bcbc1550..ad1d0b805 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -28,8 +28,8 @@ %1$d de fișiere se încarcă - \@string/contributions_subtitle_zero (%1$d) + (%1$d) (%1$d) Pornirea încărcărilor @@ -74,8 +74,8 @@ V-ați uitat parola? Înregistrare Se conectează - Vă rugăm să așteptați ... - Vă rugăm să așteptați ... + Vă rugăm să așteptați … + Vă rugăm să așteptați … Autentificare reușită! Autentificare nereușită! Fișierul nu a fost găsit. Încercați cu un alt fișier. @@ -458,7 +458,7 @@ Vezi citit Vezi necitit A apărut o eroare la alegerea imaginilor - Vă rugăm să așteptați ... + Vă rugăm să așteptați … Imaginile de Calitate sunt imagini ale unor fotografi și ilustratori de înaltă calificare, pe care comunitatea Wikimedia Commons a ales-o ca fiind de cea mai înaltă calitate pe site. Imaginile Încărcate prin Locurile din Apropiere sunt imaginile care sunt încărcate prin descoperirea locurilor de pe hartă. Această caracteristică permite editorilor să trimită o notificare de Mulțumire utilizatorilor care fac modificări utile - folosind un mic link de mulțumire pe pagina istoric sau pe pagina dif. @@ -478,7 +478,7 @@ Numere Serie Software Încărcați fotografii pe Wikimedia Commons direct de pe telefon. Descărcați aplicația Commons acum: %1$s - Partajează aplicația prin ... + Partajează aplicația prin … Informații despre imagine Nu s-au găsit categorii Nu s-au Găsit Reprezentări diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b15d77787..861d7ee27 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -45,7 +45,7 @@ * ЛингвоЧел * ОйЛ --> - + Facebook-страница Викисклада Исходный код Викисклада на гитхабе Логотип Викисклада @@ -105,7 +105,7 @@ %1$d загрузок - Получение общего содержимого. Обработка изображения может занять некоторое время, в зависимости от размера изображения и модели вашего устройства + Получение общего содержимого. Обработка изображения может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства @@ -556,7 +556,7 @@ Отказано в доступе к местоположению файла Возможно, мы не сможем автоматически получать данные о местоположении из загруженных вами изображений. Пожалуйста, добавьте подходящее место для каждого изображения перед отправкой Загружайте фото на Викисклад прямо с телефона. Скачайте приложение Wikimedia Commons прямо сейчас: %1$s - Поделиться приложением с помощью... + Поделиться приложением с помощью… Информация об изображении Категории не найдены. Описания не найдены @@ -637,7 +637,7 @@ Добавлено в закладки Что-то пошло не так. Не удалось установить фоновую заставку Сделать фоновой заставкой - Идёт установка фоновой заставки... + Идёт установка фоновой заставки… Настройки системы Тёмная Светлая @@ -695,8 +695,8 @@ Режим ограниченного подключения Качественные изображения Качественные изображения - это диаграммы или фотографии, которые соответствуют определенным стандартам качества (которые в основном носят технический характер) и представляют ценность для проектов Викимедиа - Возобновление загрузки... - Приостановка загрузки... + Возобновление загрузки… + Приостановка загрузки… Отмена загрузки… Отменить загрузку Вы включили ограниченный режим подключения. Все загрузки приостановлены и возобновятся после отключения этого режима. @@ -841,7 +841,7 @@ Другая проблема или информация (пожалуйста, объясните ниже). Ваш отзыв будет опубликован на следующей вики-странице: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Вы уверены, что хотите отменить все загрузки? - Отмена всех загрузок... + Отмена всех загрузок… Загрузки В ожидании Не удалось diff --git a/app/src/main/res/values-sd/strings.xml b/app/src/main/res/values-sd/strings.xml index 08f9a1fec..d4b591659 100644 --- a/app/src/main/res/values-sd/strings.xml +++ b/app/src/main/res/values-sd/strings.xml @@ -169,7 +169,7 @@ ھا! وڌيڪ معلومات زمرا - لاهيندي... + لاهيندي… ڪوبہ چونڊيل ناھي عنوان ناهي ڪا تشريح ناھي @@ -315,7 +315,7 @@ لينس ماڊل سيريل انگ سافٽويئر - ايپ ذريعي ونڊيو... + ايپ ذريعي ونڊيو… عڪس معلومات زمرا نہ لڌا رد-ڪيل چاڙھ diff --git a/app/src/main/res/values-se/strings.xml b/app/src/main/res/values-se/strings.xml index 78114e336..0489c363a 100644 --- a/app/src/main/res/values-se/strings.xml +++ b/app/src/main/res/values-se/strings.xml @@ -44,9 +44,9 @@ Vajáldahttetgo beassansáni? Searvva Čáliha sisa - Vuordil... + Vuordil… Ođasmáhttá govvateavsttaid ja govvádusaid - Vuordil... + Vuordil… Sisačáliheapmi lihkostuvai! Sisačáliheapmi ii lihkostuvvan! Fiila ii gávdnon. Geahččal áinnas eará fiilla. @@ -112,7 +112,7 @@ Atte máhcahaga (e-poasttain) Ii leat ásahuvvon epoastadoaimmaheaddji Áitto geavahuvvon kategoriijat - Vuordime vuosttaš synkroniserema... + Vuordime vuosttaš synkroniserema… It leat vel bajásluđen ovttage gova. Geahččal ođđasit Gaskkalduhte @@ -143,7 +143,7 @@ Jua! Lassedieđut Kategoriijat - Luđeme... + Luđeme… Ii guhtege válljejuvvon Ii leat govvateaksta Ii gávdno govvádus diff --git a/app/src/main/res/values-sh/strings.xml b/app/src/main/res/values-sh/strings.xml index 5997677ef..8e9cde75c 100644 --- a/app/src/main/res/values-sh/strings.xml +++ b/app/src/main/res/values-sh/strings.xml @@ -112,7 +112,7 @@ Pošaljite Vašu povratnu informaciju (putem e-pošte) Nemate uspostavljen klijent za e-poštu Nedavno korištene kategorije - Čekam prvo usklađivanje... + Čekam prvo usklađivanje… Još uvijek niste otpremili nijednu sliku. Pokušaj ponovo Otkaži @@ -147,7 +147,7 @@ Da! Više informacija Kategorije - Učitavanje... + Učitavanje… Ništa nije odabrano Nema opisa Nema razgovora diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 92fa25f3e..0e661acb7 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -5,25 +5,24 @@ * Sandaru * හරිත --> - + කොමන්ස් ෆේස්බුක් පිටුව කොමන්ස් ලාන්චනය කොමන්ස් වෙබ් අඩවිය - 1 ගොනුවක් උඩුගත කෙරේ + 1 ගොනුවක් උඩුගත කෙරේ ගොනු %d ක් උඩුගත කෙරේ - තවමත් කිසිදු උඩුගත කිරීමක් නැත - එක් උඩුගත කිරීමක් ඇත + එක් උඩුගත කිරීමක් ඇත උඩුගත කිරීම් %1$d ක් ඇත - 1 උඩුගත කිරීමක් ආරම්භ කරමින් + 1 උඩුගත කිරීමක් ආරම්භ කරමින් උඩුගත කිරීම් %1$d ක් ආරම්භ කරමින් - 1 උඩුගත කිරීමක් + 1 උඩුගත කිරීමක් උඩුගත කිරීම් %1$d ක් මෙම පින්තූරය %1$s යටතේ වලංගු වනු ඇත diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 49fc88a3b..99a0bf548 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -491,7 +491,7 @@ Zobraziť prečítané Zobraziť neprečítané Nastala chyba pri vyberaní obrázkov - Čakajte, prosím... + Čakajte, prosím… Najlepšie obrázky sú fotografie od vysoko skúsených fotografov a ilustrátorov, ktoré vybrala komunita Wikimedie Commons ako jedny z najkvalitnejších na stránke. Obrázky nahrané cez Miesta v okolí sú obrázky, ktoré sú nahrané vďaka objavovaniu miest na mape. Táto funkcia umožňuje poslať poďakovanie za užitočné úpravy používateľom – použitím malého odkazu poďakovať v histórií stránky alebo na stránke rozdielu medzi revíziami. @@ -513,7 +513,7 @@ Prístup k polohe médií bol odmietnutý Možno nebudeme môcť automaticky získať údaje o polohe z obrázkov, ktoré nahráte. Pred odoslaním, prosím, pridajte ku každému obrázku údaj o polohe. Nahrávajte fotky na Wikimedia Commons priamo z vášho mobilu. Stiahnite si aplikáciu Wikimedia Commons teraz: %1$s - Zdieľať aplikáciu cez... + Zdieľať aplikáciu cez… Informácie o obrázku Nenájdené žiadne kategórie Neboli nájdené spôsoby vykreslovania @@ -593,7 +593,7 @@ Pridané do záložiek Niečo sa pokazilo. Tapetu sa nepodarilo nastaviť Nastaviť ako tapetu - Nastavujem tapetu. Prosím, čakajte... + Nastavujem tapetu. Prosím, čakajte… Predvolený systém Tmavý Svetlý @@ -651,9 +651,9 @@ Mód limitovaného pripojenia Kvalitné obrázky Kvalitné obrázky sú diagramy a fotografie, ktoré spĺňajú určité štandardy (ktoré sú väčšinou technického charakteru) a sú cenné pre projekty Wikimédie - Pokračovanie nahrávania... - Pozastavovanie nahrávania... - Prerušovanie nahrávania... + Pokračovanie nahrávania… + Pozastavovanie nahrávania… + Prerušovanie nahrávania… Zrušiť nahrávanie Zapli ste mód limitovaného pripojenia. Všetky nahrávania budú teraz pozastavené a budú pokračovať až po vypnutí tohto módu. Mód limitovaného pripojenia je zapnutý. @@ -787,7 +787,7 @@ Iný problém alebo informácia (vysvetlite nižšie). Vaša spätná väzba sa zverejní na nasledujúcej wiki stránke: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Ste si istí, že chcete zrušiť všetky nahrávania? - Ruším všetky nahrávania... + Ruším všetky nahrávania… Nahrané súbory Čakajúce Zlyhané diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 61531980f..b91c3c0b1 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -6,7 +6,7 @@ * McDutchie * Upwinxp --> - + Facebook stran Zbirke Izvorna koda Zbirke v shrambi Github Logotip Zbirke @@ -66,8 +66,8 @@ %1$d nalaganj - Prejemam deljeno vsebino. Obdelava slike lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. - Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. + Prejemam deljeno vsebino. Obdelava slike lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. + Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. @@ -87,9 +87,9 @@ Ste pozabili geslo? Ustvari račun Prijavljanje - Prosimo, počakajte ... + Prosimo, počakajte … Posodabljam napise in opise - Prosimo, počakajte ... + Prosimo, počakajte … Uspešno ste se prijavili! Prijava ni uspela! Datoteka ni bila najdena. Prosimo, poskusite z drugo datoteko. @@ -133,7 +133,7 @@ Spremembe Naloži Poišči kategorije - Poiščite predmete, ki jih vaša predstavnostna datoteka prikazuje (gora, Tadž Mahal, ...) + Poiščite predmete, ki jih vaša predstavnostna datoteka prikazuje (gora, Tadž Mahal, …) Shrani Osveži Seznam @@ -159,7 +159,7 @@ Pošljite povratno informacijo (prek e-pošte) Nameščen ni noben e-poštni odjemalec Pred kratkim uporabljene kategorije - Čakam na prvo sinhronizacijo ... + Čakam na prvo sinhronizacijo … Naložili niste še nobene fotografije. Poskusi znova Prekliči @@ -199,7 +199,7 @@ Da! Več informacij Kategorije - Nalaganje ... + Nalaganje … Nič ni izbrano Ni napisa Ni opisa @@ -491,7 +491,7 @@ Ogled prebranih Ogled neprebranih Pri izbiri slik je prišlo do napake - Prosimo, počakajte ... + Prosimo, počakajte … Izbrane slike so slike izvrstnih fotografov in ilustratorjev, ki jih je skupnost Wikimedijine zbirke prepoznala kot najbolj kakovostne v tem projektu. Slike, naložene z Bližnjimi kraji, so slike, ki so naložene z odkrivanjem krajev na zemljevidu. Ta možnost vam omogoča, da urejevalcem, ki so opravili koristno urejanje, pošljete zahvalo – z uporabo kratke povezave na strani zgodovine ali strani primerjave. @@ -513,7 +513,7 @@ Dostop do lokacije predstavnosti zavrnjen Za slike, ki jih nalagate, ne moremo samodejno pridobiti lokacije. Pred pošiljanjem dodajte za vsako sliko ustrezno lokacijo. Nalagajte slike v Wikimedijino zbirko neposredno iz telefona. Prenesite aplikacijo Commons zdaj: %1$s - Deli aplikacijo prek ... + Deli aplikacijo prek … Informacije o sliki Ni najdenih kategorij Ni najdenih upodobitev @@ -569,7 +569,7 @@ Koordinat ni bilo mogoče pridobiti. Ni bilo mogoče pridobiti opisov. Uredi opise in napise - Deli slike prek ... + Deli slike prek … Ničesar še niste prispevali %s ni opravil_a še nobenega prispevka Račun ustvarjen! @@ -593,7 +593,7 @@ Dodano med zaznamke Nekaj je šlo narobe. Ozadja ni bilo mogoče nastaviti. Nastavi kot ozadje - Nastavljam ozadje. Prosimo, počakajte ... + Nastavljam ozadje. Prosimo, počakajte … Sledi sistemu Temna Svetla @@ -649,9 +649,9 @@ Način omejene povezanosti Kakovostne slike Kakovostne slike so ponazoritve ali fotografije, ki ustrezajo nekaterim merilom kakovosti (ta so predvsem tehnična) in so dragocene za projekte Wikimedie - Nalaganje se nadaljuje ... - Zaustavljam nalaganje ... - Preklicujem nalaganje ... + Nalaganje se nadaljuje… + Zaustavljam nalaganje… + Preklicujem nalaganje… Preklic nalaganja Vklopili ste način omejene povezanosti. Vsa nalaganja so začasno ustavljena in se bodo nadaljevala, ko boste ta način izklopili. Način omejene povezanosti je vklopljen. diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index fe70bc6b6..f1e7412d4 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -34,32 +34,39 @@ Слика дана %1$d датотека се отпрема + %1$d датотеке се отпремају %1$d датотеке се отпремају %1$d отпремање + %1$d отпремања %1$d отпремања Покретање отпремања Процесуирање %d отпремање + Процесуирање %d отпремања Процесуирање %d отпремања %d отпремање + %1$d отпремања %d отпремања Слика ће се водити под лиценцом %1$s + Слике ће се водити под лиценцом %1$s Слике ће се водити под лиценцом %1$s %1$d отпремање + %1$d отпремања %1$d отпремања - Примање дељеног садржаја... Процесуирање слике може потрајати неко време, у зависности од величине слике и вашег уређаја - Примање дељеног садржаја... Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја + Пријем %d дељеног садржаја… Процесуирање слике може потрајати неко време, у зависности од величине слике и вашег уређаја + Пријем %d дељеног садржаја… Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја + Пријем %d дељеног садржаја… Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја Истрага Изглед @@ -496,7 +503,7 @@ Приступ локацији медија је одбијен Можда нећемо моћи да аутоматски прибавимо податке о локацији из слика које отпремите. Додајте одговарајућу локацију за сваку слику пре објављивања Отпреми фотографије на Викимедијину Оставу директно са свог телефона. Преузми апликацију Оставе сада: %1$s - Подели апликацију преко... + Подели апликацију преко… Информације о слици Нису пронађене категорије Отказано отпремање @@ -521,12 +528,13 @@ Успешно Категорија %1$s је додата. + Категорије %1$s су додате. Категорије %1$s су додате. Није могуће додати категорије. Ажурирај категорију Уреди приказе - Ажурирање координата... + Ажурирање координата… Ажурирање координата Ажурирање описа Ажурирање натписа @@ -729,6 +737,7 @@ Чување GPX датотеке %d слика је одабрана + %d слике су одабране %d слика је одабрано Унесите коментар diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml index 79ae5ea28..64379ac92 100644 --- a/app/src/main/res/values-su/strings.xml +++ b/app/src/main/res/values-su/strings.xml @@ -26,32 +26,25 @@ Togel ka Luhur Gambar poé ieu - ngunjal %1$d berkas ngunjal %1$d berkas - (%1$d) (%1$d) Mitembeyan Ngamuat - Ngolah %d muatan Ngolah %d muatan - %1$d muatan %1$d muatan - Ieu gambar bakal dilisénsi %1$s Ieu gambar bakal dilisénsi %1$s - %1$d Dimuat %1$d Dimuat - Nampa kontén anu dibagikeun. Ngolah gambarna bisa jadi rada lila gumantung kana ukuran gambar jeung gaway anjeun Nampa kontén anu dibagikeun Langlang @@ -71,7 +64,7 @@ Asup log Tungguan… Nganyarkeun pertélaan jeung pedaran - Mangga tungguan... + Mangga tungguan… Laksana login! Gagal login! Berkas teu kapanggih. Coba berkas séjén. @@ -399,7 +392,7 @@ Tempo arsip Tempo nu can dibaca Éror pas keur nyomot gambar - Mangga tungguan... + Mangga tungguan… Iwalkeun ieu gambar Karya Hak cipta @@ -409,7 +402,7 @@ Nomer Seri Sopwér Muat poto ka Wikimedia Commons langsung tina ponsél. Unduh Commons App ayeuna: %1$s - Bagikeun app liwat... + Bagikeun app liwat… Info Gambar Euweuh Kategori kapanggih Muatan bedo diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 370bf0915..c49d64ad2 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -506,7 +506,7 @@ Åtkomst till mediaplats nekades Vi kanske inte automatiskt kan få platsdata från bilder du laddar upp. Lägg till lämplig plats för varje bild innan du skickar in Ladda upp foton till Wikimedia Commons direkt från din telefon. Ladda ned Commons-appen nu: %1$s - Dela appen via... + Dela appen via… Bildinfo Inga kategorier hittades Inga beskrivningar hittades @@ -783,7 +783,7 @@ Andra problem eller information (ange nedan). Din återkoppling kommer att skickas till följande wikisida: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobilapp/Återkoppling</a> Är du säker på att du vill avbryta alla uppladdningar? - Avbryter alla uppladdningar... + Avbryter alla uppladdningar… Uppladdningar Pågår Misslyckades diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index f9162bc7b..4f41da5f6 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -98,7 +98,7 @@ பின்னூட்டம் அனுப்பு (மின்னஞ்சல் வழியாக) மின்னஞ்சற் செயலி எதுவும் நிறுவப்படவில்லை அண்மையிற் பயன்படுத்தப்பட்ட பகுப்புகள் - முதல் ஒத்திசைவுக்காக காத்திருக்கிறது ... + முதல் ஒத்திசைவுக்காக காத்திருக்கிறது … நீர் இன்னும் எவ்வொளிப்படத்தையும் பதிவேற்றவில்லை. மீண்டும் முயல்க கைவிடு @@ -131,7 +131,7 @@ ஆம்! மேலதிக தகவல்கள் பகுப்புகள் - ஏற்றப்படுகிறது... + ஏற்றப்படுகிறது… தெரிவு செய்யப்படவில்லை தலைப்பு இல்லை விளக்கம் இல்லை diff --git a/app/src/main/res/values-tcy/strings.xml b/app/src/main/res/values-tcy/strings.xml index add46f7b7..13ee985b9 100644 --- a/app/src/main/res/values-tcy/strings.xml +++ b/app/src/main/res/values-tcy/strings.xml @@ -110,7 +110,7 @@ ಇರೆನ ಅಬಿಪ್ರಾಯೊ ಬರೆಲೆ(ಮಿಂಚಂಚೆ). ಇರೆನ ಮಿಂಚಂಚೆ ಇಜ್ಜಿ. ಇಂಚಿಗ್ ಸೃಷ್ಟಿ ಮಾಲ್ತಿನ ವರ್ಗೊ. - ಒಂತೆ ಸಮಯ ಕಾಯೊಡು.... + ಒಂತೆ ಸಮಯ ಕಾಯೊಡು…. ಇರ್ ಒಂಜಿಲಾ ಪಟೋನ್ ಅಪ್ಲೋಡ್ ಮಾಲ್ತಿಜ್ಜಿ. ನನೊರ ಪ್ರಯತ್ನ ಮಾನ್ಪುಲೇ ವಜಾ ಮಲ್ಪುಲೆ @@ -336,7 +336,7 @@ ಅನುರಕ್ಷಿತ ತೂಲೆ ಓದಂದಿನ ತೂಲೆ ಆಕೃತಿಲೆನ್ ಪೆಜ್ಜಿನಗ ದೋಷ ಆಂಡ್ - ದಯಮಲ್ತ್ ಕಾಪುಲೆ... + ದಯಮಲ್ತ್ ಕಾಪುಲೆ… ಸಂಯೋಜನೆಲು ಸೂಚನೆಲು ನನಾತ್ diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index ae80a5335..0c478b221 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -127,7 +127,7 @@ ఫీడుబ్యాకును పంపండి (ఈమెయిలు ద్వారా) ఈమెయిలు క్లయంటేదీ లేదు ఇటీవల వాడిన వర్గాలు - మొట్టమొదటి సింక్ కోసం చూస్తున్నాం... + మొట్టమొదటి సింక్ కోసం చూస్తున్నాం… ఇంకా మీరు ఫోటోలేమీ ఎక్కించలేదు. మళ్ళీ ప్రయత్నించు రద్దుచేయి @@ -457,7 +457,7 @@ క్రమ సంఖ్యలు సాఫ్టువేరు నేరుగా మీ ఫోను నుంచే వికీమీడియా కామన్స్‌కు ఫోటోలను ఎక్కించండి. కామన్స్ యాప్‌ను ఇప్పుడే దించుకోండి: %1$s - యాప్‌ను దీని ద్వారా పంచుకోండి... + యాప్‌ను దీని ద్వారా పంచుకోండి… బొమ్మ సమాచారం వర్గాలేమీ కనబడలేదు ఎక్కింపును రద్దు చేసాం @@ -523,7 +523,7 @@ బుక్‌మార్కులకు చేర్చాం ఏదో లోపం జరిగింది. వాల్‌పేపరును సెట్ చెయ్యలేకపోయాం వాల్‌పేపరుగా అమర్చు - వాల్‌పేపరుగా సెట్ చేస్తున్నాం. కాస్త ఆగండి... + వాల్‌పేపరుగా సెట్ చేస్తున్నాం. కాస్త ఆగండి… నల్లటి వెలుగుతో స్థానపు సెట్టింగులను తెరవడం విఫలమైంది. స్థానాన్ని మానవికంగా ఆన్ చెయ్యండి @@ -576,9 +576,9 @@ పరిమిత కనెక్షను మోడ్‌ను అచేతనం చేసాం. పెండింగులో ఉన్న ఎక్కింపులు తిరిగి మొదలౌతాయి. పరిమిత కనెక్షను మోడ్ నాణ్యమైన బొమ్మలు - ఎక్కింపును తిరిగి మొదలెడుతున్నాం... - ఎక్కింపును నిలుపుతున్నాం... - ఎక్కింపును రద్దు చేస్తున్నాం... + ఎక్కింపును తిరిగి మొదలెడుతున్నాం… + ఎక్కింపును నిలుపుతున్నాం… + ఎక్కింపును రద్దు చేస్తున్నాం… ఎక్కింపును రద్దుచెయ్యి మీరు పరిమిత కనెక్షను మోడ్‌ను చేతనం చేసారు. ఎక్కింపులన్నీ నిలిచిపోయాయి. మీరు ఈ మోడ్‌ను అచేతనం చెయ్యగానే అవి తిరిగి మొదలౌతాయి. పరిమిత కనెక్షను మోడ్ ఆన్ అయింది. diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 125bba590..70bee59ef 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -38,21 +38,16 @@ รูปภาพประจำวัน กำลังอัปโหลดไฟล์ %1$d ไฟล์ - \@string/contributions_subtitle_zero - (%1$d) (%1$d) กำลังเริ่มอัปโหลด - กำลังเริ่มอัปโหลด %1$d รายการ กำลังเริ่มอัปโหลด %1$d รายการ - การอัปโหลด %1$d รายการ การอัปโหลด %1$d รายการ - ภาพนี้จะอยู่ในสัญญาอนุญาต %1$s ภาะเหล่านี้จะอยู่อยู่ในสัญญาอนุญาติ %1$s สำรวจ @@ -398,7 +393,7 @@ รุ่นเลนส์ หมายเลขซีเรียล ซอฟต์แวร์ - แบ่งปันแอปผ่าน... + แบ่งปันแอปผ่าน… ไม่พบหมวดหมู่ ภาพเซลฟี ภาพเบลอ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 31a9f0b53..79482fb4e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -210,7 +210,7 @@ Evet! Daha Fazla Bilgi Kategoriler - Yükleniyor... + Yükleniyor… Hiçbir şey seçilmedi Altyazı yok Açıklama yok @@ -505,7 +505,7 @@ Okunanları görüntüle Okunmayanları görüntüle Resimler seçilirken hata oluştu - Lütfen bekleyin... + Lütfen bekleyin… Seçkin resimler, Wikimedia Commons topluluğunun sitedeki en yüksek kaliteden bazıları olarak seçtiği son derece yetenekli fotoğrafçıların ve illüstratörlerin görüntüleridir. Yakındaki yerler üzerinden yüklenen resimler, haritadaki yerleri keşfederek yüklenen resimlerdir. Bu özellik, editörlerin, geçmiş sayfasında veya fark sayfasında küçük bir teşekkür bağlantısı kullanarak faydalı düzenlemeler yapan kullanıcılara bir Teşekkür bildirimi göndermesine olanak tanır. @@ -604,7 +604,7 @@ Yer işaretlerine eklendi Bir şeyler yanlış gitti. Duvar kağıdı ayarlanamadı Duvar kağıdı olarak ayarla - Duvar Kağıdı ayarlanıyor. Lütfen bekleyin... + Duvar Kağıdı ayarlanıyor. Lütfen bekleyin… Sistemi izle Koyu Açık diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 9e821ae24..cc2343a77 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -21,7 +21,7 @@ * Ата * Пан Хаунд --> - + Facebook-сторінка Вікісховища Програмний код Вікісховища на GitHub Логотип Вікісховища @@ -81,7 +81,7 @@ %1$d завантажень - Отримання спільного контенту. Обробка зображення може зайняти трохи часу, залежно від розміру зображення і від Вашого пристрою + Отримання спільного контенту. Обробка зображення може зайняти трохи часу, залежно від розміру зображення і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою @@ -612,7 +612,7 @@ Додано у закладки Щось трапилось. Не вдалося встановити шпалери робочого столу Встановити в якості шпалер робочого столу - Встановлення робочого столу. Будь ласка зачекайте... + Встановлення робочого столу. Будь ласка зачекайте… На взірець системи Темна Світла diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index a708c873a..f09f76ac6 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -73,9 +73,9 @@ Parolni unutdingizmi? Roʻyxatdan oʻtish Kirish - Iltimos kuting... + Iltimos kuting… Sarlavhalar va tavsiflarni yangilash - Iltimos, kutib turing... + Iltimos, kutib turing… Kirish muvaffaqiyatli bajarildi! Kirish muvaffaqiyatsiz yakunlandi! Fayl topilmadi. Iltimos, boshqa faylni izlab koʻring. @@ -180,7 +180,7 @@ Ha! Batafsil maʼlumot Turkumlar - Yuklanmoqda... + Yuklanmoqda… Tanlanmagan Izoh yoʻq Tavsif yoʻq @@ -390,7 +390,7 @@ Xatchoʻplar Xatchoʻplar Bajarildi - Iltimos, kuting... + Iltimos, kuting… EXIF teglarni boshqarish Muallif Mualliflik huquqlari diff --git a/app/src/main/res/values-vec/strings.xml b/app/src/main/res/values-vec/strings.xml index bbcb64561..52bb495ea 100644 --- a/app/src/main/res/values-vec/strings.xml +++ b/app/src/main/res/values-vec/strings.xml @@ -68,7 +68,7 @@ Cargamento de %1$s no riusio Schicia par vixuałixare I me ultimi cargamenti - In coa... + In coa… Fałimento %1$d%% conpleto Drio cargar.. @@ -114,7 +114,7 @@ Mandane on comento (co ła mail) Nisun client de posta eletronega instałà Categorie doparà ultimamente - Speta par ła prima sincronixasion... + Speta par ła prima sincronixasion… No te ghe njiancora cargà na foto Riproa Descançełare @@ -403,7 +403,7 @@ Varda no lexeste Varda no lexeste Se ga vuo on eror co se jera drio ełexare łe imajini. - Speta on fià... + Speta on fià… Le foto in primo pian łe xé imajini de fotografi altamente cuałifegai che ła comunità de Wikimedia Commons ła ga ełeto come fotografi de alta cuałità sol sito. Imajini cargae via \"Posti cuà rente\", imajini che łe njien cargae scoerxendo posti n\'te ła mapa Sta funsion ła consente ai editori de enviar na notifega de ringrasiamento ai uxuari che i fa modifeghe che serve, doparando on lingambo picenin de ringrasiamento n\'te ła pajina del storego o n\'te ła pajina de łe difarense.\n\nQuesta funzione consente agli editor di inviare una notifica di ringraziamento agli utenti che apportano modifiche utili, utilizzando un piccolo link di ringraziamento nella pagina della cronologia o nella pagina delle differenze. @@ -421,7 +421,7 @@ Numari seriałi Software Carga foto so Wikimedia Commons diretamente dal to tełefonin. Descarga l\'aplicasion deso: %1$s - Spartisi aplicasion co... + Spartisi aplicasion co… Informasion so l\'imajine Nisuna categoria catada Cargamento nułà @@ -461,7 +461,7 @@ Xonta ai favorii Calcosa el xé ndà roerso. No xé sta pusibiłe canbiar el sfondo Inposta el sfondo - Drio inpostar el sfondo. Speta on fià... + Drio inpostar el sfondo. Speta on fià… Segui el sistema Scuro Ciaro diff --git a/app/src/main/res/values-xal/strings.xml b/app/src/main/res/values-xal/strings.xml index 346ff15e1..c36206061 100644 --- a/app/src/main/res/values-xal/strings.xml +++ b/app/src/main/res/values-xal/strings.xml @@ -23,15 +23,15 @@ Вики-аһулх һазр Тохрллһ Вики-аһулх һазрур ацалх - Ацалгдҗана... + Ацалгдҗана… Кергләчин нерн Нууц үг Невтрх Нууц үгән мартвт? Бүрткүлх Невтрҗәнә - Күләхнтн... - Күләхнтн... + Күләхнтн… + Күләхнтн… Невтрлт амҗлтта болла! Невтрҗ чадсн уга! Ацаллт кеҗ экллә! @@ -83,7 +83,7 @@ Тиим Делгрңгү Нерн, төрл - Умшҗана... + Умшҗана… Алькинь чигн суңһад уга Тодрхаллт уга Күүндән уга diff --git a/app/src/main/res/values-xmf/strings.xml b/app/src/main/res/values-xmf/strings.xml index b32eeb005..7927da1af 100644 --- a/app/src/main/res/values-xmf/strings.xml +++ b/app/src/main/res/values-xmf/strings.xml @@ -61,7 +61,7 @@ ვიკიოწკარუე პარამეტრეფი ვიკიოწკარუეშა ეხარგუა - ეთმიხარგუ... + ეთმიხარგუ… მახვარებუშ ჯოხო პაროლი გენშართით თქვანი პროფილით Commons Beta-შა @@ -71,7 +71,7 @@ სისტემაშა მიშულა ქორთხინთ ქჷმიცადით … მუკნაჭარეფი დო ეჭარუეფი მითმიახალებუ - ქორთხინთ ქჷმიცადით... + ქორთხინთ ქჷმიცადით… სისტემაშა მიშულაქ წჷმოძინელო გეთუ! სისტემაშა მიშულაქ ვემიხუჯინუ! ფაილქ ვეგორუ. ქორთხინთ, ქოცადით შხვა ფაილი. @@ -181,7 +181,7 @@ ქოǃ უმოსი ინფორმაცია კატეგორიეფი - იხარგუ... + იხარგუ… მუთუნ ვა რე გიშაგორილი მუკნაჭარა ვა რე ვა რე ეჭარუა diff --git a/app/src/main/res/values-zgh/strings.xml b/app/src/main/res/values-zgh/strings.xml index 27080b999..c6f27bb99 100644 --- a/app/src/main/res/values-zgh/strings.xml +++ b/app/src/main/res/values-zgh/strings.xml @@ -13,7 +13,7 @@ ⵜⴻⵜⵜⵓⴷ ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵣⵔⴰⵢ? ⵣⵎⵎⴻⵎ ⴷⴰ ⵜⴽⵛⵛⵎⴷ - ⴰⵎⵓⵔ ⵏⵏⴽ ⵇⵇⵍ... + ⴰⵎⵓⵔ ⵏⵏⴽ ⵇⵇⵍ… ⴰⴽⵛⴰⵎ !ⵉⵎⵓⵔⵙ ⴰⴽⵛⴰⵎ ⵉⵣⴳⵍ! ⴰⴼⴰⵢⵍⵓ ⵓⵔ ⵉⵜⵜⵢⵓⴼⴰ. ⴰⵎⵓⵔ ⵏⵏⴽ ⴰⵔⵎ ⴰⴼⴰⵢⵍⵓ ⵢⴰⴹⵏ. diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 74ff641c0..2a307e955 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -79,28 +79,22 @@ 地点状态 今日图片 - %1$d个文件正在上传 %1$d个文件正在上传 - %1$d次上传 %1$d次上传 开始上传 - 正在处理%d个上传 正在处理%d个上传 - %d个上传 %d个上传 - 该图像的授权协议是 %1$s 这些图像的授权协议是 %1$s - %1$d次上传 %1$d次上传 @@ -552,7 +546,7 @@ 已拒绝访问媒体位置 我们可能无法自动从你上传的图片中获取位置数据。提交前请为每张图片添加适当的位置 直接在您手机上的维基共享资源应用中上传照片。立即下载共享资源应用:%1$s - 分享到... + 分享到… 图像信息 找不到分类 找不到描写。 @@ -578,7 +572,6 @@ 分类更新 成功 - 分类%1$s已添加。 分类%1$s已添加。 无法添加分类。 @@ -586,7 +579,6 @@ 正在尝试更新描述。 编辑描述 - 已添加 %1$s 个描写。 已添加 %1$s 个描写。 无法添加描述。 @@ -687,8 +679,8 @@ 限制连接模式 优良图片 品质图像是符合一定质量标准(本质上大多是技术性的)的图表或照片,它们对维基媒体计划很有价值 - 正在恢复上传... - 暂停上传... + 正在恢复上传… + 暂停上传… 正在取消上传… 取消上传 您已启用限制连接模式。所有的上传已暂停并将在您禁用此模式后立刻恢复。 @@ -816,7 +808,6 @@ 正在保存KML文件 正在保存GPX文件 - 已选择%d个图像 已选择%d个图像 请记住,每次多图片上传会为其中的所有图片标注相同的分类和描述。如果这些图片并不共享同样的描述和分类,请分别进行多次上传。 @@ -830,7 +821,7 @@ 其他问题或信息(请在下方解释)。 您的反馈已经发布在以下wiki页面:<a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> 您确定要取消所有上传吗? - 取消所有的上传... + 取消所有的上传… 上传 待处理 失败 From 197855af0e58bf89bbc1b49877639a77a6331145 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 28 Oct 2024 13:02:08 +0100 Subject: [PATCH 15/74] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ab/strings.xml | 4 +- app/src/main/res/values-af/strings.xml | 3 +- app/src/main/res/values-anp/strings.xml | 10 ++--- app/src/main/res/values-ar/strings.xml | 8 ++-- app/src/main/res/values-as/strings.xml | 4 +- app/src/main/res/values-ast/strings.xml | 4 +- app/src/main/res/values-az/strings.xml | 2 +- .../main/res/values-b+roa+tara/strings.xml | 6 +-- app/src/main/res/values-b+sr+Latn/strings.xml | 19 +++------ app/src/main/res/values-ba/strings.xml | 8 ++-- app/src/main/res/values-ban/strings.xml | 6 +-- app/src/main/res/values-bg/strings.xml | 2 +- app/src/main/res/values-blk/strings.xml | 4 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-br/strings.xml | 6 --- app/src/main/res/values-bs/strings.xml | 5 +-- app/src/main/res/values-ca/strings.xml | 8 +--- app/src/main/res/values-ce/strings.xml | 8 ++-- app/src/main/res/values-cs/strings.xml | 15 +------ app/src/main/res/values-csb/strings.xml | 6 +-- app/src/main/res/values-cy/strings.xml | 19 --------- app/src/main/res/values-da/strings.xml | 10 ++--- app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values-diq/strings.xml | 8 ++-- app/src/main/res/values-el/strings.xml | 8 ++-- app/src/main/res/values-eo/strings.xml | 16 ++++---- app/src/main/res/values-es/strings.xml | 38 +++++++----------- app/src/main/res/values-eu/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 10 ++--- app/src/main/res/values-fi/strings.xml | 10 ++--- app/src/main/res/values-fr/strings.xml | 39 ++++++++----------- app/src/main/res/values-gcr/strings.xml | 6 +-- app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 7 ++-- app/src/main/res/values-hr/strings.xml | 17 ++++---- app/src/main/res/values-hu/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 18 +++++---- app/src/main/res/values-io/strings.xml | 14 +++---- app/src/main/res/values-is/strings.xml | 8 ++-- app/src/main/res/values-it/strings.xml | 15 ++----- app/src/main/res/values-iw/strings.xml | 26 +++++++++---- app/src/main/res/values-ja/strings.xml | 5 ++- app/src/main/res/values-kab/strings.xml | 8 ++-- app/src/main/res/values-ko/strings.xml | 35 +++++++++++++++-- app/src/main/res/values-krc/strings.xml | 14 +++---- app/src/main/res/values-ku/strings.xml | 6 +-- app/src/main/res/values-kum/strings.xml | 2 +- app/src/main/res/values-kus/strings.xml | 18 ++++----- app/src/main/res/values-ky/strings.xml | 3 +- app/src/main/res/values-lb/strings.xml | 12 +++--- app/src/main/res/values-li/strings.xml | 8 ++-- app/src/main/res/values-lt/strings.xml | 21 ++++------ app/src/main/res/values-lv/strings.xml | 4 +- app/src/main/res/values-mk/strings.xml | 12 +++--- app/src/main/res/values-mni/strings.xml | 4 +- app/src/main/res/values-mnw/strings.xml | 4 +- app/src/main/res/values-mr/strings.xml | 3 +- app/src/main/res/values-my/strings.xml | 14 ++++--- app/src/main/res/values-nl/strings.xml | 9 +++-- app/src/main/res/values-nqo/strings.xml | 20 +++++----- app/src/main/res/values-oc/strings.xml | 2 +- app/src/main/res/values-pa/strings.xml | 13 ++++--- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pms/strings.xml | 9 +++-- app/src/main/res/values-ps/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 28 +++++-------- app/src/main/res/values-pt/strings.xml | 32 ++++++--------- app/src/main/res/values-ro/strings.xml | 10 ++--- app/src/main/res/values-ru/strings.xml | 14 +++---- app/src/main/res/values-sd/strings.xml | 4 +- app/src/main/res/values-se/strings.xml | 8 ++-- app/src/main/res/values-sh/strings.xml | 4 +- app/src/main/res/values-si/strings.xml | 11 +++--- app/src/main/res/values-sk/strings.xml | 14 +++---- app/src/main/res/values-sl/strings.xml | 30 +++++++------- app/src/main/res/values-sr/strings.xml | 19 +++------ app/src/main/res/values-su/strings.xml | 13 +++++-- app/src/main/res/values-sv/strings.xml | 4 +- app/src/main/res/values-ta/strings.xml | 4 +- app/src/main/res/values-tcy/strings.xml | 4 +- app/src/main/res/values-te/strings.xml | 12 +++--- app/src/main/res/values-th/strings.xml | 7 +++- app/src/main/res/values-tr/strings.xml | 6 +-- app/src/main/res/values-uk/strings.xml | 6 +-- app/src/main/res/values-uz/strings.xml | 8 ++-- app/src/main/res/values-vec/strings.xml | 10 ++--- app/src/main/res/values-xal/strings.xml | 8 ++-- app/src/main/res/values-xmf/strings.xml | 6 +-- app/src/main/res/values-zgh/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 20 ++++++++-- 90 files changed, 445 insertions(+), 480 deletions(-) diff --git a/app/src/main/res/values-ab/strings.xml b/app/src/main/res/values-ab/strings.xml index 22f382f57..9ff1b19b4 100644 --- a/app/src/main/res/values-ab/strings.xml +++ b/app/src/main/res/values-ab/strings.xml @@ -14,7 +14,7 @@ Аҭаларҭа Иҟаҵатәуп арегистрациа Асистемахь аҭаларҭа - Шәааԥшы ԥыҭрак… + Шәааԥшы ԥыҭрак... Аҭалара қәҿиарала имҩаԥысит! Асистемахь аҭалараан агха! Афаил ԥшаам. Даҽа фаилк шәахәаԥш. @@ -64,7 +64,7 @@ Ари шәара еилышәкаама? Ааи! Акатегориақәа - Аҭагалара… + Аҭагалара... Акагь алхӡам Иҟам ахҳәаа Идырым алицензиа diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 57ba77cc9..1da8b3101 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -22,6 +22,7 @@ %1$d lêers aan die uploaden + \@string/contributions_subtitle_zero (%1$d) (%1$d) @@ -147,7 +148,7 @@ Ja! <u>Meer inligting</u> Kategorieë - Laai … + Laai ... Niks gekies nie Geen beskrywing Geen bespreking nie diff --git a/app/src/main/res/values-anp/strings.xml b/app/src/main/res/values-anp/strings.xml index e4029af9b..70a01949f 100644 --- a/app/src/main/res/values-anp/strings.xml +++ b/app/src/main/res/values-anp/strings.xml @@ -27,14 +27,14 @@ पासवर्ड भूलाय गेलौ की? साइन अप करौ प्रवेश होय रहलौ छौं - कृपया प्रतीक्षा करौ… - कृपया प्रतीक्षा करौ… + कृपया प्रतीक्षा करौ... + कृपया प्रतीक्षा करौ... प्रवेश विफल अपलोड आरंभ! हाल केरौ अपलोड कतारबद्ध विफल - अपलोड होय रहलौ छौं… + अपलोड होय रहलौ छौं... ठामे मँ हमरौ अपलोड साझा करौ @@ -68,7 +68,7 @@ हाँव! बेसी जानकारी श्रेणी सिनी - लोड होय रहलौ छौं… + लोड होय रहलौ छौं... कुछु चयनित नाय कोय शीर्षक नाय कोय विवरण नाय @@ -173,7 +173,7 @@ पूर्ण होलौं अगलका छवि हाँव, केन्हअ नाय - कृपया प्रतीक्षा करौ… + कृपया प्रतीक्षा करौ... प्रतिलिपि बनैलौ गेलै! लेखक स्थान diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index b0fda6990..46ffcb7f5 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -125,7 +125,7 @@ يجري الدخول الرجاء الانتظار… تحديث التسميات التوضيحية والأوصاف - يرجى الانتظار… + يرجى الانتظار... نجاح تسجيل الدخول! فشل تسجيل الدخول الملف غير موجود. فضلا اختر ملفا آخر. @@ -530,7 +530,7 @@ عرض المقروءة عرض غير المقروءة حدث خطأ أثناء التقاط الصور - الرجاء الانتظار… + الرجاء الانتظار... الصور المختارة هي صور من مصورين ورسامين ذوي مهارات عالية اختارها مجتمع ويكيميديا ​​كومنز كبعض الأفضل جودة على الموقع. الصور المرفوعة عبر الأماكن القريبة هي الصور المرفوعة عن طريق اكتشاف الأماكن على الخريطة. تتيح هذه الميزة للمحررين إرسال إشعار شكر للمستخدمين الذين يقومون بتعديلات مفيدة - باستخدام رابط شكر صغير في صفحة التاريخ أو صفحة الفرق. @@ -552,7 +552,7 @@ رفض الوصول إلى موقع الوسائط قد لا نتمكن من الحصول تلقائيًا على بيانات الموقع من الصور التي تقوم برفعها. يرجى إضافة الموقع المناسب لكل صورة قبل الإرسال ارفع الصور لويكيميديا ​​كومنز مباشرة من هاتفك. قم بتنزيل تطبيق كومنز الآن: %1$s - مشاركة التطبيق عبر… + مشاركة التطبيق عبر... معلومات الصورة لم يتم العثور على تصنيفات لم يتم العثور على الصور @@ -695,7 +695,7 @@ وضع الاتصال المحدود صور عالية الجودة الصور عالية الجودة هي رسوم بيانية أو صور فوتوغرافية تفي بمعايير جودة معينة (والتي تكون في الغالب ذات طبيعة فنية) وذات قيمة لمشروعات ويكيميديا - جاري استئناف التحميل … + جاري استئناف التحميل ... جاري إيقاف التحميل مؤقتًا .. الغاء التحميل إلغاء الرفع diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml index 63aa12b96..960b55bda 100644 --- a/app/src/main/res/values-as/strings.xml +++ b/app/src/main/res/values-as/strings.xml @@ -27,7 +27,7 @@ পাছৱৰ্ড পাহৰিলে? পঞ্জীয়ন কৰক লগইন হৈ আছে - অনুগ্ৰহ কৰি অপেক্ষা কৰক… + অনুগ্ৰহ কৰি অপেক্ষা কৰক... লগইন সফল হ\'ল! লগইন বিফল হৈছে! ফাইল পোৱা নগ\'ল। অনুগ্ৰহ কৰি আন এটা ফাইল চেষ্টা কৰক। @@ -74,7 +74,7 @@ <u>গোপনিয়তা নীতি</u> প্ৰতিক্ৰিয়া প্ৰেৰণ কৰক (ইমেইল যোগে) কোনো ইমেইল ক্লায়েন্ট ইনষ্টল কৰা নাই - প্ৰথম চিংকৰ বাবে অপেক্ষাৰত… + প্ৰথম চিংকৰ বাবে অপেক্ষাৰত... আপুনি এতিয়ালৈকে কোনো ফটো আপল\'ড কৰা নাই। পুনৰ চেষ্টা কৰক বাতিল কৰক diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index 34212aebb..df61ed061 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -74,7 +74,7 @@ Aniciando sesión Espera… Actualizando pies y descripciones - Porfavor espera… + Porfavor espera... ¡Identificación correuta! ¡Falló l\'aniciu de sesión! Nun s\'alcontró\'l ficheru. Tenta con otru. @@ -480,7 +480,7 @@ Númberos de serie Software Xubi semeyes a Wikimedia Commons direutamente dende\'l to móvil. Descarga yá la app de Commons: %1$s - Compartir l\'aplicación per… + Compartir l\'aplicación per... Información de la imaxe Nun s\'alcontró nenguna categoría Nun s\'alcontraron retratos diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index d2ea468ad..1edbe43fc 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -104,7 +104,7 @@ CC BY 3.0 Əlavə məlumat Kateqoriyalar - Yüklənir… + Yüklənir... Heç biri seçilməmişdir Naməlum lisenziya Yenilə diff --git a/app/src/main/res/values-b+roa+tara/strings.xml b/app/src/main/res/values-b+roa+tara/strings.xml index 8e7764323..4fa660ef8 100644 --- a/app/src/main/res/values-b+roa+tara/strings.xml +++ b/app/src/main/res/values-b+roa+tara/strings.xml @@ -40,8 +40,8 @@ Tràse Passuord scurdate? Reggistrate - Stoche a tràse… - Aspitte… + Stoche a tràse... + Aspitte... E\' trasute! Non g\'è trasute! File non acchiate. Pruève \'n\'otre file. @@ -121,7 +121,7 @@ Permesse richieste Non ge tìne notifeche non lette Errore assute mendre ca ste pigghiave le immaggine - Aspitte… + Aspitte... Zumbe ste immaggine Autore Lènghe d\'a descrizione predefinite diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml index b8b602d0d..cd1cb09e8 100644 --- a/app/src/main/res/values-b+sr+Latn/strings.xml +++ b/app/src/main/res/values-b+sr+Latn/strings.xml @@ -5,7 +5,7 @@ * Milicevic01 * Zoranzoki21 --> - + Fejsbuk stranica Ostave Izvorni kod na Github-u Logo Ostave @@ -26,39 +26,32 @@ Slika dana %1$d datoteka se otprema - %1$d datoteke se otpremaju %1$d datoteke se otpremaju %1$d otpremanje - %1$d otpremanja %1$d otpremanja Pokretanje otpremanja Procesuiranje %d otpremanje - Procesuiranje %d otpremanja Procesuiranje %d otpremanja %d otpremanje - %d otpremanja %d otpremanja Slika će se voditi pod licencom %1$s - Slike će se voditi pod licencom %1$s Slike će se voditi pod licencom %1$s %1$d otpremanje - %1$d otpremanja %1$d otpremanja - Primanje deljenog sadržaja… Procesuiranje slike može potrajati neko vreme, u zavisnosti od veličine slike i vašeg uređaja - Primanje deljenog sadržaja… Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja - Primanje deljenog sadržaja… Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja + Primanje deljenog sadržaja... Procesuiranje slike može potrajati neko vreme, u zavisnosti od veličine slike i vašeg uređaja + Primanje deljenog sadržaja... Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja Istraga Izgled @@ -493,7 +486,7 @@ Pristup lokaciji medija je odbijen Možda nećemo moći da automatski pribavimo podatke o lokaciji iz slika koje otpremite. Dodajte odgovarajuću lokaciju za svaku sliku pre objavljivanja Otpremi fotografije na Vikimedijinu Ostavu direktno sa svog telefona. Preuzmi aplikaciju Ostave sada: %1$s - Podeli aplikaciju preko… + Podeli aplikaciju preko... Informacije o slici Nisu pronađene kategorije Otkazano otpremanje @@ -518,13 +511,12 @@ Uspešno Kategorija %1$s je dodata. - Kategorije %1$s su dodate. Kategorije %1$s su dodate. Nije moguće dodati kategorije. Ažuriraj kategoriju Uredi prikaze - Pokušavanje promena koordinata… + Pokušavanje promena koordinata... Ažuriranje koordinata Ažuriranje opisa Ažuriranje natpisa @@ -706,7 +698,6 @@ Nije moguće podeliti ovu stavku %d slika je odabrana - %d slika je odabrano %d slika je odabrano diff --git a/app/src/main/res/values-ba/strings.xml b/app/src/main/res/values-ba/strings.xml index 4c33b396f..0fc68329f 100644 --- a/app/src/main/res/values-ba/strings.xml +++ b/app/src/main/res/values-ba/strings.xml @@ -61,9 +61,9 @@ Серһүҙҙе оноттоғоҙмо? Теркәлеү Системаға инеү - Зинһар, көтөгөҙ… + Зинһар, көтөгөҙ... Аңлатмалар һәм тасуирламалар яңыртыла - Зинһар, көтөгөҙ… + Зинһар, көтөгөҙ... Системаға инеү уңышлы! Системаға инеү уңышһыҙ! Файл табылманы. Башҡа файлды эҙләп ҡарағыҙ. @@ -131,7 +131,7 @@ Фекереңде ебәр (эл.почта аша) Почта клиенты асыҡланмаған Яңыраҡ ҡулланылған категориялар - Тәүге синхронлаштырыуҙы көтөү… + Тәүге синхронлаштырыуҙы көтөү... Әлегә бер фото ла йөкләмәгәнһегеҙ Ҡабатларға Кире алыу @@ -171,7 +171,7 @@ Эйе! Ентеклерәк Категориялар - Йөкләнә башланы… + Йөкләнә башланы... Бер ни ҙә һайланмаған Тасуирламаһы юҡ Фекер алышыу юҡ diff --git a/app/src/main/res/values-ban/strings.xml b/app/src/main/res/values-ban/strings.xml index 1273eaf0d..b24bc0022 100644 --- a/app/src/main/res/values-ban/strings.xml +++ b/app/src/main/res/values-ban/strings.xml @@ -61,7 +61,7 @@ Lali kruna Sandi? Daftar Ngeranjingin log - Jantos dumun… + Jantos dumun... Nganyarin sesirah miwah pidarta Jantos dumun… Mahasil manjing log! @@ -303,7 +303,7 @@ Nomor seri Piranti lunak Unggah foto nuju Wikimédia Commons langsung saking télépon ragané. Unduh aplikasi Commons mangkin: %1$s - Wedar aplikasi saking… + Wedar aplikasi saking... Pidarta Gambar Pangunggahan Kawangdé %1$s kaunggah olih: %2$s @@ -340,7 +340,7 @@ Kaanggén Paringkat Titiang Kualitas Gambar - Ngalanturang unggahan… + Ngalanturang unggahan... Ngarérénang unggahan… Wangdé Unggah Lisénsi Média diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index cb19d6e39..6ee931542 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -304,7 +304,7 @@ Преглеждане на прочетени Преглеждане на непрочетени Възникна грешка при избирането на изображенията - Моля, изчакайте… + Моля, изчакайте... напълно размазано Наблизо Прочетете повече diff --git a/app/src/main/res/values-blk/strings.xml b/app/src/main/res/values-blk/strings.xml index 51a8ec1ba..2cf4aba5b 100644 --- a/app/src/main/res/values-blk/strings.xml +++ b/app/src/main/res/values-blk/strings.xml @@ -38,7 +38,7 @@ အွောန်ႏဖေင်ꩻထိုꩻ ငဝ်းဗိဉ်ႏပလို့ꩻနဲ့? ဒင်ႏမတ်ပိုင်တိဉ် အဝ်ႏနွို့အကောက်ကျာꩻ - အိုင်ပွေားဆောင်းတဆင်ႏသြ… + အိုင်ပွေားဆောင်းတဆင်ႏသြ... နွို့အကောက်အောင်ႏလဲဉ်း! နွို့အကောက်အောင်ႏတဝ်း! မော့ꩻတဝ်းဖုဲင်၊ စံꩻထွားစံꩻသွော့ ဖုဲင်အလင်တဗာႏသြ။ @@ -97,7 +97,7 @@ မွေး! ထဲင်းယင်း သꩻတင်ꩻအချက်လက် ကဏ္ဍဖုံႏ - အဝ်ႏဒင်ႏဝွန်ႏကျာꩻ… + အဝ်ႏဒင်ႏဝွန်ႏကျာꩻ... လွိုက်ခါꩻတဝ်းမုဲင်ꩻမုဲင်ꩻ ပုင်ႏလိတ်အဝ်ႏတဝ်း အွောန်ႏနယ်ချက်အဝ်ႏတဝ်း diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 51502c264..2d156c199 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -393,7 +393,7 @@ কোনও চিত্র ব্যবহৃত হয়নি পঠিতগুলি দেখান অপঠিতগুলি দেখান - অনুগ্রহ করে অপেক্ষা করুন… + অনুগ্রহ করে অপেক্ষা করুন... অনুলিপি করা হয়েছে এই চিত্র এড়িয়ে যান প্রণেতা diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 9537c45e6..1c7d09617 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -40,9 +40,6 @@ %1$d bellgargadenn loc\'het - %1$d bellgargadenn loc\'het - %1$d bellgargadennoù loc\'het - %1$d bellgargadennoù loc\'het %1$d pellgargadennoù loc\'het @@ -54,9 +51,6 @@ gant an aotre-implijout %1$s e vo ar skeudenn-mañ - gant an aotre-implijout %1$s e vo an div skeudenn-mañ - gant an aotre-implijout %1$s e vo meur a skeudenn-mañ - gant an aotre-implijout %1$s e vo kalz a skeudenn-mañ gant an aotreoù-implijout %1$s e vo ar skeudenn-mañ Ergerzhout diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index d178ff507..91860b1e1 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -10,22 +10,19 @@ Logo Commonsa postavlja se %1$d datoteka - postavlja se %1$d datoteke postavlja se %1$d datoteka + \@string/contributions_subtitle_zero postavljena %1$d datoteka - postavljena %1$d datoteke postavljenih datoteka: %1$d Započinjem postavljanje %1$d datoteke - Započinjem postavljanje %1$d datoteke Započinjem postavljanje %1$d datoteka/-e %1$d postavljanje - %1$d postavljanja %1$d postavljanja Slika će se voditi pod licencom %1$s diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 51330cb9d..0c15e58c3 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -20,33 +20,27 @@ Imatge del dia s\'està carregant %1$d fitxer - S\'estan carregant de %1$d fitxers s\'estan carregant %1$d fitxers (%1$d) - (%1$d) (%1$d) S\'inicien les càrregues S\'està processant %1$d càrrega - S\'estan processant %1$d càrregues S\'estan processant %1$d càrregues %d càrrega - $d càrregues %d càrregues Aquesta imatge quedarà sota llicència %1$s - Aquestes imatges quedaran sota llicència %1$s Aquestes imatges quedaran sota llicència %1$s %1$d pujada - %1$d pujades %1$d pujades Explora @@ -398,7 +392,7 @@ Model de lent Números de sèrie Programari - Comparteix l\'aplicació a través de… + Comparteix l\'aplicació a través de... Informació de la imatge No s’ha trobat cap categoria No s\'han trobat representacions diff --git a/app/src/main/res/values-ce/strings.xml b/app/src/main/res/values-ce/strings.xml index e25b83e25..e13e8c040 100644 --- a/app/src/main/res/values-ce/strings.xml +++ b/app/src/main/res/values-ce/strings.xml @@ -64,7 +64,7 @@ Викиларма Параметраш Викиларма чуйаккха - ДӀадоьдуш ду чуйаккхар… + ДӀадоьдуш ду чуйаккхар... Декъашхочун цӀе Пароль Commons Beta тӀехь хьай цӀарца чугӀо @@ -146,7 +146,7 @@ ЦӀе: Сиднейн операн театр ХӀаъ! Категореш - Чуйолуш… + Чуйолуш... ХӀума хаьржина йац Куьг доцуш Хаамаш бац @@ -297,7 +297,7 @@ Серийн лоьмар Программан кхачам Файл йолу меттиган тӀекхача бакъо ца ло - Йекъа программа, гӀоьнца… + Йекъа программа, гӀоьнца... Суьртан информаци Цхьа а категори ца карийна. Цхьа а хаам ца карийна. @@ -362,7 +362,7 @@ ДӀайаьккхина закладки йукъара Цхьа хӀума галдаьлла. Фонан сурт хӀотто аьтто ца баьлла Фонан сурт санна хӀоттайе - Фонан сурт дӀахӀоттош ду… + Фонан сурт дӀахӀоттош ду... Системин нисдаран гӀирс Бодане Сирла diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index fb4ee05ef..4d49ee632 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -32,44 +32,31 @@ Obrázek dne %1$d soubor se nahrává - %1$d soubory se nahrávají - %1$d souborů se nahrává %1$d souborů se nahrává + \@string/contributions_subtitle_zero (%1$d) - (%1$d) - (%1$d) (%1$d) Spouští se nahrávání %1$d souboru - Spouští se nahrávání %1$d souborů - Spouští se nahrávání %1$d souborů Spouští se nahrávání %1$d souborů %1$d nahrávání - %1$d nahrávání - %1$d nahrávání %1$d nahrávání Tento obrázek bude zveřejněn pod licencí %1$s - Tyto obrázky budou zveřejněny pod licencí %1$s - Tyto obrázky budou zveřejněny pod licencí %1$s Tyto obrázky budou zveřejněny pod licencí %1$s %1$d nahrání - %1$d nahrávání - %1$d nahrávání %1$d nahrání Probíhá příjem sdíleného obsahu. Zpracování obrázku může chvíli trvat v závislosti na velikosti obrázku a vašem zařízení - Probíhá příjem sdíleného obsahu. Zpracovávání obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení - Probíhá příjem sdíleného obsahu. Zpracovávání obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení Probíhá příjem sdíleného obsahu. Zpracování obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení Objevit diff --git a/app/src/main/res/values-csb/strings.xml b/app/src/main/res/values-csb/strings.xml index 7cdfb1382..623a48c8c 100644 --- a/app/src/main/res/values-csb/strings.xml +++ b/app/src/main/res/values-csb/strings.xml @@ -29,7 +29,7 @@ Wlogùjë mie Wregistrëjë sã Logòwanié - Proszã żdac… + Proszã żdac... Ùdałi logòwanié! Logòwanié nie darzëło sã! Felënk lopka. Proszã spróbòwac znowa. @@ -78,7 +78,7 @@ Sélôj òpinijã (przez e-mail) Felënk wjinstalowónegò e-mailowégò klienta Slédno ùżëwóne kategòrëje - Żdanié na pierszą synchronizacëjã… + Żdanié na pierszą synchronizacëjã... Nie môsz jesz wladowónych òdjimków Próbùjë znowa Òprzestóń @@ -99,7 +99,7 @@ Przëmiôr wladënka: Jo! Kategòrëje - Wladënk… + Wladënk... Felënk nacéchòwaniô Felënk òpisënka Nieznónô licencëja diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 50df9b6d8..8c4b4a652 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -21,44 +21,25 @@ Popeth Llun y Dydd - %1$d ffeil yn uwchlwytho %1$d ffeil yn uwchlwytho - %1$d ffeil yn uwchlwytho - %1$d ffeil yn uwchlwytho - %1$d ffeil yn uwchlwytho %1$d ffeil yn uwchlwytho \@string/contributions_subtitle_zero (%1$d) - (%1$d) - (%1$d) - (%1$d) (%1$d) Cychwyn Uwchlwytho - Dechrau %1$d uwchlwythiad Cychwyn %1$d uwchlwythiad - Dechrau %1$d uwchlwythiad - Dechrau %1$d uwchlwythiad - Dechrau %1$d uwchlwythiad Cychwyn uwchlwytho %1$d ffeil - %1$d uwchlwythiad %1$d uwchlwythiad - %1$d uwchlwythiad - %1$d uwchlwythiad - %1$d uwchlwythiad %1$d uwchlwythiad - Ni chaiff unrhyw ddelweddau eu trwyddedu dan %1$s Caiff y ddelwedd hon ei thrwyddedu yn ôl termau\'r drwydded %1$s - Caiff y delweddau hyn eu trwyddedu dan %1$s - Caiff y delweddau hyn eu trwyddedu dan %1$s - Caiff y delweddau hyn eu trwyddedu dan %1$s Caiff y delweddau hyn eu trwyddedu dan %1$s Archwilio diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 80a82afb5..3b6822c47 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -507,7 +507,7 @@ Adgang til medieplacering nægtet Vi kan muligvis ikke automatisk indhente placeringsdata fra billeder, du uploader. Tilføj den passende placering for hvert billede, før du indsender Upload billeder til Wikimedia Commons direkte fra din telefon. Download Commons-appen nu: %1$s - Del app via… + Del app via... Billedoplysninger Ingen kategorier blev fundet Ingen afbildninger fundet @@ -642,9 +642,9 @@ Begrænset forbindelsestilstand Kvalitetsbilleder Kvalitetsbilleder er tegninger eller fotografier, der opfylder visse kvalitetsstandarder (som for det meste er af teknisk karakter) og er værdifulde for Wikimedia-projekter - Genoptager upload… - Sætter upload på pause… - Annullerer upload… + Genoptager upload... + Sætter upload på pause... + Annullerer upload... Annuller upload Du har aktiveret begrænset forbindelsestilstand. Alle uploads er sat på pause og genoptages, når du deaktiverer denne tilstand. Begrænset forbindelsestilstand aktiveret! @@ -784,7 +784,7 @@ Andet problem eller anden information (forklar venligst nedenfor). Din feedback bliver slået op på følgende wiki-side: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Er du sikker på, at du vil annullere alle uploads? - Annullerer alle uploads… + Annullerer alle uploads... Uploads Afventer Mislykkedes diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 404304021..a6471c1fe 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -516,7 +516,7 @@ Ungelesene ansehen Beim Auswählen der Bilder ist ein Fehler aufgetreten Bitte warten … - Vorgestellte Bilder sind Bilder von professionellen Fotografen und Zeichnern, die Gemeinschaft von Wikimedia Commons als diejenigen mit der höchsten Qualität auf der Website ausgewählt hat. + Vorgestellte Bilder sind Bilder von professionellen Fotografen und Zeichnern, die die Gemeinschaft von Wikimedia Commons als diejenigen mit der höchsten Qualität auf der Website ausgewählt hat. Über Orte in der Nähe hochgeladene Bilder sind die Bilder, die von entdeckten Orten auf der Karte hochgeladen wurden. Diese Funktion erlaubt es Autoren, eine Dankeschön-Benachrichtigung an Benutzer zu senden, die nützliche Bearbeitungen durchgeführt haben – durch die Benutzung eines kleinen Dankeschön-Links in der Versionsgeschichte oder Unterschiedsseite. Auf Folgemedien kopieren @@ -611,7 +611,7 @@ zu den Lesezeichen hinzugefügt Etwas ist schiefgelaufen. Das Hintergrundbild konnte nicht eingestellt werden Als Hintergrundbild festlegen - Hintergrundbild wird festgelegt. Bitte warten… + Hintergrundbild wird festgelegt. Bitte warten... Systemeinstellung Dunkel Hell diff --git a/app/src/main/res/values-diq/strings.xml b/app/src/main/res/values-diq/strings.xml index ebb3cfbe4..840b0198d 100644 --- a/app/src/main/res/values-diq/strings.xml +++ b/app/src/main/res/values-diq/strings.xml @@ -62,8 +62,8 @@ Parola, xo vira kerde? Qeyd be Kewno cı - Kerem kerên, bıpawên… - Kerem ke, bıpawe… + Kerem kerên, bıpawên... + Kerem ke, bıpawe... Cıkewtış hewl bi. Nidekeweya de Dosya nêvineya. Dosyê da bine bıcerebnê. @@ -93,7 +93,7 @@ Şınasnayış Bınnuşte Xırabiya kewten-network xeta - Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2–3 deqey ra tepeya reyna bıcerrebnên. + Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2-3 deqey ra tepeya reyna bıcerrebnên. Qısur mewni rê, Karber commons dı bloqe biyo. Kodê kamiya raştkerdışi dıfaktorın gani cı kewê. Nidekeweya de @@ -298,7 +298,7 @@ Pêhesnayışê toyê wendışi çıniyê Wendışi bıvêne Nêwendeyan bıvêne - Kerem kerên, bıpawên… + Kerem kerên, bıpawên... Nê resımi raviyarnê Nuştekar Heqa telifi diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e4b597fb1..e3675ef0a 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -91,7 +91,7 @@ Σύνδεση Ξεχάσατε τον κωδικό πρόσβασης σας; Εγγραφή - Γίνεται σύνδεση… + Γίνεται σύνδεση... Παρακαλούμε αναμείνετε… Ενημέρωση λεζάντων και περιγραφών Παρακαλούμε αναμείνετε… @@ -204,7 +204,7 @@ Ναι! Περισσότερες πληροφορίες Κατηγορίες - Φόρτωση σε εξέλιξη… + Φόρτωση σε εξέλιξη... Καμία επιλεγμένη Χωρίς λεζάντα Χωρίς περιγραφή @@ -521,7 +521,7 @@ Δεν επιτρέπεται η πρόσβαση στην τοποθεσία πολυμέσων Ενδέχεται να μην μπορούμε να λάβουμε αυτόματα δεδομένα τοποθεσίας από φωτογραφίες που ανεβάζετε. Προσθέστε την κατάλληλη τοποθεσία για κάθε εικόνα πριν την υποβολή Ανεβάστε φωτογραφίες στα Wikimedia Commons απευθείας από το τηλέφωνό σας. Κάντε λήψη της εφαρμογής Commons τώρα: %1$s - Κοινή χρήση εφαρμογής μέσω… + Κοινή χρήση εφαρμογής μέσω... Πληροφορίες Εικόνας Δεν βρέθηκαν Κατηγορίες Δεν βρέθηκαν απεικονίσεις @@ -798,7 +798,7 @@ Άλλο πρόβλημα ή πληροφορίες (παρακαλούμε εξηγήστε παρακάτω). Τα σχόλιά σας δημοσιεύονται στην ακόλουθη σελίδα wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Εφαρμογή για κινητά/Σχόλια</a> Είστε βέβαιοι ότι θέλετε να ακυρώσετε όλες τις μεταφορτώσεις; - Ακύρωση όλων των μεταφορτώσεων… + Ακύρωση όλων των μεταφορτώσεων... Μεταφορτώσεις Σε εκκρεμότητα Απέτυχε diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 323c823b2..69673afbe 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -78,7 +78,7 @@ Ĉu pasvorto forgesita? Registriĝi Ensalutado - Bonvolu atendi… + Bonvolu atendi... Ĝisdatiganta subtekstojn kaj priskribojn Bonvolu atendi… Ensalutado sukcesis @@ -150,7 +150,7 @@ Sendi viajn komentojn (per retpoŝto) Neniu retpoŝtilo instalita Laste uzitaj kategorioj - Atendas la unuan Sinkronigado… + Atendas la unuan Sinkronigado... Vi ankoraŭ ne alŝutis fotojn. Reprovi Nuligi @@ -190,7 +190,7 @@ Jes! <u>Ekscii pli</u> Kategorioj - Ŝargado… + Ŝargado... Neniu elektita Neniu substeksto Sen priskribo @@ -482,7 +482,7 @@ Vidu legitajn Vidi nelegitojn Eraro okazis dum elektado de bildoj - Bonvolu atendi… + Bonvolu atendi... Elstaraj bildoj estas tiuj bildoj far tre spertaj fotografistoj kaj ilustristoj, kiujn la komunumo de Vikimedia Komunejo elektis kiel iujn de la plej alta kvalito en la retejo. Bildoj Alŝutitaj per Apudaj lokoj estas bildoj alŝutitaj per trovado de lokoj sur la mapo. Tiu funkcio ebligas sendi Dankantan sciigon al farinto de utila redakto – per malgranda dankiga ligilo ĉe la paĝo de historio aŭ diferenco. @@ -504,7 +504,7 @@ Aliro al loko de plurmediaĵo malakceptita Ni eble ne povos aŭtomate akiri pri-lokajn datumojn de bildoj, kiujn vi alŝutas. Bonvolu aldoni la taŭgan lokon por ĉiu bildo antaŭ ol sendi Alŝutu fotojn al Vikimedia Komunejo rekte de via telefono. Elŝutu la Komunejan aplikaĵon nun: %1$s - Diskonigi aplikaĵon per… + Diskonigi aplikaĵon per... Informo pri Bildo Neniu Kategorio troviĝis Neniu bildo-priskribo trovita @@ -636,9 +636,9 @@ Modo por limigita konekto Kvalitaj Bildoj Kvalitaj bildoj estas diagramoj aŭ fotoj kiuj kontentigas certajn normojn pri kvalito (kiuj estas plejparte teknikaj) kaj estas valoraj por Vikimediaj projektoj. - Rekomencante alŝuton… - Paŭzante alŝuton… - Nuligante alŝuton… + Rekomencante alŝuton... + Paŭzante alŝuton... + Nuligante alŝuton... Ĉesigi alŝutadon Vi aktivigis Modon por limigita konekto. Ĉiuj alŝutoj estas paŭzitaj kaj rekomencos post kiam vi malŝaltos ĉi modon. Modo por limigita konekto estas aktivigita. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 218953470..4e90f6864 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -51,7 +51,7 @@ * Vivaelcelta * Wizardeck --> - + Página de Facebook de Commons Código fuente de Commons en GitHub Logo de Commons @@ -75,38 +75,31 @@ Foto del día Cargando %1$d archivo - Cargando %1$d archivos Cargando %1$d archivos (%1$d) - (%1$d) (%1$d) Comenzando las subidas Procesando %d carga - Procesando %d cargas Procesando %d cargas %d carga - %1 cargas %1 cargas Esta imagen se publicará bajo la licencia %1$s - Estas imágenes se publicarán bajo la licencia %1$s Estas imágenes se publicarán bajo la licencia %1$s %1$d Subida - %1$d Subidas %1$d Subidas Recepción de contenido compartido. El procesamiento de la imagen puede tardar cierto tiempo, dependiendo del tamaño de la imagen y de tu dispositivo - Recepción de contenido compartido. El procesamiento de las imágenes puede tardar cierto tiempo, dependiendo del tamaño de las imágenes y de tu dispositivo Recepción de contenido compartido. El procesamiento de las imágenes puede tardar cierto tiempo, dependiendo del tamaño de las imágenes y de tu dispositivo Explorar @@ -342,7 +335,7 @@ Omitir tutorial Internet no disponible Error al recuperar las notificaciones - Hubo un error al recuperar la imágen a revisar. Toca refrescar para intentarlo de nuevo. + Hubo un error al recuperar la imágen a revisar. Toca refrescar para intentarlo de nuevo. No se encontró ninguna notificación Traducir Idiomas @@ -484,7 +477,7 @@ Permitir Descartar Por favor, activa el acceso a la ubicación desde Configuración y vuelva a intentarlo. \n\nNota: Es posible que la subida no tenga datos de la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. - La cámara dentro de la aplicación necesita el permiso a la ubicación para adjuntarla a sus imágenes en caso de que la ubicación no esté disponible en EXIF. Por favor, permita que la aplicación acceda a su ubicación e inténtelo de nuevo.\n\nNota: Es posible que la subida no tenga los datos de la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. + La cámara dentro de la aplicación necesita el permiso a la ubicación para adjuntarla a sus imágenes en caso de que la ubicación no esté disponible en EXIF. Por favor, permita que la aplicación acceda a su ubicación e inténtelo de nuevo.\n\nNota: Es posible que la subida no tenga los datos de la la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. La aplicación no registrará la ubicación junto con las tomas debido a la falta del permiso de la ubicación. La aplicación no registrará la ubicación junto con las tomas porque el GPS está apagado Utilizar el selector de fotografías basado en documentos @@ -512,8 +505,8 @@ ¿Está correctamente categorizado? ¿Está dentro de los objetivos del proyecto? ¿Quieres agradecer al colaborador? - Toca en NO para nominar esta imágen para ser borrada si no es para nada útil. - Los logotipos, las capturas de pantalla y los pósteres de películas son habitualmente infracciones a los derechos de autor.\n Toca NO para nominar esta imágen para borrado + Toca en NO para nominar esta imágen para ser borrada si no es para nada útil. + Los logotipos, las capturas de pantalla y los pósteres de películas son habitualmente infracciones a los derechos de autor.\n Toca NO para nominar esta imágen para borrado Tu apreciación animara a %1$s ¡Oh, esto ni siquiera esta categorizado! Esta imagen esta dentro de %1$s categorías. @@ -531,7 +524,7 @@ Compartir registros usando Ver leídas Ver no leidas - Ocurrió un error mientras se elegían imágenes + Ocurrió un error mientras se elegían imagenes Un momento… Las imágenes destacadas son creaciones de talentosos fotógrafos e ilustradores que la comunidad de Wikimedia Commons ha reconocido como las de mayor calidad del sitio. Las imágenes subidas vía Lugares Cercanos son las imágenes que han sido subidas al descubrir lugares en el mapa. @@ -554,7 +547,7 @@ Acceso a la ubicación del archivo multimedia denegado Es posible que no podamos obtener automáticamente los datos de ubicación de las imágenes que suba. Añada la ubicación adecuada a cada imagen antes de enviarla Sube fotos a Wikimedia Commons directamente desde tu celular. Descarga la aplicación de Commons ahora: %1$s - Compartir la aplicación vía… + Compartir la aplicación vía... Información de la imagen No se encontró ninguna categoría No se encontraron representaciones @@ -581,7 +574,6 @@ Éxito Se añade %1$s categoría. - Se añaden %1$s categorías. Se añaden %1$s categorías. No se pudieron añadir las categorías. @@ -590,7 +582,6 @@ Editar las descripciones %1$s Se añade la descripción. - Descripción %1$s se añadieron. Descripción %1$s se añadieron. No se pueden añadir descripciones. @@ -608,7 +599,7 @@ Las coordenadas de la imagen no están actualizadas. No se puede obtener descripciones. Editar descripciones y leyendas - Compartir imagen via + Compartir imagen via Todavía no has hecho ninguna contribución. %s Aún no ha realizado ninguna contribución Cuenta creada @@ -633,7 +624,7 @@ añadido a marcadores Algo salió mal. No se pudo establecer el fondo de pantalla Colocar como fondo de pantalla - Estableciendo el fondo de pantalla. Por favor espere… + Estableciendo el fondo de pantalla. Por favor espere... Seguir sistema Oscuro Claro @@ -691,9 +682,9 @@ Modo de conexión limitada Imágenes de calidad Las imágenes de calidad son diagramas o fotografías que cumplen determinados estándares de calidad (mayormente de carácter técnico) y que son valiosas para proyectos de Wikimedia - Reanudando carga… - Pausando carga… - Cancelando carga… + Reanudando carga... + Pausando carga... + Cancelando carga... Cancelar carga Has habilitado el modo de conexión limitada. Todas las cargas están pausadas y se reanudarán cuando deshabilites este modo. El modo de conexión limitada está encendido. @@ -820,8 +811,7 @@ Guardar archivo GPX %d imagen seleccionada - %d imágenes seleccionadas - %d imágenes seleccionadas + %d imagenes seleccionadas Recuerde que todas las imágenes en una carga múltiple tienen la misma categoría y representación. Si las imágenes no comparten representación y categoría, haga varias cargas por separado. Nota sobre cargas múltiples @@ -829,7 +819,7 @@ Por favor, escriba algunos comentarios. Discusión Escriba algo sobre el elemento \'%1$s\'. Será visible públicamente. - Cancelando todas las subidas… + Cancelando todas las subidas... Subidas Pendiente Falló diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 3dd463b34..ff75cbc7f 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -154,7 +154,7 @@ Mesedez, igo bakarrik zuk ateratako edo sortutako irudiak: Naturako elementuak (loreak, animaliak, mendiak) Objektu erabilgarriak (bizikletak, tren geltokiak) - Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat…) + Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat...) Mesedez EZ igo: Autorretratuak edo zure lagunen argazkiak Internetetik jaitsitako irudiak diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 4cdd2b87a..841160581 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -83,7 +83,7 @@ رمز عبور خودتان را فراموش کرده‌اید؟ ثبت نام واردشدن - شکیبا باشید… + شکیبا باشید... ورود موفق! ورود ناموفق! پرونده یافت نشد لطفاً پرونده دیگری را امتحان کنید. @@ -122,7 +122,7 @@ تغییرها بارگذاری جستجوی رده‌ها - جستجو برای موضوعی که در پروندهٔ شما به‌نمایش کشیده‌شده است (مثلا کوه، تاج محل، …) + جستجو برای موضوعی که در پروندهٔ شما به‌نمایش کشیده‌شده است (مثلا کوه، تاج محل، ...) ذخیره تازه کردن فهرست @@ -411,7 +411,7 @@ شما هیچ اعلان خوانده‌شده‌ای ندارید نمایش دیده‌شده مشاهده خوانده نشده ها - لطفاً صبر کنید… + لطفاً صبر کنید... نمونه تصاویری که برای بازگذاری مناسب نیستند از این تصویر صرف نظر کن مدیریت تگ‌های EXIF @@ -423,7 +423,7 @@ مدل لنز شماره سریال نرم‌افزار - اشتراک از طریق… + اشتراک از طریق... اطلاعات عکس هیچ رده‌ای یافت نشد بارگذاری لغو شد @@ -455,7 +455,7 @@ به بوکمارک‌ها افزوده شد مشکل به وجود آمد. به عنوان پس‌زمینه انتخاب نشد. انتخاب به عنوان پس‌زمینه - قرار دادن پس‌زمینه. لطفاً صبر کنید… + قرار دادن پس‌زمینه. لطفاً صبر کنید... سامانه را دنبال کنید تیره روشن diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 26328a3e2..312ebc84c 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -80,7 +80,7 @@ Kirjaudutaan Odota… Päivitetään kuvatekstejä ja kuvauksia - Odota… + Odota... Kirjautuminen onnistui! Kirjautuminen epäonnistui! Tiedostoa ei löytynyt. Yritä toista tiedostoa. @@ -481,7 +481,7 @@ Sarjanumerot Ohjelmisto Lähetä valokuvia suoraan Wikimedia Commonsiin puhelimestasi. Lataa Commons-appi nyt: %1$s - Jaa sovellus… + Jaa sovellus... Kuvan tiedot Luokkia ei löytynyt Kuvauksia ei löytynyt @@ -546,7 +546,7 @@ Lisätty kirjanmerkkeihin Jotain meni väärin. Ei voitu asettaa taustakuvaksi. Aseta taustakuvaksi - Asetetaan taustakuvaksi. Odota… + Asetetaan taustakuvaksi. Odota... Käytä järjestelmän Tumma Vaalea @@ -594,8 +594,8 @@ Rajoitettu yhteistila pois päältä. Jonossa olevat lähetykset kopioidaan nyt. Rajoitettu yhteystila Laatukuvat - Jatketaan lähettämistä… - Keskeytetään lähetys… + Jatketaan lähettämistä... + Keskeytetään lähetys... Peruutetaan tallennusta… Peruuta tallennus Rajoitettu yhteystila on päällä. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 995e4041b..ae4dfb966 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -46,7 +46,7 @@ * Wladek92 * Y-M D --> - + Page Facebook de Commons Code source Github de Commons Logo de Commons @@ -70,38 +70,31 @@ Image du jour %1$d fichier en cours de téléversement - %1$d fichiers en cours de téléversement %1$d fichiers en cours de téléversement (%1$d) - (%1$d) (%1$d) Démarrage des téléversements %d téléversement en cours - %d téléversements en cours %d téléversements en cours %d téléversement - %d téléversements %d téléversements Cette image sera sous licence %1$s. - Ces images seront sous licence %1$s. Ces images seront sous licence %1$s. %1$d téléversement - %1$d téléversements %1$d téléversements - 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. + 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. Explorer @@ -120,9 +113,9 @@ Mot de passe oublié ? S’inscrire Connexion - Veuillez patienter… + Veuillez patienter... Mise à jour des légendes et des descriptions - Veuillez patienter… + Veuillez patienter... Connexion réussie ! Échec de la connexion ! Fichier non trouvé. Veuillez en essayer un autre. @@ -192,7 +185,7 @@ Envoyer vos commentaires (par courriel) Aucun client de courriel installé Catégories récemment utilisées - En attente de première synchronisation… + En attente de première synchronisation... Vous n’avez encore téléchargé aucune photo. Réessayer Annuler @@ -232,7 +225,7 @@ Oui ! Davantage d’informations Catégories - Chargement en cours… + Chargement en cours... Aucune catégorie sélectionnée Aucune légende Aucune description @@ -528,7 +521,7 @@ Afficher les lus Afficher les non lus Une erreur est survenue lors de la sélection des images - Veuillez patienter… + Veuillez patienter... Les images en vedette sont des images de photographes et d’illustrateurs très doués que la communauté de Wikimédia Commons a choisies comme étant de la meilleure qualité pour le site. Les images téléversées par « Lieux à proximité » sont les images téléversées lors de la découverte de lieux sur la carte. Cette fonctionnalité 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. @@ -550,7 +543,7 @@ Accès à l’emplacement du média refusé Nous ne pourrons pas obtenir automatiquement les données de localisation des images que vous téléchargez. Veuillez ajouter l’emplacement approprié pour chaque image avant de la soumettre. Téléversez des photos sur Wikimedia Commons directement depuis votre téléphone. Téléchargez l’application Commons maintenant : %1$s - Partager l’application via… + Partager l’application via... Informations sur l’image Aucune catégorie trouvée Aucun élément représenté trouvé @@ -577,7 +570,6 @@ Succès La catégorie %1$s est ajoutée. - Les catégories %1$s sont ajoutées. Les catégories %1$s sont ajoutées. Impossible d’ajouter des catégories. @@ -586,7 +578,6 @@ Modifier les éléments représentés L’élément représenté %1$s est ajouté. - Les éléments représentés %1$s sont ajoutés. Les éléments représentés %1$s sont ajoutés. Impossible d’ajouter des éléments représentés. @@ -629,7 +620,7 @@ Ajouté aux favoris Un problème est survenu. Impossible d’installer le fond d’écran. Définir comme fond d’écran - Installation du fond d’écran. Veuillez patienter… + Installation du fond d’écran. Veuillez patienter... Suivre le système Sombre Clair @@ -687,9 +678,9 @@ Mode de connexion limitée Images de qualité Les images de qualité sont des diagrammes ou des photographies qui respectent certains standards de qualité (qui sont, par nature, essentiellement techniques) et sont précieuses pour les projets Wikimedia. - Reprise du téléversement… - Mise en pause du téléversement… - Annulation du téléversement… + Reprise du téléversement... + Mise en pause du téléversement... + Annulation du téléversement... Annuler le téléversement Vous avez activé le mode de connexion limitée. Tous les téléversements sont suspendus et reprendront une fois ce mode désactivé. Le mode de connexion limitée est actif. @@ -818,7 +809,6 @@ Fichier GPX enregistré %d image sélectionnée - %d images sélectionnées %d images sélectionnées Souvenez-vous que toutes les images dans une importation multiple prennent les mêmes catégories et descriptions. Si les images de partagent pas les descriptions et catégories, veuillez effectuer plusieurs importations séparées. @@ -832,9 +822,12 @@ Autre problème ou information (merci d\'expliquer ci-dessous). Vos commentaires sont publiés sur la page wiki suivante : <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Êtes-vous sûr de vouloir annuler tous les téléchargements ? - Annulation de tous les téléchargements… + Annulation de tous les téléchargements... Téléversements En attente Échec Les données du lieu n\'ont pas pu être chargées + Cet endroit n\'a pas encore de photo, allez en prendre une ! + Cet endroit a déjà une photo. + Je vérifie maintenant si cet endroit a une photo. diff --git a/app/src/main/res/values-gcr/strings.xml b/app/src/main/res/values-gcr/strings.xml index 4659eecf1..b0ec66423 100644 --- a/app/src/main/res/values-gcr/strings.xml +++ b/app/src/main/res/values-gcr/strings.xml @@ -38,9 +38,9 @@ Ou bliyé ou Kodsigré ? Enskri oukò Konnègsyon - Souplé antann… + Souplé antann... Mizajou di léjann-yan ké dèskripsyon-yan - Souplé antann… + Souplé antann... Konnègsyon bon ! Konnègsyon pabon ! Fiché pa trouvé. Souplé éséyé ké rounòt. @@ -96,7 +96,7 @@ Enren ! Plis lenfòrmasyon Katégori-ya - Chajman ka fèt… + Chajman ka fèt... Pyès katégori sélègsyonnen Pyès léjann Pyès dèskripsyon diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index e11716a51..1740c1890 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -452,7 +452,7 @@ Modelo de lente Números de serie Software - Compartir a aplicación vía… + Compartir a aplicación vía... Información da imaxe Non se atoparon categorías Cancelouse a carga diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 237583853..50a04319b 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -39,6 +39,7 @@ %1$d फ़ाइलें अपलोड हो रहीं + \@string/contributions_subtitle_zero (%1$d) (%1$d) @@ -69,8 +70,8 @@ पासवर्ड भूल गये? खाता बनायें लॉग इन हो रहा है - कृपया प्रतीक्षा करें… - कृपया प्रतीक्षा करें… + कृपया प्रतीक्षा करें... + कृपया प्रतीक्षा करें... लॉग इन सफल! लॉग इन विफल! फ़ाइल नहीं मिली, कृपया अन्य फ़ाइल से प्रयास करें। @@ -349,7 +350,7 @@ रद्द करें वार्ता क्या आप वाकई सभी अपलोड रद्द करना चाहते हैं? - सभी अपलोड रद्द किये जा रहे हैं… + सभी अपलोड रद्द किये जा रहे हैं... अपलोड लंबित विफल हुआ diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 414f0dd40..d2d731c39 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -15,22 +15,19 @@ Slika dana Postavlja se %1$d datoteka - Postavlja se %1$d datoteke Postavljaju se %1$d datoteke + \@string/contributions_subtitle_zero %1$d postavljena datoteka - %1$d postavljena datoteke %1$d postavljene datoteke Započeto %1$d postavljanje - Započinjem %1$d postavljanja Započeta %1$d postavljanja %1$d postavljanje - %1$d postavljanja %1$d postavljanja Ova će slika biti licencirana pod %1$s @@ -49,7 +46,7 @@ Zaboravljena zaporka? Otvori račun Prijava - Molimo pričekajte … + Molimo pričekajte ... Prijava uspješna! Prijava neuspješna! Datoteka nije pronađena. Molimo probajte drugu. @@ -107,7 +104,7 @@ Pošaljite povratnu informaciju (putem elektroničke pošte) Klijent za elektroničku poštu nije instaliran Nedavno rabljene kategorije - Pričekajte za prvu sinkronizaciju… + Pričekajte za prvu sinkronizaciju... Nemate još postavljenih slika. Pokušaj ponovo Odustani @@ -147,7 +144,7 @@ Da! Više informacija Kategorije - Učitavanje… + Učitavanje... Ništa nije odabrano Nema opisa Nepoznata licencija @@ -196,7 +193,7 @@ Stranica datoteke na Zajedničkom poslužitelju Stavka na Wikidati Članak na Wikipediji - Opišite medij što je više moguće: gdje je napravljen, što prikazuje,… Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. + Opišite medij što je više moguće: gdje je napravljen, što prikazuje,... Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. Mogući problemi s ovom slikom: Slika je pretamna. Slika je mutna. @@ -284,7 +281,7 @@ Promijenio/la sam mišljenje, ne želim da više bude javno vidljivo Toliko ste pridonijeli projektu da se naš sustav za računanje postignuća ne može nositi s time. To je vrhunsko postignuće. Došlo je do pogrješke tijekom obradbe slike. Molimo Vas, pokušajte ponovo! - Molimo Vas, pričekajte … + Molimo Vas, pričekajte ... Preskoči ovu sliku Zadani jezik za opis Pokušavanje ažuriranja kategorija. @@ -296,7 +293,7 @@ Dodano u oznake Nešto je pošlo po zlu. Ne možemo postaviti pozadinu Postavi kao pozadinu - Postavljanje pozadine. Molimo, pričekajte… + Postavljanje pozadine. Molimo, pričekajte... Zadano Tamno Svijetlo diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index eb3438674..aefc17d9d 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -441,7 +441,7 @@ Sorozatszámok Szoftver Képek feltöltése Wikimedia Commons-ba közvetlenül a telefonodról. Töltsd le a Commons applikációt most: %1$s - Alkalmazás megosztása ezzel… + Alkalmazás megosztása ezzel... Képinformáció Nem található kategória Megszakított feltöltés @@ -474,7 +474,7 @@ Híd, múzeum, szálloda, stb. A belépés nem sikerült, kérj új jelszót. Beállítás háttérképnek - Beállítás háttérképnek. Kérem várjon… + Beállítás háttérképnek. Kérem várjon... Rendszerbeállítás követése Sötét Világos diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 8fff554e3..219fa4521 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -61,6 +61,7 @@ %1$d Unggahan + Sedang menerima konten yang dibagikan. Memproses gambar mungkin memerlukan waktu lebih lama tergantung pada ukuran gambar dan perangkat Anda Sedang menerima konten yang dibagikan. Memproses gambar mungkin memerlukan waktu lebih lama tergantung pada ukuran gambar dan perangkat Anda Jelajahi @@ -81,7 +82,7 @@ Memasuki log Silakan tunggu… Memperbarui takarir dan deskripsi - Mohon tunggu… + Mohon tunggu... Berhasil masuk log! Gagal masuk log! Berkas tidak ditemukan. Silakan coba berkas lain. @@ -190,7 +191,7 @@ Ya! Informasi selengkapnya Kategori - Memuat… + Memuat... Tidak ada yang dipilih Tanpa takarir Tidak ada keterangan @@ -496,7 +497,7 @@ Akses lokasi media ditolak Kami mungkin tidak dapat memperoleh data lokasi secara otomatis dari gambar yang Anda unggah. Harap tambahkan lokasi yang sesuai untuk setiap gambar sebelum mengirimkannya Mengunggah foto ke Wikimedia Commons secara langsung dari telepon Anda. Unduh aplikasi Commons sekarang: %1$s - Bagikan aplikasi lewat… + Bagikan aplikasi lewat... Info Gambar Kategori tidak ditemukan Penggambaran tidak ditemukan @@ -522,6 +523,7 @@ Pembaruan kategori Berhasil + Kategori %1$s ditambahkan. Kategori %1$s ditambahkan. Tidak bisa menambahkan kategori. @@ -567,7 +569,7 @@ Ditambahkan ke pembatas Terjadi kesalahan. Tidak bisa menetapkan wallpaper Jadikan Wallpaper - Sedang menetapkan Wallpaper. Tolong tunggu… + Sedang menetapkan Wallpaper. Tolong tunggu... Ikuti sistem Gelap Terang @@ -623,9 +625,9 @@ Mode Koneksi Terbatas Gambar Berkualitas Gambar berkualitas adalah diagram atau foto yang memenuhi standar kualitas tertentu (yang sifatnya teknis) dan berharga bagi proyek Wikimedia - Melanjutkan unggahan… - Menunda unggahan… - Membatalkan pengunggahan… + Melanjutkan unggahan... + Menunda unggahan... + Membatalkan pengunggahan... Batalkan pengunggahan Anda menyalakan mode koneksi terbatas. Semua pengunggahan ditunda dan akan dilanjutkan begitu Anda mematikan mode ini. Mode sambungan terbatas sedang menyala. @@ -741,7 +743,7 @@ %d gambar dipilih Bicara - Membatalkan semua unggahan… + Membatalkan semua unggahan... Unggahan Menunggu Gagal diff --git a/app/src/main/res/values-io/strings.xml b/app/src/main/res/values-io/strings.xml index 994b1c3d3..51fe16441 100644 --- a/app/src/main/res/values-io/strings.xml +++ b/app/src/main/res/values-io/strings.xml @@ -70,9 +70,9 @@ Ka tu obliviis tua pasovorto? Enirar Eniranta - Voluntez vartar… + Voluntez vartar... Aktualiganta etiketi e deskripturi - Voluntez vartar… + Voluntez vartar... Eniro sucesoza! Eniro faliis! Arkivo ne trovita. Voluntez probar altr arkivo. @@ -142,7 +142,7 @@ Sendez komenti (per e-posto) Nula kliento di e-posto instalesis Kategorii recente uzita - Vartanta unesma sinkronigo… + Vartanta unesma sinkronigo... Vu ankore ne sendis fotografuri. Riprobar Nuligar @@ -180,7 +180,7 @@ Yes! Plusa informo Kategorii - Karganta… + Karganta... Nulo selektesis Nula deskripto-texto Nula deskripto @@ -410,7 +410,7 @@ Vu ne lektis irga avizo Vidar lektita Vidar ne-lektata - Vartez… + Vartez... Kopiita Exempli pri bona imaji por sendar a Commons Saltez ca imajo @@ -472,7 +472,7 @@ Ajusti Adjuntita marko-rubandi Uzar kom skreno-kovrilo - Kreanta skreno-kovrilo. Voluntez vartar… + Kreanta skreno-kovrilo. Voluntez vartar... Koloro obskura Koloro klara Charjez pluse @@ -500,7 +500,7 @@ Uzita Mea rango Imaji di qualeso - Nuliganta sendajo… + Nuliganta sendajo... Cesar kargajo Lektez pluse En omna idiomi diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index ac64fbf2c..417652953 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -3,7 +3,7 @@ * Sveinki * Sveinn í Felli --> - + Commons Facebook-síðan Grunnkóði Commons á Github Táknmerki Commons @@ -51,7 +51,7 @@ %1$d innsendingar - Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndarinnar og gerð tækisins þíns + Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndarinnar og gerð tækisins þíns Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndaanna og gerð tækisins þíns Uppgötva @@ -138,7 +138,7 @@ Senda umsögn (með tölvupósti) Ekkert tölvupóstforrit er uppsett Nýlega notaðir flokkar - Bíð eftir fyrstu samstillingu… + Bíð eftir fyrstu samstillingu... Þú ert ekki ennþá búin(n) að senda inn neinar myndir. Reyna aftur Hætta við @@ -477,7 +477,7 @@ Hugbúnaður Aðgangi að staðsetningu gagnamiðla hafnað Sendu myndir inn á Wikimedia Commons beint úr símanum þínum. Sæktu Commons-appið núna: %1$s - Deila forriti með… + Deila forriti með... Upplýsingar í mynd Engir flokkar fundust Engar myndlýsingar fundust diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e9aa8934e..f40863870 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -46,38 +46,31 @@ Foto del giorno %1$d file in caricamento - %1$d file in caricamento %1$d file in caricamento (%1$d) - (%1$d) (%1$d) Avvio del caricamento Elaborando %d caricamento - Elaborando %d caricamenti Elaborando %d caricamenti %d caricamento - %d caricamenti %d caricamenti Questa immagine sarà rilasciata in base alla licenza %1$s - Queste immagini saranno rilasciate in base alla licenza %1$s Queste immagini saranno rilasciate in base alla licenza %1$s %1$d caricamento - %1$d caricamenti %1$d caricamenti Ricezione di contenuti condivisi. L\'elaborazione dell\'immagine potrebbe richiedere del tempo a seconda delle dimensioni dell\'immagine e del dispositivo - Ricezione di contenuti condivisi. L\'elaborazione delle immagini potrebbe richiedere del tempo a seconda delle dimensioni delle immagini e del dispositivo Ricezione di contenuti condivisi. L\'elaborazione delle immagini potrebbe richiedere del tempo a seconda delle dimensioni delle immagini e del dispositivo Esplora @@ -523,7 +516,7 @@ Accesso alla posizione multimediale negato Potremmo non essere in grado di ottenere automaticamente i dati sulla posizione dalle immagini caricate. Si prega di aggiungere la posizione appropriata per ciascuna immagine prima di inviarla Carica foto su Wikimedia Commons direttamente dal tuo telefono. Scarica subito l\'app Commons: %1$s - Condividi applicazione tramite… + Condividi applicazione tramite... Informazioni sull\'immagine Nessuna categoria trovata Nessuna definizione trovata @@ -550,7 +543,6 @@ Successo Categoria %1$s aggiunta. - Categorie %1$s aggiunte. Categorie %1$s aggiunte. Non è stato possibile aggiungere le categorie. @@ -583,7 +575,7 @@ Esiste Necessita della fotografia Tipo di luogo: - Ponte, museo, albergo, ecc… + Ponte, museo, albergo, ecc... Si è verificato un errore durante l\'accesso. Devi reimpostare la password! MEDIA CLASSI FIGLIE @@ -596,7 +588,7 @@ Aggiungi ai preferiti Qualcosa è andato storto. Non è stato possibile impostare lo sfondo schermo Imposta come sfondo - Impostazione di sfondo in corso… + Impostazione di sfondo in corso... Segui sistema Scuro Chiaro @@ -766,7 +758,6 @@ Sessione scaduta. Accedi nuovamente. %d immagine selezionata - %d immagini selezionate %d immagini selezionate Questo posto non ha ancora una foto, scattane una! diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 4b8c51f6c..0b512102b 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -45,37 +45,44 @@ מועלה קובץ אחד מועלים %1$d קבצים + מועלים %1$d קבצים מועלים %1$d קבצים (%1$d) (%1$d) + (%1$d) (%1$d) ההעלאות מתחילות עיבוד העלאה עיבוד d% העלאות + עיבוד d% העלאות עיבוד d% העלאות העלאה אחת %d העלאות + %d העלאות %d העלאות התמונה הזאת תפורסם ברישיון %1$s התמונות האלה תפורסמנה ברישיון %1$s + התמונות האלה תפורסמנה ברישיון %1$s התמונות האלה תפורסמנה ברישיון %1$s העלאה אחת %1$d העלאות + %1$d העלאות %1$d העלאות מתקבל תוכן שיתופי. עיבוד התמונה עשוי לארוך זמן מה כתלות בגודל התמונה והמכשיר שלך מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך + מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך לחקור @@ -94,9 +101,9 @@ שכחת את הסיסמה? רישום כניסה לחשבון - נא להמתין… + נא להמתין... עדכון כיתובים ותיאורים - נא להמתין… + נא להמתין... הכניסה הצליחה! הכניסה נכשלה! הקובץ לא נמצא. נא לנסות קובץ אחר. @@ -206,7 +213,7 @@ כן! מידע נוסף קטגוריות - בטעינה… + בטעינה... לא נבחר דבר אין כיתוב אין תיאור @@ -501,7 +508,7 @@ הצגת התראות שנקראו הצגת התראות שלא נקראו אירעה שגיאה בעת בחירת תמונות - נא להמתין… + נא להמתין... תמונות מובילות הן תמונות של צלמים ומאיירים מיומנים אותם בחרה קהילת ויקישיתוף בזכות איכות התוצר שהם תורמים לאתר. תמונות שהועלו דרך מקומות בסביבה הן התמונות שנשלחות על ידי גילוי מקומות במפה. תכונה זו מאפשרת לעורכים לשלוח מסרי תודה למשתמשים שביצעו עריכות מועילות - על ידי שימוש בקישור תודה בדף ההיסטוריה או בדף ההבדלים. @@ -523,7 +530,7 @@ הגישה למקום המדיה נדחתה ייתכן שלא נוכל לאתר את נתוני המקום מתמונות שהעלית. נא להוסיף את המקום המתאים לכל תמונה בטרם הגשתה כדי להעלות תמונות לוויקינתונים של ויקימדיה ישר מהטלפון שלך. אתם מוזמנים להוריד את היישום של ויקינתונים עכשיו: %1$s - שיתוף היישום דרך… + שיתוף היישום דרך... פרטי תמונה לא נמצאו קטגוריות לא נמצאו מוצגים @@ -551,6 +558,7 @@ נוספה קטגוריה. נוספו %1$s קטגוריות. + נוספו %1$s קטגוריות. נוספו %1$s קטגוריות. לא ניתן להוסיף קטגוריות. @@ -560,6 +568,7 @@ נוסף מוצג %1$s נוספו המוצגים %1$s + נוספו המוצגים %1$s נוספו המוצגים %1$s לא היה אפשר להוסיף מוצגים. @@ -602,7 +611,7 @@ נוסף לסימניות משהו השתבש. לא היה אפשר להגדיר את הטפט להגדיר בתור טפט - הגדרת טפט. נא להמתין… + הגדרת טפט. נא להמתין... מערכת מעקב כהה בהירה @@ -662,7 +671,7 @@ תמונות איכות הן תרשימים או תמונות שעומדות בתקני איכות מסוימים (שמטבעם בעיקר טכניים) והן בעלות ערך למיזמי ויקימדיה ההעלאה ממשיכה… ההעלאה מושהית… - ביטול ההעלאה… + ביטול ההעלאה... ביטול ההעלאה הפעלת מצב חיבור מוגבל. כל ההעלאות מושהות ותמשכנה לאחר השבתת המצב הזה. מצב חיבור מוגבל פעיל. @@ -792,6 +801,7 @@ נבחרה תמונה אחת נבחרו שתי תמונות + נבחרו %d תמונות נבחרו %d תמונות נא לזכור שכשמועלות כמה תמונות, כולן מקבלות את אותן הקטגוריות והמוצגים. אם התמונות אינן חולקות מוצגים וקטגוריות, נא לעשות כמה העלאות נפרדות. @@ -805,7 +815,7 @@ בעיה אחרת או מידע אחר (נא להסביר הלאה). המשוב שלך מתפרסם בדף הוויקי הבא: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> האם ברצונך באמת לבטל את כל ההעלאות? - ביטול כל ההעלאות… + ביטול כל ההעלאות... העלאות ממתינות נכשלו diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f60bb30dd..f20b986f8 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -44,6 +44,7 @@ %1$d 件のファイルをアップロード中 + (%1$d) (%1$d) アップロードを開始中です @@ -54,12 +55,14 @@ %d 件のアップロード + この画像は%1$sライセンスのもとにアップロードされます これらの画像は%1$sライセンスのもとにアップロードされます %1$d 件のアップロード + 共有コンテンツを受信中です。 この画像の投稿の処理には、サイズやご使用の機器により時間がかかる事があります 共有コンテンツの受信中です。投稿画像の処理には、サイズやご使用の機器により時間がかかる事があります 探索 @@ -557,7 +560,7 @@ ブックマークに追加 問題が発生しました。壁紙を設定できませんでした。 壁紙として設定 - 壁紙を設定中。お待ちください… + 壁紙を設定中。お待ちください... システムのまま ダーク ライト diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index eb90e4a23..40eb01629 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -47,8 +47,8 @@ Qqen Tettuḍ awal uffir? Jerred - Tuqqna… - Rǧu… + Tuqqna... + Rǧu... Tuqqna tedda! Tqqna ur teddi ara! Ulac afaylu. Ɛreḍ wayeḍ ma ulac aɣilif. @@ -100,7 +100,7 @@ Azen tikti (s yimayl) Ulac amsaɣ n yimayl ibedden Taggayin yettwasqedcenmelmi kan - Araǧu n umtawi amezwaru… + Araǧu n umtawi amezwaru... Ur tsuliḍ ara yakan tiwlafin. Ɛref̣ tikelt-nniḍen Sefsex @@ -130,7 +130,7 @@ Tɣileḍ igarrez? Ih! Taggayin - Asali… + Asali... Ula d yiwet ur tettwafren Ulac aglam Turagt tarussint diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 3703d373f..aa7ae98e7 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -43,22 +43,28 @@ 검색 뷰 오늘의 이미지 + %1$d개의 파일을 올리는 중 %1$d개의 파일을 올리는 중 + (%1$d) (%1$d) 파일 올리기 + %1$d장의 업로드를 처리하는 중입니다 %1$d장의 업로드를 처리하는 중입니다 + %d개 업로드 %d개 업로드 + 이 그림은 %1$s에 따라 사용이 허가됩니다 이 그림은 %1$s에 따라 사용이 허가됩니다 + %1$d개 업로드 %1$d개 업로드 찾아보기 @@ -79,7 +85,7 @@ 로그인 중 기다려 주세요… 캡션 및 설명를 업데이트하는 중 - 기다려 주십시오… + 기다려 주십시오... 로그인 성공! 로그인 실패! 파일을 찾을 수 없습니다. 다른 파일을 사용해 주십시오. @@ -278,6 +284,7 @@ 위키텍스트를 클립보드에 복사했습니다 주변이 제대로 작동되지 않을 수 있습니다. 위치를 사용할 수 없습니다. 주변 장소의 목록을 표시하기 위한 권한이 필요합니다. + 주변 장소의 이미지 목록을 표시하기 위한 권한이 필요합니다 방향 위키데이터 위키백과 @@ -433,6 +440,7 @@ 완료 감사 표현 보내기: 성공 감사 표현 보내기: 실패 + 이것이 저작권 규정을 준수하고 있습니까? 알맞게 분류됐습니까? 기여자에게 감사를 표하시겠습니까? 앗, 분류가 달리지 않은 것 같습니다! @@ -447,10 +455,11 @@ 이미지가 올려지지 않음 읽지 않은 알림이 없습니다 읽은 알림이 없습니다 + 이메일의 받은 편지함을 확인하세요 읽은 항목 보기 읽지 않은 항목 보기 이미지 선택 도중 오류가 발생했습니다 - 기다려 주십시오… + 기다려 주십시오... 다음 미디어로 복사 복사했습니다 공용에 업로드할 좋은 이미지의 예 @@ -465,7 +474,7 @@ 렌즈 모델 일련 번호 소프트웨어 - 앱 공유… + 앱 공유... 이미지 정보 분류가 없습니다 서술이 발견되지 않았습니다 @@ -493,6 +502,7 @@ 성공 설명이 추가되었습니다. 캡션이 추가되었습니다. + 좌표를 추가하지 못했습니다. 설명을 추가하지 못했습니다. 캡션을 추가하지 못했습니다. 이미지 좌표가 업데이트되지 않았습니다 @@ -523,7 +533,7 @@ 북마크에 추가됨 무언가 잘못되었습니다. 배경화면을 설정하지 못했습니다 배경화면으로 설정 - 배경화면을 설정 중입니다. 기다려 주십시오… + 배경화면을 설정 중입니다. 기다려 주십시오... 어두운 밝은 위치 설정을 열지 못했습니다. 위치를 수동으로 켜주세요 @@ -543,6 +553,7 @@ 일시 정지 계속하기 일시 중단됨 + 더 보기 책갈피 리더보드 순위: @@ -638,6 +649,9 @@ 전체 화면 선택 모드에 오신 것을 환영합니다 두 손가락으로 확대 / 축소하세요. 다음 방향으로 길고 재빠르게 넘겨보세요. \n- 왼쪽/오른쪽: 이전/다음으로 이동 \n- 위쪽: 선택\n- 아래쪽: 비업로드용으로 표시 + 스토리지 접근이 거부됨 + 이 항목을 공유할 수 없습니다 + 기능에 대한 권한이 필요합니다 유용한 설명을 추가하는 법 알아보기 유용한 캡션을 추가하는 법 알아보기 업적 보기 @@ -651,6 +665,7 @@ 작성자에게 감사 표시하기 작성자에게 감사를 표하던 도중에 오류가 발생하였습니다. 로그인 세션 만료. 다시 로그인해 주십시오. + GPX 파일을 열 수 있는 응용 프로그램이 없습니다 파일이 성공적으로 저장되었습니다 GPX 파일을 여시겠습니까? KML 파일을 여시겠습니까? @@ -658,8 +673,20 @@ GPX 파일을 저장하지 못했습니다. KML 파일을 저장 중 GPX 파일을 저장 중 + + %d개 이미지 선택됨 + 다중 업로드에 대한 참고사항 이 항목에 관한 문제를 위키데이터에 보고하기 + 의견을 입력해 주십시오 토론 기타 문제 또는 정보 (아래에 설명해 주십시오) + 모든 업로드를 취소하는 중... + 업로드 + 보류 중 + 실패 + 장소 데이터를 불러오지 못했습니다 + 이 장소에 아직 사진이 없습니다. 사진을 찍어보세요! + 이 장소에 이미 사진이 있습니다. + 지금 이 장소에 사진이 있는지 확인 중입니다. diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index be63e9db5..55fa4ac35 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -143,7 +143,7 @@ Оюмунгу билдир (эл. почта бла) Почта клиент къурулмагъанды Кёб болмай хайырланнган категорияла - Биринчи синхронизацияны сакълаб турады… + Биринчи синхронизацияны сакълаб турады... Алкъын джюкленнген фотосуратыгъыз джокъду. Джангыдан сына Ызына ал @@ -227,7 +227,7 @@ Ызына ал Ач Джаб - Баш бет + Тамал бет Джюкле Джуўукъда Юсюнден @@ -498,7 +498,7 @@ Медиа локациягъа джетишиу уналмады Джюклеген суратладан локация билгилени автомат халда алмазгъа боллукъбуз. Тилейбиз, джибериуден алгъа хар сурат ючюн келишген локацияны къошугъуз Фотосуратланы телефонугъуздан туура Викигёзеннге джюклегиз. Гёзен Къошакъны энди эндиригиз: %1$s - Къошакъны буну бла юлюшле… + Къошакъны буну бла юлюшле... Сурат Информация Категорияла табылмадыла Танытыула табылмадыла @@ -575,7 +575,7 @@ Китаб белгилеге къошулду Не эсе да терс кетди. Къабыргъа къагъыт къурулалмады Къабыргъа къагъыт эт - Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз… + Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз... Системаны джарашдыр Къарангы Джарыкъ @@ -633,9 +633,9 @@ Чекленнген Байланыу Режим Агъачлары Мийик Суратла Агъачлы суратла, белгили агъач стандартларына (асламысыны техника халы болады) келишген эмда Викимедиа проектле ючюн багъалы болгъан диаграммала неда фотосуратладыла - Джюклениу андан ары бардырылады… - Джюклениу туракъланады… - Джюклениу ызына алынады… + Джюклениу андан ары бардырылады... + Джюклениу туракъланады... + Джюклениу ызына алынады... Джюклеуню Ызына Ал Чекли байланыу режимни джандырдыгъыз. Бютеу джюклениуле туракълатыллыкъдыла эмда бу режимни джукълатсагъыз, тохтагъан джерден башларыкъдыла. Чекленнген байланыу режим джандырылгъанды. diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index d9d5b65b9..506e9e4b4 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -70,8 +70,8 @@ Te şîfreya xwe ji bîr kir? Xwe tomar bike Têdikeve - Ji kerema xwe piçek bisekine … - Xêra xwe hinek bisekine… + Ji kerema xwe piçek bisekine ... + Xêra xwe hinek bisekine... Têketin bi ser ket! Têketin bi ser neket! Dosye nehat dîtin. Ji kerema xwe re dosyeyek din biceribîne. @@ -183,7 +183,7 @@ Wêneyên Barkirî Wêneyê din Belê, çima na - Ji kerema xwe piçek bisekine … + Ji kerema xwe piçek bisekine ... Wêne tevlî Wîkîpediyayê bike Tu dixwazî vê wêneyê tevlî gotara Wîkîpediyayê ya bi zimanê %1$s bikî? Pişrast bike diff --git a/app/src/main/res/values-kum/strings.xml b/app/src/main/res/values-kum/strings.xml index ab657b354..8112afea6 100644 --- a/app/src/main/res/values-kum/strings.xml +++ b/app/src/main/res/values-kum/strings.xml @@ -49,7 +49,7 @@ Юклев уьлгю: Дюр! Категориялар - Юклев… + Юклев... Бир зат сайланмагъан Тасвири ёкъ Пикирлешивлер ёкъ diff --git a/app/src/main/res/values-kus/strings.xml b/app/src/main/res/values-kus/strings.xml index 02abd4ea1..99fb8c1f7 100644 --- a/app/src/main/res/values-kus/strings.xml +++ b/app/src/main/res/values-kus/strings.xml @@ -62,9 +62,9 @@ Fʋ tami fʋ paaswɛɛtɛ? Yɔ\'ɔgin kpɛn\' Kpɛn\'ɛdnɛ - M bɛlimnɛ gu\'usim… + M bɛlimnɛ gu\'usim... Maligim maal pian\'azut nɛ pa\'alʋg nam - M bɛlimnɛ gu\'usim… + M bɛlimnɛ gu\'usim... Kpɛn\'ɛb nyaŋya Kpɛn\'ɛb gʋ\'ʋŋya M Pʋ nyɛ faal la. M bɛlimnɛ tiakim faal si\'a. @@ -169,7 +169,7 @@ Ɛɛn! Labaya bɛdigʋ Buudi kɔn\'ɔb-kɔn\'ɔb - Bɛ tʋʋma ni… + Bɛ tʋʋma ni... Pʋ gaŋ si\'ela Pian\'azug kae Pa\'alʋg kae @@ -400,7 +400,7 @@ Gɔsim dinɛ ka fʋ karim sa Gɔsim dinɛ ka fʋ nam pʋ karim Daʋŋʋ kidig footonam la nɔkirin - M bɛlimnɛ gu\'usim… + M bɛlimnɛ gu\'usim... Footo banɛ ka fʋ kpɛn\'ɛsi dɔlis zin\'ibanɛ be yamma anɛ footo banɛ ka fʋ kpɛn\'ɛs ka di yinɛ fʋn nyɛ di map ni la. Yaam paas media banɛ bɛ tuon Yaaiya @@ -418,7 +418,7 @@ Serial Numbers Software Pʋ bas suor ye fʋ kpɛn\' midia zin\'iginɛ - Pʋdigim app la dɔlis… + Pʋdigim app la dɔlis... Footo labaar Pʋ paam buudinama Pʋ nyɛ nwɛnnɛm si\'aa. @@ -492,7 +492,7 @@ Ba zaŋi paas bookmarknamin Daʋŋsi\'a naam. Pʋ nyaŋi maal nibdaa footo la Maalimi fʋ nindaa footo la - Maanɛ nindaa footo. M bɛlimnɛ gu\'usim… + Maanɛ nindaa footo. M bɛlimnɛ gu\'usim... Dɔl sistɛm la Lik Nɛɛsim @@ -538,9 +538,9 @@ Bas suor ye di tʋm saŋa bi\'ela! Atʋm bi\'ela zi\'esim Footo sʋma - Lɛm pin\'in kpɛn\'ɛsʋg… - Gu\'om kpɛn\'ɛsʋg… - Basid kpɛn\'ɛsʋg… + Lɛm pin\'in kpɛn\'ɛsʋg... + Gu\'om kpɛn\'ɛsʋg... + Basid kpɛn\'ɛsʋg... Basim kpɛn\'ɛsʋg Bas suor ye di tʋm saŋa bi\'ela. Nwɛnnɛm nam diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index 8b2ab6b95..2eb2fcf2f 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -21,6 +21,7 @@ %1$d файл жүктөлүүдө + Азырынча жүктөөлөр жок 1 жүктөө %1$d жүктөө @@ -136,7 +137,7 @@ Жүктөөнү жокко чыгаруу Артка баскычын колдонуу менен бул жүктөө жокко чыгарылат жана сиз ийгиликти жоготосуз Жүктөөнү улантуу - Күтө туруңуз… + Күтө туруңуз... Аталыш Сыпаттама Элементтер diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index d99e269ab..2ef8f9e03 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -61,7 +61,7 @@ Aloggen Waart wgl. … Beschrëftungen a Beschreiwungen aktualiséieren - Waart wgl. … + Waart wgl. ... Umeldung huet geklappt! D\'Aloggen huet net funktionéiert! Fichier net fonnt. Probéiert wgl. en anere Fichier. @@ -86,6 +86,7 @@ Nobäi Meng eropgeluede Fichieren Deelen + Fichierssäit weisen Beschrëftung (obligatoresch) Gitt wgl. eng Beschrëftung fir dëse Fichier un Beschreiwung @@ -93,6 +94,7 @@ Aloggen huet net funktionéiert – Problemer mam Reseau Ze dacks ouni Succès probéiert. Probéiert wgl. an e puer Minutten nach eng Kéier. Pardon, dëse Benotzer ass op Commons gespaart + Dir musst de Code vun Ärer Zwee-Facteur-Authentifizéierung uginn. Aloggen huet net funktionéiert Eroplueden Gitt dëser Biller een Numm @@ -349,7 +351,7 @@ Déi geliese weisen Déi net geliese weisen Feeler beim Eraussiche vun de Biller - Waart wgl. … + Waart wgl. ... Kopéiert Beispiller vu gudde Biller fir op Commons eropzelueden Beispiller fir Biller, déi een net eropluede sollt @@ -361,7 +363,7 @@ Seriennummeren Software Luet Fotoen direkt vun Ärem Handy op Wikimedia Commons erop. Luet d\'Commons-App elo erof: %1$s - App deelen iwwer… + App deelen iwwer... Bildinformatiounen Keng Kategorie fonnt. Eroplueden ofgebrach @@ -411,7 +413,7 @@ Bei d\'Lieszeechen derbäigesat Et ass Eppes schif gaangen. D\'Hannergrondbild konnt net agestallt ginn Als Hannergrondbild festleeën - Hannergrondbild gëtt agestallt. Waart wgl… + Hannergrondbild gëtt agestallt. Waart wgl... System suivéieren Däischter Hell @@ -454,7 +456,7 @@ Limitéierte Verbindungsmodus Qualitéitsbiller Qualitéitsbiller sinn Diagrammen oder Fotoen, déi gewësse Qualitéitscritèren erfëllen (déi haaptsächlech vun technescher Natur sinn) a wäertvoll fir Wikimedia-Projete sinn. - Eropluede gëtt ofgebrach…. + Eropluede gëtt ofgebrach.... Eroplueden ofbriechen Kategoriesäit weisen Sprooch vum Interface vum Benotzer vun der App diff --git a/app/src/main/res/values-li/strings.xml b/app/src/main/res/values-li/strings.xml index 1720bfbcb..f477ed8f0 100644 --- a/app/src/main/res/values-li/strings.xml +++ b/app/src/main/res/values-li/strings.xml @@ -33,8 +33,8 @@ Melj dich aan Wachwaord vergaete? Teiken dich in - Aan \'nt melje… - Wach estebleef… + Aan \'nt melje... + Wach estebleef... Aanmelje gelök! Aanmelje mislök! Bestandj neet gevónje. Perbeer \'n anger bestandj. @@ -88,7 +88,7 @@ Sjik feedback (mitten e-mail) Geine e-mailcliënt geïnstalleerd Recèntelik gebroekde categorieje - Oppe ieëste synchronisatie \'nt wachte… + Oppe ieëste synchronisatie \'nt wachte... Doe höbs nag gein plaetjes geüpload. Perbeer oppernuuj Braek aaf @@ -127,7 +127,7 @@ Versteis se \'t? Jao! Categorieje - \'nt laje… + \'nt laje... Geine gekaoze Gein besjrieving Ónbekande licentie diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index cb7bebe41..26a9bc7f7 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -33,27 +33,20 @@ Dienos nuotrauka %1$d keliamas failas - %1$d keliami failai - %1$d failų keliamas %1$d keliami failai - %1$d įkėlimas - %1$d įkėlimai - %1$d įkėlimų + \@string/contributions_subtitle_zero + 1 įkėlimas Įkėlimai pradedami Pradedamas %1$d įkėlimas - Pradedami %1$d įkėlimai - Pradedami %1$d įkėlimų Pradedami %1$d įkėlimai %1$d įkėlimas - %1$d įkėlimai - %1$d įkėlimų %1$d įkėlimai Šio paveikslėlio licencija bus %1$s @@ -75,7 +68,7 @@ Jungiamasi Prašome palaukti… Antraštės ir aprašymai atnaujinami - Prašome palaukti… + Prašome palaukti... Sėkmingai prisijungėte! Prisijungti nepavyko! Failas nerastas. Prašome pabandyti kitą failą. @@ -179,7 +172,7 @@ Taip! Daugiau informacijos Kategorijos - Kraunasi… + Kraunasi... Niekas nepasirinkta Nėra antraštės Nėra aprašymo @@ -472,7 +465,7 @@ Žiūrėti perskaitytus Žiūrėti neperskaitytus Renkant vaizdus įvyko klaida - Prašome palaukti… + Prašome palaukti... Rinktinės nuotraukos yra aukštos kvalifikacijos fotografų ir iliustratorių vaizdai, kuriuos Vikiteka bendruomenė pasirinko kaip svetainėje aukščiausios kokybės. Vaizdai, įkelti per Netoliese esančias vietas, yra vaizdai, kurie įkeliami atrandant vietas žemėlapyje. Ši funkcija leidžia redaktoriams siųsti padėkos pranešimą naudotojams, kurie atlieka naudingus pakeitimus, naudojant nedidelę padėkos nuorodą istorijos puslapyje arba skirtumų puslapyje. @@ -493,7 +486,7 @@ Prieiga prie medijos vietos uždrausta Gali būti, kad negalėsime automatiškai gauti vietos duomenų iš jūsų įkeltų nuotraukų. Prieš pateikdami kiekvienai nuotraukai pridėkite tinkamą vietą Įkelkite nuotraukas į Vikiteką tiesiai iš savo telefono. Atsisiųskite Vikitekos programėlę dabar: %1$s - Dalintis programą per … + Dalintis programą per ... Vaizdo informacija Kategorijų nerasta Vaizdų nerasta @@ -753,7 +746,7 @@ Kita problema arba informacija (paaiškinkite toliau). Jūsų atsiliepimai bus paskelbti šiame viki puslapyje: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile App/Feedback</a> Ar tikrai norite atšaukti visus įkėlimus? - Atšaukiami visi įkėlimai… + Atšaukiami visi įkėlimai... Įkėlimai Laukiama Nepavyko diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 9038eec9d..7a6d9e362 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -21,7 +21,7 @@ Reģistrēties Pieslēdzas Lūdzu, uzgaidiet… - Lūdzu, uzgaidi… + Lūdzu, uzgaidi... Ieiešana veiksmīga Pieteikšanās neizdevās. Autentifikācija neizdevās! @@ -163,7 +163,7 @@ Nākamais attēls Skatīt arhivētos Skatīt nelasītos - Lūdzu, uzgaidiet… + Lūdzu, uzgaidiet... Izlaist šo attēlu Autors Autortiesības diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index c496505ae..916f4f420 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -4,7 +4,7 @@ * Violetova * Vlad5250 --> - + Ризницата на Фејсбук Изворен код на Ризницата на Github Лого на Ризницата @@ -52,7 +52,7 @@ %1$d подигања - Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликата и вашиот уред + Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликата и вашиот уред Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликите и вашиот уред Истражи @@ -73,7 +73,7 @@ Најава Почекајте… Поднова на толкувања и описи - Почекајте… + Почекајте... Најавата е успешна! Најавата не успеа! Не ја пронајдов податотеката. Пробајте со друга. @@ -479,7 +479,7 @@ Погл. прочитани Погл. непрочитани Се јави грешка при избирањето на сликите - Почекајте… + Почекајте... Избраните слики се дела на високообучени фотографи и илустратори кои заедницата ги избрала за да бидат истакнати како едни од најдобрите слики на Ризницата. Сликите подигнати преку „Околни места“ се оние подигнати при откривање на места на картата. Ова им дава можност на уредниците да им испраќаат благодарници на корисниците што вршат полезни уредувања. Ова се прави стискајќи на малата врска за заблагодарување во страницата за историја или разлики. @@ -501,7 +501,7 @@ Одибиен пристапот до местоположбата на сликата Можеби нема да можеме автоматски да ги добиеме податоците за местоположба од сликите што ги подигате. Ставете ја соодветната местоположба за секоја слика пред да подигате Подигајте слики непосредно на Ризницата од телефон. Преземете го прилогот на Ризницата сега: %1$s - Сподели преку… + Сподели преку... Инфо за сликата Не пронајдов ниедна категорија Не пронајдов ниедно прикажување @@ -780,7 +780,7 @@ Друг проблем или информација (објаснете подолу). Вашите мислења се објавуваат на следнава викистраница: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Дали сигурно сакате да ги откажете сите подигања? - Ги откажувам сите подигања… + Ги откажувам сите подигања... Подигања Во исчекување Неуспешно diff --git a/app/src/main/res/values-mni/strings.xml b/app/src/main/res/values-mni/strings.xml index 0d8e029a4..de888dcbc 100644 --- a/app/src/main/res/values-mni/strings.xml +++ b/app/src/main/res/values-mni/strings.xml @@ -18,7 +18,7 @@ ꯈꯨꯠꯌꯦꯛ ꯄꯤꯈꯠꯂꯨ ꯃꯅꯨꯡ ꯆꯪꯁꯤꯟꯂꯤ ꯉꯥꯏꯍꯥꯛ ꯉꯥꯏꯕꯤꯌꯨ - ꯉꯥꯏꯍꯥꯛꯇꯪ ꯉꯥꯏꯕꯤꯈꯣ… + ꯉꯥꯏꯍꯥꯛꯇꯪ ꯉꯥꯏꯕꯤꯈꯣ... ꯃꯥꯏꯄꯥꯛꯅꯥ ꯆꯪꯁꯤꯜꯂꯦ ꯫ ꯆꯪꯁꯤꯟꯕ ꯃꯥꯏꯄꯥꯛꯇꯔꯦ! ꯐꯥꯏꯜ ꯊꯤꯕꯥ ꯐꯪꯗꯔꯦ ꯫ ꯆꯥꯟꯕꯤꯗꯨꯅꯥ ꯑꯇꯣꯞꯄ ꯑꯃꯥ ꯇꯧꯕꯤꯔꯣ ꯫ @@ -59,7 +59,7 @@ ꯍꯣꯏ! ꯑꯍꯦꯟꯕ ꯋꯥꯔꯣꯜ ꯃꯆꯥꯈꯥꯏꯕꯁꯤꯡ - ꯆꯤꯡꯂꯤ/ꯎꯪꯂꯤ….. + ꯆꯤꯡꯂꯤ/ꯎꯪꯂꯤ..... ꯑꯃꯠꯇ ꯈꯟꯗꯦ ꯑꯀꯨꯞꯄ ꯃꯔꯣꯜ ꯌꯥꯎꯗꯦ ꯈꯟꯅ-ꯅꯩꯅꯕ ꯂꯩꯇꯦ diff --git a/app/src/main/res/values-mnw/strings.xml b/app/src/main/res/values-mnw/strings.xml index 27a76b0a7..a6c18bca3 100644 --- a/app/src/main/res/values-mnw/strings.xml +++ b/app/src/main/res/values-mnw/strings.xml @@ -45,7 +45,7 @@ ဝိုတ်စ မအက္ခရ်ပၞုက် ပတိုန် စၟတ်သမ္တီ လုပ်လံက်အေန် ဒၟံင် - ပဂုန်တုဲ မင်မွဲလစုတ်… + ပဂုန်တုဲ မင်မွဲလစုတ်... လုက်အေန် အာစိုပ်ဒတုဲ! လံက်အေန် လီုလာ်! ဝှာင် ဟွံဂွံဆဵု၊ ပဂုန်တုဲ ဂၠာဲ ဝှာင်တၞဟ်။ @@ -148,7 +148,7 @@ ယွံ! ဆက်လဴ ပရူတင်ဂၞင် ကဏ္ဍဂမၠိုင် - ပတိုန်ဒၟံင်… + ပတိုန်ဒၟံင်... ဟွံမဲကဵု ပရေၚ်ရုဲစှ် ဟွံမဲကဵု က္ဍိုပ်လိက် ဟွံမဲကဵု ဗမံက်ထ္ၜး diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 965594585..546b43f4f 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -17,6 +17,7 @@ %1$d संचिका अपभारीत होत आहे + अद्याप अपभारणे नाहीत एक अपभारण %1$d अपभारणे @@ -93,7 +94,7 @@ प्रतिसाद पाठवा (विपत्राद्वारे) कोणतेही ईमेल क्लायंट स्थापित नाहीत अलीकडे वापरलेले वर्ग - प्रथम संकालनाची प्रतीक्षा करीत आहे … + प्रथम संकालनाची प्रतीक्षा करीत आहे ... आपण अद्याप काहीच चित्रे अपभारीत केली नाहीत. पुन्हा प्रयत्न करा रद्द करा diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index e5dd0f3be..1fce0c0da 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -19,16 +19,20 @@ အားလုံး ယနေ့အတွက် အထူးဓာတ်ပုံ + ဖိုင် %1$d ခု တင်နေသည် ဖိုင် %1$d ခု တင်နေသည် အပ်ပလုဒ်များ စတင်ခြင်း + %1$d ခု တင်ထားသည် %1$d ခု တင်ထားသည် + ဤရုပ်ပုံသည် %1$ အောက်တွင် လိုင်စင်သတ်မှတ်ထးပါမည် ဤရုပ်ပုံများသည် %1$ အောက်တွင် လိုင်စင်သတ်မှတ်ထးပါမည် + %1$d အက်ပလုပ် %1$d အက်ပလုပ်များ ရှာဖွေစူးစမ်းပါ @@ -45,9 +49,9 @@ အကောင့်ဝင်ရန် စကားဝှက် မေ့နေပါသလား မှတ်ပုံတင်ရန် - လော့ဂ်အင် ဝင်ရောက်နေသည်… - ခေတ္တစောင့်ပါ… - ကျေးဇူးပြု၍ ခဏစောင့်ပါ… + လော့ဂ်အင် ဝင်ရောက်နေသည်... + ခေတ္တစောင့်ပါ... + ကျေးဇူးပြု၍ ခဏစောင့်ပါ... လော့အင် အောင်မြင်သည် လော့အင် မအောင်မြင်ပါ ဖိုင်မတွေ့ပါ၊ အခြးဖိုင်တစ်ခု စမ်းကြည့်ပါ။ @@ -129,7 +133,7 @@ ဟုတ်ကဲ့ သတင်းအချက်အလက် ပို၍ ကဏ္ဍများ - ဝန်ဆွဲတင်နေသည်… + ဝန်ဆွဲတင်နေသည်... ဘာမှရွေးချယ်မထားပါ ပုံစာ မရှိ ဖော်ပြချက် မရှိ @@ -311,7 +315,7 @@ မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ ရုပ်ပုံများကိုရွေးနေစဉ် အမှားဖြစ်ပွားခဲ့ပါသည် - ကျေးဇူးပြု၍ ခဏစောင့်ပါ… + ကျေးဇူးပြု၍ ခဏစောင့်ပါ... နမူနာရုပ်ပုံများ အက်ပလုပ်တင်ရန် မဟုတ်ပါ ဤရုပ်ပုံအား ကျော်သွားမည် ဒေါင်းလုဒ် မအောင်မြင်ပါ။ ပြင်ပသိုလှောင်မှုခွင့်ပြုချက်မရှိဘဲ ဖိုင်ဒေါင်းလုဒ်မဆွဲနိုင်ပါ။ diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3b4bf30dc..de59e28de 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -522,7 +522,7 @@ Toegang tot medialocatie geweigerd Het is mogelijk dat we niet automatisch locatiegegevens kunnen verkrijgen van foto\'s die u uploadt. Voeg de locatie bij elke foto toe voordat u die upload Upload foto\'s rechtstreeks vanaf uw telefoon naar Wikimedia Commons. Download de Commons-app nu: %1$s - App delen via… + App delen via... Afbeeldingsinfo Geen categorieën gevonden Geen beschrijvingen gevonden @@ -599,7 +599,7 @@ Als bladwijzer toegevoegd Er is iets fout gegaan. Kan de achtergrond niet instellen Instellen als achtergrond - Wordt ingesteld als achtergrond. Een ogenblik geduld… + Wordt ingesteld als achtergrond. Een ogenblik geduld... Systeem volgen Donker Licht @@ -659,7 +659,7 @@ Kwaliteitsafbeeldingen zijn diagrammen of foto\'s die voldoen aan bepaalde kwaliteitsnormen (die meestal technisch van aard zijn) en waardevol zijn voor Wikimedia-projecten Uploaden hervatten… Uploaden onderbreken… - Uploaden wordt geannuleerd… + Uploaden wordt geannuleerd... Uploaden Annuleren U hebt de beperkte verbindingsmodus ingeschakeld. Alle uploads worden gepauzeerd en worden hervat zodra u deze modus uitschakelt. Beperkte verbindingsmodus is ingeschakeld. @@ -806,4 +806,7 @@ In behandeling Mislukt Plaatsgegevens konden niet geladen worden + Er is nog geen foto van deze plek, maak er eentje! + Er is al een foto van deze plek. + We controleren nu of er een foto van deze plek is. diff --git a/app/src/main/res/values-nqo/strings.xml b/app/src/main/res/values-nqo/strings.xml index 62e01d4d5..7e11ea03a 100644 --- a/app/src/main/res/values-nqo/strings.xml +++ b/app/src/main/res/values-nqo/strings.xml @@ -51,9 +51,9 @@ ߌ ߓߘߊ߫ ߢߌ߬ߣߊ߬ ߌ ߟߊ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߠߊ߫؟ ߖߊ߬ߕߋ߬ߘߊ ߟߊߞߊ߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߦߋ߫ ߛߋ߲߬ߠߊ߫ - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... ߝߍ߬ߛߓߍߟߌ ߣߌ߫ ߞߊ߲߬ߛߓߍߟߌ ߟߊߞߎߘߦߊ ߦߴߌ ߘߐ߫ - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߛߎߘߊ߲߫߹ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߗߌߙߏ߲߫߹ ߞߐߕߐ߮ ߡߊ߫ ߛߐ߬ߘߐ߲߬. ߘߏ߫ ߜߘߍ߫ ߡߊߝߍߣߍ߲߫ ߖߊ߰ߣߌ߲߫. @@ -69,7 +69,7 @@ ߊ߬ ߛߐ߲߬ߞߌ߲߫ ߞߵߊ߬ ߦߋ߫ ߊ߬ ߛߐ߲߬ߞߌ߲߫ ߞߵߊ߬ ߦߋ߫ ߒ ߠߊ߫ ߟߊ߬ߦߟߍ߬ߣߍ߲߬ ߞߐ߯ߟߕߊ ߟߎ߬ - ߘߞߐ߬ߣߐ߲߬ߠߌ߲ ߘߐ߫… + ߘߞߐ߬ߣߐ߲߬ߠߌ߲ ߘߐ߫... ߊ߬ ߓߘߊ߫ ߗߌߙߏ߲߫ %1$d%% ߓߘߊ߫ ߘߝߊ߫ ߟߊ߬ߦߟߍ߬ߟߌ ߦߋ߫ ߛߋ߲߬ߠߊ߫ @@ -148,7 +148,7 @@ ߐ߲߬ߐ߲߬ߐ߲߫߹ ߞߎ߲߬ߠߊ߬ߝߎ߬ߟߋ߲߬ ߜߘߍ ߟߎ߬ ߦߌߟߡߊ ߟߎ߬ - ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫… + ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫... ߊ߬ ߡߊ߫ ߓߊߕߐ߬ߡߐ߲߬ ߝߍ߬ߛߓߍߟߌ߫ ߕߍ߫ ߦߋ߲߬ ߞߊ߲߬ߛߓߍߟߌ߫ ߕߴߦߋ߲߬ @@ -408,7 +408,7 @@ ߘߐ߬ߞߊ߬ߙߊ߲߬ߣߍ߲ ߠߎ߬ ߦߋ߫ ߘߐ߬ߞߊ߬ߙߊ߲߬ߓߊߟߌ ߟߎ߬ ߦߋ߫ ߝߎ߬ߕߎ߲߬ߕߌ ߓߌ߬ߟߊ߬ߣߍ߲߫ ߊ߬ ߘߐ߫ ߞߵߌ ߕߏ߫ ߖߌ߬ߦߊ߬ߓߍ ߓߊߕߐ߬ߡߐ߲ ߞߊ߲߬. - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... ߓߘߊ߫ ߓߊߓߌ߬ߟߊ߬ ߖߌ߬ߦߊ߬ߓߍ߬ ߢߌ߬ߡߊ߬ ߟߊߦߟߍ߬ߕߊ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ ߖߌ߬ߦߊ߬ߓߍ߬ ߖߎ߰ ߟߊߦߟߍ߬ߓߊߟߌ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ @@ -418,7 +418,7 @@ ߘߌ߲߬ߞߌߙߊ ߖߌ߬ߦߊ߬ߕߊ߬ߟߊ߲ ߛߎ߮ߦߊ ߛߎ߲ߝߘߍ - ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬… + ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬... ߖߌ߬ߦߊ߬ߓߍ ߞߌ߬ߓߊ߬ߙߏ߬ߦߊ ߦߌߟߡߊߙߋ߲߫ ߕߴߦߋ߲߬ ߘߊ߲߬ߠߊ߬ߕߍ߰ߟߌ ߡߊ߫ ߛߐ߬ߘߐ߲߬ @@ -486,7 +486,7 @@ ߊ߬ ߓߌ߬ߟߊ߬ ߟߊ߬ߡߊ ߘߐ߫ ߞߏ ߘߏ߫ ߓߍ߲߬ߣߍ߫ ߕߎ߲߬ ߕߍ߫. ߘߊ߲߬ߘߊ߲߬ߥߟߊ ߕߍ߫ ߛߐ߲߬ ߘߐߓߍ߲߬ ߠߊ߫. ߊ߬ ߓߌ߬ߟߊ߬ ߘߊ߬ߣߊ߲߬ߥߟߊ ߟߊ߫. - ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫… + ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫... ߞߊ߲ߞߋ ߟߊߓߊ߬ߕߏ߬ ߘߌ߬ߓߌ ߦߋߟߋ߲ @@ -533,9 +533,9 @@ ߟߊߓߊ߯ߙߊߣߍ߲ ߒ ߠߊ߫ ߛߝߊ ߖߌ߬ߦߊ߬ߓߍ ߛߎ߯ߦߊ - ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫… - ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫… - ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫… + ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫... + ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫... + ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫... ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߓߌ߬ߟߊ߬ ߡߋߘߌߦߊ ߝߊߙߊ߲ߝߊ߯ߛߌ ߦߌߟߡߊ߫ ߞߐߜߍ ߘߐߜߍ߫ diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index eab67e076..c097898e9 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -81,7 +81,7 @@ Mandar vòstres comentaris (per corrièl) Cap de client de corrièl pas installat Categorias utilizadas recentament - Espèra de primièra sincronizacion… + Espèra de primièra sincronizacion... Avètz pas encara telecargat cap de fòto. Tornar ensajar Anullar diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index d0ee73396..8c64900a5 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -8,7 +8,7 @@ * Sony dandiwal * ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ --> - + ਕਾਮਨਜ਼ ਮਾਰਕਾ ਇੱਕ ਹੋਰ ਵੇਰਵਾ ਸ਼ਾਮਲ ਕਰੋ ਨਵਾਂ ਯੋਗਦਾਨ ਸ਼ਾਮਲ ਕਰੋ @@ -17,10 +17,11 @@ ਸਾਰੇ ਦਿਨ ਦੀ ਤਸਵੀਰ - ੧ ਫ਼ਾਈਲ ਚੜ੍ਹਾਈ ਜਾ ਰਹੀ ਹੈ + ੧ ਫ਼ਾਈਲ ਚੜ੍ਹਾਈ ਜਾ ਰਹੀ ਹੈ %1$d ਫ਼ਾਈਲਾਂ ਚੜ੍ਹਾਈਆਂ ਜਾ ਰਹੀਆਂ ਹਨ + \@string/contributions_subtitle_zero %1$d upload %1$d ਅੱਪਲੋਡ @@ -29,7 +30,7 @@ %1$d ਸ਼ੁਰੂ ਹੋ ਰਹੇ ਹਨ - %1$d ਅੱਪਲੋਡ + &d ਅੱਪਲੋਡ %1$d ਅੱਪਲੋਡਾਂ ਇਹ ਤਸਵੀਰ ਦਾ %1$s ਹੇਠ ਲਸੰਸ ਜਾਰੀ ਕੀਤੀ ਜਾਵੇਗਾ @@ -44,7 +45,7 @@ ਪਾਰਸ਼ਬਦ ਭੁੱਲ ਗਏ? ਦਾਖ਼ਲਾ ਹੋ ਰਿਹਾ ਹੈ ਉਡੀਕੋ ਜੀ… - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... ਦਾਖ਼ਲ ਹੋਣਾ ਸਫ਼ਲ! ਦਾਖ਼ਲ ਹੋਣਾ ਅਸਫ਼ਲ! ਫ਼ਾਇਲ ਦੀ ਖੋਜ ਨਹੀਂ ਹੋ ਸਕੀ। ਕਿਰਪਾ ਕਰਕੇ ਹੋਰ ਫ਼ਾਇਲ ਖੋਜੋ। @@ -128,7 +129,7 @@ ਹਾਂ! ਹੋਰ ਜਾਣਕਾਰੀ ਸ਼੍ਰੇਣੀਆਂ - ਲੱਦ ਰਿਹਾ ਹੈ… + ਲੱਦ ਰਿਹਾ ਹੈ... ਕੋਈ ਵੀ ਨਹੀਂ ਚੁਣਿਆ ਕੋਈ ਵੇਰਵਾ ਨਹੀਂ ਕੋਈ ਗੱਲਬਾਤ ਨਹੀਂ @@ -200,7 +201,7 @@ ਇਜਾਜ਼ਤ ਦਿਓ ਖ਼ਾਰਜ ਕਰੋ ਧੰਨਵਾਦ ਭੇਜਣਾ: ਸਫਲ ਹੋਇਆ - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... ਉਤਾਰਾ ਕੀਤਾ ਟਿਕਾਣਾ ਲਿਖਤ ਛਾਪੋ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 09132f40e..dcd8ea284 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -508,7 +508,7 @@ Zobacz przeczytane Wyświetl nieprzeczytane Wystąpił błąd podczas pobierania zdjęć - Proszę czekać… + Proszę czekać... Polecane zdjęcia to zdjęcia wysoko wykwalifikowanych fotografów i ilustratorów, które społeczność Wikimedia Commons wybrała jako jedne z najwyższych jakości na stronie. Obrazy przesłane przez Pobliskie miejsca to obrazy, które są przesyłane przez odkrywanie miejsc na mapie. Ta funkcja umożliwia redaktorom wysyłanie powiadomień z podziękowaniem do użytkowników, którzy dokonują przydatnych zmian - za pomocą małego linku z podziękowaniem na stronie historii lub na stronie diff. diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index b7449e957..bfbd64413 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -477,7 +477,7 @@ Vëdde lòn ch\'a l\'é stàit lesù Vëdde lòn ch\'a l\'é ancor nen ëstàit lesù A-i é staje n\'eror an selessionand le plance - Ch\'a l\'abia passiensa… + Ch\'a l\'abia passiensa... Le fòto an evidensa a son ëd plance fàite da dij fotògraf e ilustrator motobin àbij che la comunità ëd Wikipedia Commons a l\'ha sernù tra cole ëd qualità pi àuta an sël sit. Le plance carià dai pòst ëd prossimità a son le plance carià con la dëscuverta dij pòst an sla carta. Costa fonsionalità a përmet ai contributor ëd mandé na notìfica d\'aringrassiament a j\'utent ch\'a fan dle modìfiche ùtij - an dovrand na cita liura d\'aringrassiament an sla pàgina dla stòria o cola dle diferense. @@ -499,7 +499,7 @@ Acess a la locassion dël mojen arfudà I podoma pa oten-e an automàtich ij dàit ëd localisassion dle plance che chiel a caria. Për piasì, ch\'a giontà la posission apropià për tute le plance prima ëd mandeje Ch\'a caria dle fòto su Wikimedia Commons diretaman da sò teléfon. Ch\'a dëscaria l\'aplicassion Commons adess: %1$s - Partagé l\'aplicassion via… + Partagé l\'aplicassion via... Anformassion an sla plancia Gnun-e categorìe trovà Gnun-e descrission trovà @@ -776,9 +776,12 @@ Àutr problema o anformassion (për piasì, ch\'a spiega sì-sota). Ij sò sugeriment a saran giontà a coste pàgine wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> É-lo sigur ëd vorèj anulé tuti ij cariament? - Anulament ëd tuti ij cariament… + Anulament ëd tuti ij cariament... Cariament An atèisa Falì Impossìbil carié ij dàit dël pòst + Ës pòst a l\'ha ancor gnun-e fòto, ch\'a na pija un-a! + Ës pòst a l\'ha già dle fòto. + An camin ch\'as verìfica si cost pòst -sì a l\'ha dle fòto. diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 461cb6b1d..4f17da26f 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -65,7 +65,7 @@ CC BY 3.0 هو وېشنيزې - رابرسېرېږي… + رابرسېرېږي... هېڅ هم نه دی ټاکل شوی څرگندونه نشته نامعلوم جواز diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index b0dd3b016..3779a8a51 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -28,7 +28,7 @@ * Tuliouel * YuriNikolai --> - + Página do Commons no Facebook Código fonte do Commons no Github Logotipo do Commons @@ -51,39 +51,32 @@ Estado do local Imagem do Dia - carregando arquivo - carregando %1$d arquivos + carregando arquivo carregando %1$d arquivos (%1$d) - (%1$d) (%1$d) Iniciando carregamentos Processando %d carregamento - Processando %d carregamentos Processando %d carregamentos %d carregamento - %d carregamentos %d carregamentos Esta imagem será licenciada sob %1$s - Estas imagens serão licenciadas sob %1$s Estas imagens serão licenciadas sob %1$s %1$d carregamento - %1$d carregamentos %1$d carregamentos - Recebendo conteúdo compartilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da imagem e do seu dispositivo - Recebendo conteúdo compartilhado. O processamento das imagens pode levar algum tempo, dependendo do tamanho das imagens e do dispositivo + Recebendo conteúdo compartilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da imagem e do seu dispositivo Recebendo conteúdo compartilhado. O processamento das imagens pode levar algum tempo, dependendo do tamanho das imagens e do dispositivo Explorar @@ -102,9 +95,9 @@ Esqueceu a senha? Cadastre-se Efetuar login - Por favor, aguarde… + Por favor, aguarde... Atualizando legendas e descrições - Por favor, aguarde… + Por favor, aguarde... Login bem sucedido Falha na identificação Arquivo não encontrado. Tente outro arquivo. @@ -168,7 +161,7 @@ Sobre O Wikimedia Commons é um aplicativo de código aberto criado e mantido por beneficiários e voluntários da comunidade Wikimedia. A Wikimedia Foundation não está envolvida na criação, desenvolvimento ou manutenção do aplicativo. Criar uma nova <a href=\"%1$s\">publicação no GitHub</a> para informar erros e sugestões. - Política de privacidade + Politica de privacidade Créditos Sobre Enviar comentários (por e-mail) @@ -255,7 +248,7 @@ Ponte de Arco-Íris Tulipa Bem-vindo à Wikipédia - Direitos de autor são bem-vindo + Direitos de autor são bem vindo Ópera de Sydney Cancelar Abrir @@ -313,7 +306,7 @@ Commons Avalie-nos Perguntas frequentes - Guia de usuário + Guia de usuario Pular Tutorial A Internet não está disponível Erro ao tentar obter as notificações @@ -528,7 +521,7 @@ Acesso à localização da mídia negado É possível que não possamos obter automaticamente os dados de localização das imagens que você carregar. Por favor adicione a localização adequada para cada imagem antes de envia-las Carregue fotos na wiki Wikimedia Commons, diretamente do seu celular. Baixe o aolicativo Commons agora: %1$s - Compartilhar aplicativo via… + Compartilhar aplicativo via... Informação da imagem Nenhuma categoria encontrada Nenhuma representação encontrada @@ -555,7 +548,6 @@ Sucesso A categoria %1$s foi adicionada. - As categorias %1$s foram adicionadas. As categorias %1$s foram adicionadas. Não foi possível adicionar categorias. @@ -564,7 +556,6 @@ Editar representações O elemento retratado %1$s está adicionado. - Os elementos retratados %1$s estão adicionados. Os elementos retratados %1$s estão adicionados. Não foi possível adicionar os elementos retratados. @@ -776,7 +767,6 @@ Salvar arquivo GPX %d imagem selecinada - %d imagens selecionadas %d imagens selecionadas Escreva algo sobre o item %1$s. Isso será visivel publicamente. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 19a52d72f..bad9dc500 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -20,7 +20,7 @@ * Unamane * Vitorvicentevalente --> - + Página da wiki Commons no Facebook Código-fonte da wiki Commons no Github Logótipo da wiki Commons @@ -44,38 +44,31 @@ Imagem do Dia a carregar %1$d ficheiro - a carregar muitos %1$d ficheiros a carregar %1$d ficheiros (%1$d) - (%1$d) (%1$d) A iniciar carregamentos A processar %d carregamento - A processar %d carregamentos A processar %d carregamentos %d carregamento - %d carregamentos %d carregamentos Esta imagem será licenciada com a %1$s - Estas imagens serão licenciadas com a %1$s Estas imagens serão licenciadas com a %1$s %1$d carregamento - %1$d carregamentos %1$d carregamentos - A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo - A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo + A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo A receber conteúdo partilhado. O processamento das imagens pode demorar algum tempo, dependendo do tamanho das mesmas e do seu dispositivo Explorar @@ -163,8 +156,8 @@ Política de privacidade Créditos Sobre - Enviar comentários (por correio eletrónico) - Não foi instalado nenhum cliente de correio eletrónico + Enviar comentários (por correio eletrónico) + Não foi instalado nenhum cliente de correio eletrónico Categorias usadas recentemente A aguardar pela primeira sincronização… Não carregou ainda nenhuma foto. @@ -283,7 +276,7 @@ Gravar as fotografias tiradas com a câmara da aplicação no armazenamento do seu dispositivo Inicie sessão na sua conta Enviar ficheiro de registos - Enviar o ficheiro de registos aos programadores por correio eletrónico para ajudar a corrigir problemas da aplicação. Nota: os registos podem conter informações identificativas + Enviar o ficheiro de registos aos programadores por correio eletrónico para ajudar a corrigir problemas da aplicação. Nota: os registos podem conter informações identificativas Não foi encontrado nenhum navegador da Internet para abrir o URL Erro! Não foi possível encontrar o URL Nomear para eliminação @@ -498,7 +491,7 @@ Ver lidas Ver não lidas Ocorreu um erro ao escolher imagens - Aguarde, por favor… + Aguarde, por favor... As fotografias destacadas são imagens de fotógrafos e ilustradores altamente qualificados, que a comunidade da wiki Wikimedia Commons escolheu como as de melhor qualidade do \'\'site\'\'. As imagens carregadas via \"Locais próximos\" são as imagens que são carregadas descobrindo locais do mapa. Esta funcionalidade permite que os editores enviem uma notificação de agradecimento aos utilizadores que fizerem edições úteis - usando uma pequena hiperligação de agradecimento na página do historial ou na de diferenças. @@ -520,7 +513,7 @@ Acesso à localização de multimédia negado Podemos não conseguir obter automaticamente os dados de localização das fotografias que carregar. Adicione a localização apropriada de cada fotografia antes de a enviar, por favor Carregue fotografias na wiki Wikimedia Commons, diretamente do seu telemóvel. Descarregue a aplicação Commons agora: %1$s - Partilhar aplicação por… + Partilhar aplicação por... Informação da imagem Não foi encontrada nenhuma categoria Não foi encontrada nenhuma representação @@ -547,7 +540,6 @@ Êxito A categoria %1$s foi adicionada. - As categorias %1$s foram adicionadas. As categorias %1$s foram adicionadas. Não foi possível adicionar categorias. @@ -556,7 +548,6 @@ Editar elementos retratados O elemento retratado %1$s está adicionado. - Os elementos retratados %1$s estão adicionados. Os elementos retratados %1$s estão adicionados. Não foi possível adicionar os elementos retratados. @@ -598,7 +589,7 @@ Adicionado aos marcadores Ocorreu um problema. Não foi possível definir a imagem de fundo Definir como imagem de fundo - A definir a imagem de fundo. Aguarde, por favor… + A definir a imagem de fundo. Aguarde, por favor... Seguir sistema Escuro Claro @@ -654,8 +645,8 @@ Modo de ligação limitada Imagens de qualidade As imagens de qualidade são diagramas ou fotografias que satisfazem certos padrões de qualidade (principalmente de natureza técnica) e são valiosos para projetos da Wikimedia - A retomar carregamento… - A pausar carregamento… + A retomar carregamento... + A pausar carregamento... A cancelar o carregamento… Cancelar carregamento Ativou o modo de ligação limitada. Todos os carregamentos foram colocados em pausa e serão retomados quando desativar este modo. @@ -718,7 +709,7 @@ Não foi encontrada nenhuma localização Que tal adicionar o local onde a imagem foi tirada?\nOs dados de localização ajudam os editores da wiki a encontrarem a sua fotografia, tornando-a muito mais útil.\nObrigado! Adicionar localização - Remova desta mensagem de correio todas as informações que não se sinta à vontade em partilhar publicamente, por favor. Adicionalmente, esteja consciente de que o seu endereço de correio eletrónico, com o qual está a fazer esta publicação, e o nome e imagem de perfil a ele associados, serão visíveis pelo público geral. + Remova desta mensagem de correio todas as informações que não se sinta à vontade em partilhar publicamente, por favor. Adicionalmente, esteja consciente de que o seu endereço de correio eletrónico, com o qual está a fazer esta publicação, e o nome e imagem de perfil a ele associados, serão visíveis pelo público geral. Detalhes As realizações só estão disponíveis na versão de produção; consulte a documentação para programadores, por favor. A tabela de classificação só está disponível na versão prod. Consulte a documentação do desenvolvedor. @@ -769,7 +760,6 @@ Erro no envio de agradecimento ao autor. %d imagem selecionada - %d imagens selecionadas %d imagens selecionadas diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index ad1d0b805..0bcbc1550 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -28,8 +28,8 @@ %1$d de fișiere se încarcă + \@string/contributions_subtitle_zero (%1$d) - (%1$d) (%1$d) Pornirea încărcărilor @@ -74,8 +74,8 @@ V-ați uitat parola? Înregistrare Se conectează - Vă rugăm să așteptați … - Vă rugăm să așteptați … + Vă rugăm să așteptați ... + Vă rugăm să așteptați ... Autentificare reușită! Autentificare nereușită! Fișierul nu a fost găsit. Încercați cu un alt fișier. @@ -458,7 +458,7 @@ Vezi citit Vezi necitit A apărut o eroare la alegerea imaginilor - Vă rugăm să așteptați … + Vă rugăm să așteptați ... Imaginile de Calitate sunt imagini ale unor fotografi și ilustratori de înaltă calificare, pe care comunitatea Wikimedia Commons a ales-o ca fiind de cea mai înaltă calitate pe site. Imaginile Încărcate prin Locurile din Apropiere sunt imaginile care sunt încărcate prin descoperirea locurilor de pe hartă. Această caracteristică permite editorilor să trimită o notificare de Mulțumire utilizatorilor care fac modificări utile - folosind un mic link de mulțumire pe pagina istoric sau pe pagina dif. @@ -478,7 +478,7 @@ Numere Serie Software Încărcați fotografii pe Wikimedia Commons direct de pe telefon. Descărcați aplicația Commons acum: %1$s - Partajează aplicația prin … + Partajează aplicația prin ... Informații despre imagine Nu s-au găsit categorii Nu s-au Găsit Reprezentări diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 861d7ee27..b15d77787 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -45,7 +45,7 @@ * ЛингвоЧел * ОйЛ --> - + Facebook-страница Викисклада Исходный код Викисклада на гитхабе Логотип Викисклада @@ -105,7 +105,7 @@ %1$d загрузок - Получение общего содержимого. Обработка изображения может занять некоторое время, в зависимости от размера изображения и модели вашего устройства + Получение общего содержимого. Обработка изображения может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства @@ -556,7 +556,7 @@ Отказано в доступе к местоположению файла Возможно, мы не сможем автоматически получать данные о местоположении из загруженных вами изображений. Пожалуйста, добавьте подходящее место для каждого изображения перед отправкой Загружайте фото на Викисклад прямо с телефона. Скачайте приложение Wikimedia Commons прямо сейчас: %1$s - Поделиться приложением с помощью… + Поделиться приложением с помощью... Информация об изображении Категории не найдены. Описания не найдены @@ -637,7 +637,7 @@ Добавлено в закладки Что-то пошло не так. Не удалось установить фоновую заставку Сделать фоновой заставкой - Идёт установка фоновой заставки… + Идёт установка фоновой заставки... Настройки системы Тёмная Светлая @@ -695,8 +695,8 @@ Режим ограниченного подключения Качественные изображения Качественные изображения - это диаграммы или фотографии, которые соответствуют определенным стандартам качества (которые в основном носят технический характер) и представляют ценность для проектов Викимедиа - Возобновление загрузки… - Приостановка загрузки… + Возобновление загрузки... + Приостановка загрузки... Отмена загрузки… Отменить загрузку Вы включили ограниченный режим подключения. Все загрузки приостановлены и возобновятся после отключения этого режима. @@ -841,7 +841,7 @@ Другая проблема или информация (пожалуйста, объясните ниже). Ваш отзыв будет опубликован на следующей вики-странице: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Вы уверены, что хотите отменить все загрузки? - Отмена всех загрузок… + Отмена всех загрузок... Загрузки В ожидании Не удалось diff --git a/app/src/main/res/values-sd/strings.xml b/app/src/main/res/values-sd/strings.xml index d4b591659..08f9a1fec 100644 --- a/app/src/main/res/values-sd/strings.xml +++ b/app/src/main/res/values-sd/strings.xml @@ -169,7 +169,7 @@ ھا! وڌيڪ معلومات زمرا - لاهيندي… + لاهيندي... ڪوبہ چونڊيل ناھي عنوان ناهي ڪا تشريح ناھي @@ -315,7 +315,7 @@ لينس ماڊل سيريل انگ سافٽويئر - ايپ ذريعي ونڊيو… + ايپ ذريعي ونڊيو... عڪس معلومات زمرا نہ لڌا رد-ڪيل چاڙھ diff --git a/app/src/main/res/values-se/strings.xml b/app/src/main/res/values-se/strings.xml index 0489c363a..78114e336 100644 --- a/app/src/main/res/values-se/strings.xml +++ b/app/src/main/res/values-se/strings.xml @@ -44,9 +44,9 @@ Vajáldahttetgo beassansáni? Searvva Čáliha sisa - Vuordil… + Vuordil... Ođasmáhttá govvateavsttaid ja govvádusaid - Vuordil… + Vuordil... Sisačáliheapmi lihkostuvai! Sisačáliheapmi ii lihkostuvvan! Fiila ii gávdnon. Geahččal áinnas eará fiilla. @@ -112,7 +112,7 @@ Atte máhcahaga (e-poasttain) Ii leat ásahuvvon epoastadoaimmaheaddji Áitto geavahuvvon kategoriijat - Vuordime vuosttaš synkroniserema… + Vuordime vuosttaš synkroniserema... It leat vel bajásluđen ovttage gova. Geahččal ođđasit Gaskkalduhte @@ -143,7 +143,7 @@ Jua! Lassedieđut Kategoriijat - Luđeme… + Luđeme... Ii guhtege válljejuvvon Ii leat govvateaksta Ii gávdno govvádus diff --git a/app/src/main/res/values-sh/strings.xml b/app/src/main/res/values-sh/strings.xml index 8e9cde75c..5997677ef 100644 --- a/app/src/main/res/values-sh/strings.xml +++ b/app/src/main/res/values-sh/strings.xml @@ -112,7 +112,7 @@ Pošaljite Vašu povratnu informaciju (putem e-pošte) Nemate uspostavljen klijent za e-poštu Nedavno korištene kategorije - Čekam prvo usklađivanje… + Čekam prvo usklađivanje... Još uvijek niste otpremili nijednu sliku. Pokušaj ponovo Otkaži @@ -147,7 +147,7 @@ Da! Više informacija Kategorije - Učitavanje… + Učitavanje... Ništa nije odabrano Nema opisa Nema razgovora diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 0e661acb7..92fa25f3e 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -5,24 +5,25 @@ * Sandaru * හරිත --> - + කොමන්ස් ෆේස්බුක් පිටුව කොමන්ස් ලාන්චනය කොමන්ස් වෙබ් අඩවිය - 1 ගොනුවක් උඩුගත කෙරේ + 1 ගොනුවක් උඩුගත කෙරේ ගොනු %d ක් උඩුගත කෙරේ - එක් උඩුගත කිරීමක් ඇත + තවමත් කිසිදු උඩුගත කිරීමක් නැත + එක් උඩුගත කිරීමක් ඇත උඩුගත කිරීම් %1$d ක් ඇත - 1 උඩුගත කිරීමක් ආරම්භ කරමින් + 1 උඩුගත කිරීමක් ආරම්භ කරමින් උඩුගත කිරීම් %1$d ක් ආරම්භ කරමින් - 1 උඩුගත කිරීමක් + 1 උඩුගත කිරීමක් උඩුගත කිරීම් %1$d ක් මෙම පින්තූරය %1$s යටතේ වලංගු වනු ඇත diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 99a0bf548..49fc88a3b 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -491,7 +491,7 @@ Zobraziť prečítané Zobraziť neprečítané Nastala chyba pri vyberaní obrázkov - Čakajte, prosím… + Čakajte, prosím... Najlepšie obrázky sú fotografie od vysoko skúsených fotografov a ilustrátorov, ktoré vybrala komunita Wikimedie Commons ako jedny z najkvalitnejších na stránke. Obrázky nahrané cez Miesta v okolí sú obrázky, ktoré sú nahrané vďaka objavovaniu miest na mape. Táto funkcia umožňuje poslať poďakovanie za užitočné úpravy používateľom – použitím malého odkazu poďakovať v histórií stránky alebo na stránke rozdielu medzi revíziami. @@ -513,7 +513,7 @@ Prístup k polohe médií bol odmietnutý Možno nebudeme môcť automaticky získať údaje o polohe z obrázkov, ktoré nahráte. Pred odoslaním, prosím, pridajte ku každému obrázku údaj o polohe. Nahrávajte fotky na Wikimedia Commons priamo z vášho mobilu. Stiahnite si aplikáciu Wikimedia Commons teraz: %1$s - Zdieľať aplikáciu cez… + Zdieľať aplikáciu cez... Informácie o obrázku Nenájdené žiadne kategórie Neboli nájdené spôsoby vykreslovania @@ -593,7 +593,7 @@ Pridané do záložiek Niečo sa pokazilo. Tapetu sa nepodarilo nastaviť Nastaviť ako tapetu - Nastavujem tapetu. Prosím, čakajte… + Nastavujem tapetu. Prosím, čakajte... Predvolený systém Tmavý Svetlý @@ -651,9 +651,9 @@ Mód limitovaného pripojenia Kvalitné obrázky Kvalitné obrázky sú diagramy a fotografie, ktoré spĺňajú určité štandardy (ktoré sú väčšinou technického charakteru) a sú cenné pre projekty Wikimédie - Pokračovanie nahrávania… - Pozastavovanie nahrávania… - Prerušovanie nahrávania… + Pokračovanie nahrávania... + Pozastavovanie nahrávania... + Prerušovanie nahrávania... Zrušiť nahrávanie Zapli ste mód limitovaného pripojenia. Všetky nahrávania budú teraz pozastavené a budú pokračovať až po vypnutí tohto módu. Mód limitovaného pripojenia je zapnutý. @@ -787,7 +787,7 @@ Iný problém alebo informácia (vysvetlite nižšie). Vaša spätná väzba sa zverejní na nasledujúcej wiki stránke: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Ste si istí, že chcete zrušiť všetky nahrávania? - Ruším všetky nahrávania… + Ruším všetky nahrávania... Nahrané súbory Čakajúce Zlyhané diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index b91c3c0b1..61531980f 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -6,7 +6,7 @@ * McDutchie * Upwinxp --> - + Facebook stran Zbirke Izvorna koda Zbirke v shrambi Github Logotip Zbirke @@ -66,8 +66,8 @@ %1$d nalaganj - Prejemam deljeno vsebino. Obdelava slike lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. - Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. + Prejemam deljeno vsebino. Obdelava slike lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. + Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. @@ -87,9 +87,9 @@ Ste pozabili geslo? Ustvari račun Prijavljanje - Prosimo, počakajte … + Prosimo, počakajte ... Posodabljam napise in opise - Prosimo, počakajte … + Prosimo, počakajte ... Uspešno ste se prijavili! Prijava ni uspela! Datoteka ni bila najdena. Prosimo, poskusite z drugo datoteko. @@ -133,7 +133,7 @@ Spremembe Naloži Poišči kategorije - Poiščite predmete, ki jih vaša predstavnostna datoteka prikazuje (gora, Tadž Mahal, …) + Poiščite predmete, ki jih vaša predstavnostna datoteka prikazuje (gora, Tadž Mahal, ...) Shrani Osveži Seznam @@ -159,7 +159,7 @@ Pošljite povratno informacijo (prek e-pošte) Nameščen ni noben e-poštni odjemalec Pred kratkim uporabljene kategorije - Čakam na prvo sinhronizacijo … + Čakam na prvo sinhronizacijo ... Naložili niste še nobene fotografije. Poskusi znova Prekliči @@ -199,7 +199,7 @@ Da! Več informacij Kategorije - Nalaganje … + Nalaganje ... Nič ni izbrano Ni napisa Ni opisa @@ -491,7 +491,7 @@ Ogled prebranih Ogled neprebranih Pri izbiri slik je prišlo do napake - Prosimo, počakajte … + Prosimo, počakajte ... Izbrane slike so slike izvrstnih fotografov in ilustratorjev, ki jih je skupnost Wikimedijine zbirke prepoznala kot najbolj kakovostne v tem projektu. Slike, naložene z Bližnjimi kraji, so slike, ki so naložene z odkrivanjem krajev na zemljevidu. Ta možnost vam omogoča, da urejevalcem, ki so opravili koristno urejanje, pošljete zahvalo – z uporabo kratke povezave na strani zgodovine ali strani primerjave. @@ -513,7 +513,7 @@ Dostop do lokacije predstavnosti zavrnjen Za slike, ki jih nalagate, ne moremo samodejno pridobiti lokacije. Pred pošiljanjem dodajte za vsako sliko ustrezno lokacijo. Nalagajte slike v Wikimedijino zbirko neposredno iz telefona. Prenesite aplikacijo Commons zdaj: %1$s - Deli aplikacijo prek … + Deli aplikacijo prek ... Informacije o sliki Ni najdenih kategorij Ni najdenih upodobitev @@ -569,7 +569,7 @@ Koordinat ni bilo mogoče pridobiti. Ni bilo mogoče pridobiti opisov. Uredi opise in napise - Deli slike prek … + Deli slike prek ... Ničesar še niste prispevali %s ni opravil_a še nobenega prispevka Račun ustvarjen! @@ -593,7 +593,7 @@ Dodano med zaznamke Nekaj je šlo narobe. Ozadja ni bilo mogoče nastaviti. Nastavi kot ozadje - Nastavljam ozadje. Prosimo, počakajte … + Nastavljam ozadje. Prosimo, počakajte ... Sledi sistemu Temna Svetla @@ -649,9 +649,9 @@ Način omejene povezanosti Kakovostne slike Kakovostne slike so ponazoritve ali fotografije, ki ustrezajo nekaterim merilom kakovosti (ta so predvsem tehnična) in so dragocene za projekte Wikimedie - Nalaganje se nadaljuje… - Zaustavljam nalaganje… - Preklicujem nalaganje… + Nalaganje se nadaljuje ... + Zaustavljam nalaganje ... + Preklicujem nalaganje ... Preklic nalaganja Vklopili ste način omejene povezanosti. Vsa nalaganja so začasno ustavljena in se bodo nadaljevala, ko boste ta način izklopili. Način omejene povezanosti je vklopljen. diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index f1e7412d4..40e99618f 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -34,39 +34,32 @@ Слика дана %1$d датотека се отпрема - %1$d датотеке се отпремају %1$d датотеке се отпремају %1$d отпремање - %1$d отпремања %1$d отпремања Покретање отпремања Процесуирање %d отпремање - Процесуирање %d отпремања Процесуирање %d отпремања %d отпремање - %1$d отпремања %d отпремања Слика ће се водити под лиценцом %1$s - Слике ће се водити под лиценцом %1$s Слике ће се водити под лиценцом %1$s %1$d отпремање - %1$d отпремања %1$d отпремања - Пријем %d дељеног садржаја… Процесуирање слике може потрајати неко време, у зависности од величине слике и вашег уређаја - Пријем %d дељеног садржаја… Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја - Пријем %d дељеног садржаја… Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја + Примање дељеног садржаја... Процесуирање слике може потрајати неко време, у зависности од величине слике и вашег уређаја + Примање дељеног садржаја... Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја Истрага Изглед @@ -503,7 +496,7 @@ Приступ локацији медија је одбијен Можда нећемо моћи да аутоматски прибавимо податке о локацији из слика које отпремите. Додајте одговарајућу локацију за сваку слику пре објављивања Отпреми фотографије на Викимедијину Оставу директно са свог телефона. Преузми апликацију Оставе сада: %1$s - Подели апликацију преко… + Подели апликацију преко... Информације о слици Нису пронађене категорије Отказано отпремање @@ -528,13 +521,12 @@ Успешно Категорија %1$s је додата. - Категорије %1$s су додате. Категорије %1$s су додате. Није могуће додати категорије. Ажурирај категорију Уреди приказе - Ажурирање координата… + Ажурирање координата... Ажурирање координата Ажурирање описа Ажурирање натписа @@ -737,7 +729,6 @@ Чување GPX датотеке %d слика је одабрана - %d слике су одабране %d слика је одабрано Унесите коментар @@ -746,4 +737,6 @@ Отпремања На чекању Није успело + Ово место већ има слику + Проверавање да ли ово место има слику. diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml index 64379ac92..79ae5ea28 100644 --- a/app/src/main/res/values-su/strings.xml +++ b/app/src/main/res/values-su/strings.xml @@ -26,25 +26,32 @@ Togel ka Luhur Gambar poé ieu + ngunjal %1$d berkas ngunjal %1$d berkas + (%1$d) (%1$d) Mitembeyan Ngamuat + Ngolah %d muatan Ngolah %d muatan + %1$d muatan %1$d muatan + Ieu gambar bakal dilisénsi %1$s Ieu gambar bakal dilisénsi %1$s + %1$d Dimuat %1$d Dimuat + Nampa kontén anu dibagikeun. Ngolah gambarna bisa jadi rada lila gumantung kana ukuran gambar jeung gaway anjeun Nampa kontén anu dibagikeun Langlang @@ -64,7 +71,7 @@ Asup log Tungguan… Nganyarkeun pertélaan jeung pedaran - Mangga tungguan… + Mangga tungguan... Laksana login! Gagal login! Berkas teu kapanggih. Coba berkas séjén. @@ -392,7 +399,7 @@ Tempo arsip Tempo nu can dibaca Éror pas keur nyomot gambar - Mangga tungguan… + Mangga tungguan... Iwalkeun ieu gambar Karya Hak cipta @@ -402,7 +409,7 @@ Nomer Seri Sopwér Muat poto ka Wikimedia Commons langsung tina ponsél. Unduh Commons App ayeuna: %1$s - Bagikeun app liwat… + Bagikeun app liwat... Info Gambar Euweuh Kategori kapanggih Muatan bedo diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index c49d64ad2..370bf0915 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -506,7 +506,7 @@ Åtkomst till mediaplats nekades Vi kanske inte automatiskt kan få platsdata från bilder du laddar upp. Lägg till lämplig plats för varje bild innan du skickar in Ladda upp foton till Wikimedia Commons direkt från din telefon. Ladda ned Commons-appen nu: %1$s - Dela appen via… + Dela appen via... Bildinfo Inga kategorier hittades Inga beskrivningar hittades @@ -783,7 +783,7 @@ Andra problem eller information (ange nedan). Din återkoppling kommer att skickas till följande wikisida: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobilapp/Återkoppling</a> Är du säker på att du vill avbryta alla uppladdningar? - Avbryter alla uppladdningar… + Avbryter alla uppladdningar... Uppladdningar Pågår Misslyckades diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 4f41da5f6..f9162bc7b 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -98,7 +98,7 @@ பின்னூட்டம் அனுப்பு (மின்னஞ்சல் வழியாக) மின்னஞ்சற் செயலி எதுவும் நிறுவப்படவில்லை அண்மையிற் பயன்படுத்தப்பட்ட பகுப்புகள் - முதல் ஒத்திசைவுக்காக காத்திருக்கிறது … + முதல் ஒத்திசைவுக்காக காத்திருக்கிறது ... நீர் இன்னும் எவ்வொளிப்படத்தையும் பதிவேற்றவில்லை. மீண்டும் முயல்க கைவிடு @@ -131,7 +131,7 @@ ஆம்! மேலதிக தகவல்கள் பகுப்புகள் - ஏற்றப்படுகிறது… + ஏற்றப்படுகிறது... தெரிவு செய்யப்படவில்லை தலைப்பு இல்லை விளக்கம் இல்லை diff --git a/app/src/main/res/values-tcy/strings.xml b/app/src/main/res/values-tcy/strings.xml index 13ee985b9..add46f7b7 100644 --- a/app/src/main/res/values-tcy/strings.xml +++ b/app/src/main/res/values-tcy/strings.xml @@ -110,7 +110,7 @@ ಇರೆನ ಅಬಿಪ್ರಾಯೊ ಬರೆಲೆ(ಮಿಂಚಂಚೆ). ಇರೆನ ಮಿಂಚಂಚೆ ಇಜ್ಜಿ. ಇಂಚಿಗ್ ಸೃಷ್ಟಿ ಮಾಲ್ತಿನ ವರ್ಗೊ. - ಒಂತೆ ಸಮಯ ಕಾಯೊಡು…. + ಒಂತೆ ಸಮಯ ಕಾಯೊಡು.... ಇರ್ ಒಂಜಿಲಾ ಪಟೋನ್ ಅಪ್ಲೋಡ್ ಮಾಲ್ತಿಜ್ಜಿ. ನನೊರ ಪ್ರಯತ್ನ ಮಾನ್ಪುಲೇ ವಜಾ ಮಲ್ಪುಲೆ @@ -336,7 +336,7 @@ ಅನುರಕ್ಷಿತ ತೂಲೆ ಓದಂದಿನ ತೂಲೆ ಆಕೃತಿಲೆನ್ ಪೆಜ್ಜಿನಗ ದೋಷ ಆಂಡ್ - ದಯಮಲ್ತ್ ಕಾಪುಲೆ… + ದಯಮಲ್ತ್ ಕಾಪುಲೆ... ಸಂಯೋಜನೆಲು ಸೂಚನೆಲು ನನಾತ್ diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index 0c478b221..ae80a5335 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -127,7 +127,7 @@ ఫీడుబ్యాకును పంపండి (ఈమెయిలు ద్వారా) ఈమెయిలు క్లయంటేదీ లేదు ఇటీవల వాడిన వర్గాలు - మొట్టమొదటి సింక్ కోసం చూస్తున్నాం… + మొట్టమొదటి సింక్ కోసం చూస్తున్నాం... ఇంకా మీరు ఫోటోలేమీ ఎక్కించలేదు. మళ్ళీ ప్రయత్నించు రద్దుచేయి @@ -457,7 +457,7 @@ క్రమ సంఖ్యలు సాఫ్టువేరు నేరుగా మీ ఫోను నుంచే వికీమీడియా కామన్స్‌కు ఫోటోలను ఎక్కించండి. కామన్స్ యాప్‌ను ఇప్పుడే దించుకోండి: %1$s - యాప్‌ను దీని ద్వారా పంచుకోండి… + యాప్‌ను దీని ద్వారా పంచుకోండి... బొమ్మ సమాచారం వర్గాలేమీ కనబడలేదు ఎక్కింపును రద్దు చేసాం @@ -523,7 +523,7 @@ బుక్‌మార్కులకు చేర్చాం ఏదో లోపం జరిగింది. వాల్‌పేపరును సెట్ చెయ్యలేకపోయాం వాల్‌పేపరుగా అమర్చు - వాల్‌పేపరుగా సెట్ చేస్తున్నాం. కాస్త ఆగండి… + వాల్‌పేపరుగా సెట్ చేస్తున్నాం. కాస్త ఆగండి... నల్లటి వెలుగుతో స్థానపు సెట్టింగులను తెరవడం విఫలమైంది. స్థానాన్ని మానవికంగా ఆన్ చెయ్యండి @@ -576,9 +576,9 @@ పరిమిత కనెక్షను మోడ్‌ను అచేతనం చేసాం. పెండింగులో ఉన్న ఎక్కింపులు తిరిగి మొదలౌతాయి. పరిమిత కనెక్షను మోడ్ నాణ్యమైన బొమ్మలు - ఎక్కింపును తిరిగి మొదలెడుతున్నాం… - ఎక్కింపును నిలుపుతున్నాం… - ఎక్కింపును రద్దు చేస్తున్నాం… + ఎక్కింపును తిరిగి మొదలెడుతున్నాం... + ఎక్కింపును నిలుపుతున్నాం... + ఎక్కింపును రద్దు చేస్తున్నాం... ఎక్కింపును రద్దుచెయ్యి మీరు పరిమిత కనెక్షను మోడ్‌ను చేతనం చేసారు. ఎక్కింపులన్నీ నిలిచిపోయాయి. మీరు ఈ మోడ్‌ను అచేతనం చెయ్యగానే అవి తిరిగి మొదలౌతాయి. పరిమిత కనెక్షను మోడ్ ఆన్ అయింది. diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 70bee59ef..125bba590 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -38,16 +38,21 @@ รูปภาพประจำวัน กำลังอัปโหลดไฟล์ %1$d ไฟล์ + \@string/contributions_subtitle_zero + (%1$d) (%1$d) กำลังเริ่มอัปโหลด + กำลังเริ่มอัปโหลด %1$d รายการ กำลังเริ่มอัปโหลด %1$d รายการ + การอัปโหลด %1$d รายการ การอัปโหลด %1$d รายการ + ภาพนี้จะอยู่ในสัญญาอนุญาต %1$s ภาะเหล่านี้จะอยู่อยู่ในสัญญาอนุญาติ %1$s สำรวจ @@ -393,7 +398,7 @@ รุ่นเลนส์ หมายเลขซีเรียล ซอฟต์แวร์ - แบ่งปันแอปผ่าน… + แบ่งปันแอปผ่าน... ไม่พบหมวดหมู่ ภาพเซลฟี ภาพเบลอ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 79482fb4e..31a9f0b53 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -210,7 +210,7 @@ Evet! Daha Fazla Bilgi Kategoriler - Yükleniyor… + Yükleniyor... Hiçbir şey seçilmedi Altyazı yok Açıklama yok @@ -505,7 +505,7 @@ Okunanları görüntüle Okunmayanları görüntüle Resimler seçilirken hata oluştu - Lütfen bekleyin… + Lütfen bekleyin... Seçkin resimler, Wikimedia Commons topluluğunun sitedeki en yüksek kaliteden bazıları olarak seçtiği son derece yetenekli fotoğrafçıların ve illüstratörlerin görüntüleridir. Yakındaki yerler üzerinden yüklenen resimler, haritadaki yerleri keşfederek yüklenen resimlerdir. Bu özellik, editörlerin, geçmiş sayfasında veya fark sayfasında küçük bir teşekkür bağlantısı kullanarak faydalı düzenlemeler yapan kullanıcılara bir Teşekkür bildirimi göndermesine olanak tanır. @@ -604,7 +604,7 @@ Yer işaretlerine eklendi Bir şeyler yanlış gitti. Duvar kağıdı ayarlanamadı Duvar kağıdı olarak ayarla - Duvar Kağıdı ayarlanıyor. Lütfen bekleyin… + Duvar Kağıdı ayarlanıyor. Lütfen bekleyin... Sistemi izle Koyu Açık diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index cc2343a77..9e821ae24 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -21,7 +21,7 @@ * Ата * Пан Хаунд --> - + Facebook-сторінка Вікісховища Програмний код Вікісховища на GitHub Логотип Вікісховища @@ -81,7 +81,7 @@ %1$d завантажень - Отримання спільного контенту. Обробка зображення може зайняти трохи часу, залежно від розміру зображення і від Вашого пристрою + Отримання спільного контенту. Обробка зображення може зайняти трохи часу, залежно від розміру зображення і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою @@ -612,7 +612,7 @@ Додано у закладки Щось трапилось. Не вдалося встановити шпалери робочого столу Встановити в якості шпалер робочого столу - Встановлення робочого столу. Будь ласка зачекайте… + Встановлення робочого столу. Будь ласка зачекайте... На взірець системи Темна Світла diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index f09f76ac6..a708c873a 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -73,9 +73,9 @@ Parolni unutdingizmi? Roʻyxatdan oʻtish Kirish - Iltimos kuting… + Iltimos kuting... Sarlavhalar va tavsiflarni yangilash - Iltimos, kutib turing… + Iltimos, kutib turing... Kirish muvaffaqiyatli bajarildi! Kirish muvaffaqiyatsiz yakunlandi! Fayl topilmadi. Iltimos, boshqa faylni izlab koʻring. @@ -180,7 +180,7 @@ Ha! Batafsil maʼlumot Turkumlar - Yuklanmoqda… + Yuklanmoqda... Tanlanmagan Izoh yoʻq Tavsif yoʻq @@ -390,7 +390,7 @@ Xatchoʻplar Xatchoʻplar Bajarildi - Iltimos, kuting… + Iltimos, kuting... EXIF teglarni boshqarish Muallif Mualliflik huquqlari diff --git a/app/src/main/res/values-vec/strings.xml b/app/src/main/res/values-vec/strings.xml index 52bb495ea..bbcb64561 100644 --- a/app/src/main/res/values-vec/strings.xml +++ b/app/src/main/res/values-vec/strings.xml @@ -68,7 +68,7 @@ Cargamento de %1$s no riusio Schicia par vixuałixare I me ultimi cargamenti - In coa… + In coa... Fałimento %1$d%% conpleto Drio cargar.. @@ -114,7 +114,7 @@ Mandane on comento (co ła mail) Nisun client de posta eletronega instałà Categorie doparà ultimamente - Speta par ła prima sincronixasion… + Speta par ła prima sincronixasion... No te ghe njiancora cargà na foto Riproa Descançełare @@ -403,7 +403,7 @@ Varda no lexeste Varda no lexeste Se ga vuo on eror co se jera drio ełexare łe imajini. - Speta on fià… + Speta on fià... Le foto in primo pian łe xé imajini de fotografi altamente cuałifegai che ła comunità de Wikimedia Commons ła ga ełeto come fotografi de alta cuałità sol sito. Imajini cargae via \"Posti cuà rente\", imajini che łe njien cargae scoerxendo posti n\'te ła mapa Sta funsion ła consente ai editori de enviar na notifega de ringrasiamento ai uxuari che i fa modifeghe che serve, doparando on lingambo picenin de ringrasiamento n\'te ła pajina del storego o n\'te ła pajina de łe difarense.\n\nQuesta funzione consente agli editor di inviare una notifica di ringraziamento agli utenti che apportano modifiche utili, utilizzando un piccolo link di ringraziamento nella pagina della cronologia o nella pagina delle differenze. @@ -421,7 +421,7 @@ Numari seriałi Software Carga foto so Wikimedia Commons diretamente dal to tełefonin. Descarga l\'aplicasion deso: %1$s - Spartisi aplicasion co… + Spartisi aplicasion co... Informasion so l\'imajine Nisuna categoria catada Cargamento nułà @@ -461,7 +461,7 @@ Xonta ai favorii Calcosa el xé ndà roerso. No xé sta pusibiłe canbiar el sfondo Inposta el sfondo - Drio inpostar el sfondo. Speta on fià… + Drio inpostar el sfondo. Speta on fià... Segui el sistema Scuro Ciaro diff --git a/app/src/main/res/values-xal/strings.xml b/app/src/main/res/values-xal/strings.xml index c36206061..346ff15e1 100644 --- a/app/src/main/res/values-xal/strings.xml +++ b/app/src/main/res/values-xal/strings.xml @@ -23,15 +23,15 @@ Вики-аһулх һазр Тохрллһ Вики-аһулх һазрур ацалх - Ацалгдҗана… + Ацалгдҗана... Кергләчин нерн Нууц үг Невтрх Нууц үгән мартвт? Бүрткүлх Невтрҗәнә - Күләхнтн… - Күләхнтн… + Күләхнтн... + Күләхнтн... Невтрлт амҗлтта болла! Невтрҗ чадсн уга! Ацаллт кеҗ экллә! @@ -83,7 +83,7 @@ Тиим Делгрңгү Нерн, төрл - Умшҗана… + Умшҗана... Алькинь чигн суңһад уга Тодрхаллт уга Күүндән уга diff --git a/app/src/main/res/values-xmf/strings.xml b/app/src/main/res/values-xmf/strings.xml index 7927da1af..b32eeb005 100644 --- a/app/src/main/res/values-xmf/strings.xml +++ b/app/src/main/res/values-xmf/strings.xml @@ -61,7 +61,7 @@ ვიკიოწკარუე პარამეტრეფი ვიკიოწკარუეშა ეხარგუა - ეთმიხარგუ… + ეთმიხარგუ... მახვარებუშ ჯოხო პაროლი გენშართით თქვანი პროფილით Commons Beta-შა @@ -71,7 +71,7 @@ სისტემაშა მიშულა ქორთხინთ ქჷმიცადით … მუკნაჭარეფი დო ეჭარუეფი მითმიახალებუ - ქორთხინთ ქჷმიცადით… + ქორთხინთ ქჷმიცადით... სისტემაშა მიშულაქ წჷმოძინელო გეთუ! სისტემაშა მიშულაქ ვემიხუჯინუ! ფაილქ ვეგორუ. ქორთხინთ, ქოცადით შხვა ფაილი. @@ -181,7 +181,7 @@ ქოǃ უმოსი ინფორმაცია კატეგორიეფი - იხარგუ… + იხარგუ... მუთუნ ვა რე გიშაგორილი მუკნაჭარა ვა რე ვა რე ეჭარუა diff --git a/app/src/main/res/values-zgh/strings.xml b/app/src/main/res/values-zgh/strings.xml index c6f27bb99..27080b999 100644 --- a/app/src/main/res/values-zgh/strings.xml +++ b/app/src/main/res/values-zgh/strings.xml @@ -13,7 +13,7 @@ ⵜⴻⵜⵜⵓⴷ ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵣⵔⴰⵢ? ⵣⵎⵎⴻⵎ ⴷⴰ ⵜⴽⵛⵛⵎⴷ - ⴰⵎⵓⵔ ⵏⵏⴽ ⵇⵇⵍ… + ⴰⵎⵓⵔ ⵏⵏⴽ ⵇⵇⵍ... ⴰⴽⵛⴰⵎ !ⵉⵎⵓⵔⵙ ⴰⴽⵛⴰⵎ ⵉⵣⴳⵍ! ⴰⴼⴰⵢⵍⵓ ⵓⵔ ⵉⵜⵜⵢⵓⴼⴰ. ⴰⵎⵓⵔ ⵏⵏⴽ ⴰⵔⵎ ⴰⴼⴰⵢⵍⵓ ⵢⴰⴹⵏ. diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2a307e955..2adabec26 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -79,22 +79,28 @@ 地点状态 今日图片 + %1$d个文件正在上传 %1$d个文件正在上传 + %1$d次上传 %1$d次上传 开始上传 + 正在处理%d个上传 正在处理%d个上传 + %d个上传 %d个上传 + 该图像的授权协议是 %1$s 这些图像的授权协议是 %1$s + %1$d次上传 %1$d次上传 @@ -546,7 +552,7 @@ 已拒绝访问媒体位置 我们可能无法自动从你上传的图片中获取位置数据。提交前请为每张图片添加适当的位置 直接在您手机上的维基共享资源应用中上传照片。立即下载共享资源应用:%1$s - 分享到… + 分享到... 图像信息 找不到分类 找不到描写。 @@ -572,6 +578,7 @@ 分类更新 成功 + 分类%1$s已添加。 分类%1$s已添加。 无法添加分类。 @@ -579,6 +586,7 @@ 正在尝试更新描述。 编辑描述 + 已添加 %1$s 个描写。 已添加 %1$s 个描写。 无法添加描述。 @@ -679,8 +687,8 @@ 限制连接模式 优良图片 品质图像是符合一定质量标准(本质上大多是技术性的)的图表或照片,它们对维基媒体计划很有价值 - 正在恢复上传… - 暂停上传… + 正在恢复上传... + 暂停上传... 正在取消上传… 取消上传 您已启用限制连接模式。所有的上传已暂停并将在您禁用此模式后立刻恢复。 @@ -808,6 +816,7 @@ 正在保存KML文件 正在保存GPX文件 + 已选择%d个图像 已选择%d个图像 请记住,每次多图片上传会为其中的所有图片标注相同的分类和描述。如果这些图片并不共享同样的描述和分类,请分别进行多次上传。 @@ -821,9 +830,12 @@ 其他问题或信息(请在下方解释)。 您的反馈已经发布在以下wiki页面:<a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> 您确定要取消所有上传吗? - 取消所有的上传… + 取消所有的上传... 上传 待处理 失败 无法加载地点数据 + 这个地点还没有照片,快去拍一张吧! + 这个地点已经有照片了。 + 现在检查这个地点是否有照片。 From 7c826502b676400a513bed7f64206701f45425d9 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 31 Oct 2024 13:01:42 +0100 Subject: [PATCH 16/74] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-krc/strings.xml | 12 +++++++++++- app/src/main/res/values-pa/strings.xml | 1 + app/src/main/res/values-vi/strings.xml | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index 55fa4ac35..eb9193e7f 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -473,6 +473,7 @@ Окъулмагъана хапарландырыуугъуз джокъду Окъулмагъан хапарландырыуугъуз барды Логларыгъызны хайырланыб юлюшлегиз + Электрон почтагъызны тинтигиз Окъулгъанны кёргюз Окъулмагъанланы кёргюз Суратла сайланнган заманда халат болду. @@ -686,7 +687,7 @@ Джууукъдагъы картала тюз ишлер ючюн ТЕЛЕФОННУ БОЛУМУн окъургъа амал болургъа кереклиди Хайырланыучуну къошумлары: %s Хайырланыучуну джетишимлери: %s - Хайырланыучу бетни кёргюз + Хайырланыучу профильни кёр Танытыуланы тюзет Категорияланы тюзет Кенгленнген Сайлаула @@ -774,4 +775,13 @@ \'%1$s\' - башха джерди. Тилейбиз, тюз джерни энишгерекде белгилегиз, эмда мадар бар эсе, тюз кенглик бла узунлукъну джазыгъыз. Башха проблема неда информация (тилейбиз, энишгерекде ангылатыгъыз). Сизни кери оюмугъуз тюбюндеги вики бетге джиберилликди: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> + Бютеу джюклеулени тохтатыргъа излегенигизге ишексизмисиз? + Бютеу джюклеулени тохтатыу... + Джюклеуле + Сакълауда + Джетишимсиз + Джерни юсюнден билгилени джюклеялмады + Бу джерни сураты джокъду, хайда бирин эт! + Бу джерни алайсыз да сураты барды. + Бу джерни сураты болуб-болмагъанын тинте турама. diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 8c64900a5..ebc535d50 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -207,5 +207,6 @@ ਲਿਖਤ ਛਾਪੋ ਮੁਹਰੈਲ ਵਰਤੋਂਕਾਰ + ਟਿਕਾਣਾ ਨਵਿਆਈਆ ਗਿਆ ਤੁਹਾਡੇ ਦਾਖਲੇ ਦੀ ਮਿਆਦ ਪੁੱਗ ਗਈ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਦਾਖਲ ਹੋਵੋ। diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 224492ab7..635d71a3f 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,6 +1,7 @@ + + + + app:layout_constraintStart_toEndOf="@id/image_limit_error" + app:srcCompat="@drawable/ic_overflow" /> + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_custom_selector.xml b/app/src/main/res/menu/menu_custom_selector.xml new file mode 100644 index 000000000..fc432439e --- /dev/null +++ b/app/src/main/res/menu/menu_custom_selector.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f9de8e051..187c5fc96 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -117,6 +117,7 @@ Search categories Search for items that your media depicts (mountain, Taj Mahal, etc.) Save + Overflow menu Refresh List (No uploads yet) @@ -832,6 +833,17 @@ Upload your first media by tapping on the add button. Pending Failed Could not load place data + + Delete Folder + Confirm Deletion + Are you sure you want to delete folder %1$s containing %2$d items? + Delete + Cancel + Folder %1$s deleted successfully + Failed to delete folder %1$s + Error in trashing folder contents: %1$s + Failed to retrieve folder path for bucket ID: %1$d + This place has no picture yet, go take one! This place has a picture already. Now checking whether this place has a picture. diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 8ac890545..5d2dabb59 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -51,6 +51,13 @@ android:summary="@string/display_campaigns_explanation" android:title="@string/display_campaigns" /> + + Date: Thu, 14 Nov 2024 13:35:05 +1100 Subject: [PATCH 25/74] Improve Unique File Name Search (#5877) * Modified findUniqueFileName() in UploadWorker.kt to use a random 3-digit alphanumeric hash to append to a file name to make it unique. This improves speed over using an incrementing number to append as there are fewer collisions. * Modified findUniqueFileName() in UploadWorker.kt to use a random 5-digit numeric hash rather than the previous 3-digit alphanumeric hash * Removed unnecessary variable "chars" --------- Co-authored-by: Jinniu Du <127721018+Donutcheese@users.noreply.github.com> Co-authored-by: Zihan Pan Co-authored-by: Nicolas Raoul --- .../nrw/commons/upload/worker/UploadWorker.kt | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index 144c503bb..00cd29a6d 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Date +import java.util.Random import java.util.regex.Pattern import javax.inject.Inject @@ -548,33 +549,30 @@ class UploadWorker( } private fun findUniqueFileName(fileName: String): String { - var sequenceFileName: String? - var sequenceNumber = 1 - while (true) { + var sequenceFileName: String? = fileName + val random = Random() + + // Loops until sequenceFileName does not match any existing file names + while (mediaClient + .checkPageExistsUsingTitle( + String.format( + "File:%s", + sequenceFileName, + ), + ).blockingGet()) { + + // Generate a random 5-character alphanumeric string + val randomHash = (random.nextInt(90000) + 10000).toString() + sequenceFileName = - if (sequenceNumber == 1) { - fileName + if (fileName.indexOf('.') == -1) { + "$fileName #$randomHash" } else { - if (fileName.indexOf('.') == -1) { - "$fileName $sequenceNumber" - } else { - val regex = - Pattern.compile("^(.*)(\\..+?)$") - val regexMatcher = regex.matcher(fileName) - regexMatcher.replaceAll("$1 $sequenceNumber$2") - } + val regex = + Pattern.compile("^(.*)(\\..+?)$") + val regexMatcher = regex.matcher(fileName) + regexMatcher.replaceAll("$1 #$randomHash") } - if (!mediaClient - .checkPageExistsUsingTitle( - String.format( - "File:%s", - sequenceFileName, - ), - ).blockingGet() - ) { - break - } - sequenceNumber++ } return sequenceFileName!! } From 248c7b0ceb0074d1761d099fb0bd99fd297da177 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 14 Nov 2024 13:02:04 +0100 Subject: [PATCH 26/74] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ar/strings.xml | 10 ++++ app/src/main/res/values-cs/strings.xml | 62 +++++++++++++++++++------ app/src/main/res/values-da/strings.xml | 10 ++++ app/src/main/res/values-fr/strings.xml | 10 ++++ app/src/main/res/values-hi/strings.xml | 3 ++ app/src/main/res/values-it/strings.xml | 5 ++ app/src/main/res/values-ko/strings.xml | 9 ++++ app/src/main/res/values-lb/strings.xml | 3 ++ app/src/main/res/values-pa/strings.xml | 1 + app/src/main/res/values-pms/strings.xml | 9 ++++ app/src/main/res/values-sv/strings.xml | 4 ++ 11 files changed, 112 insertions(+), 14 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index e1f29eb2d..acb7cd122 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -171,6 +171,7 @@ تصنيفات البحث (.البحث عن العناصر التي تصورها وسائطك (جبل ،تاج محل، إلخ حفظ + القائمة الزائدة أنعش القائمة (لا يوجد تحميلات حتى الآن) @@ -850,6 +851,15 @@ قيد الانتظار فشل تعذر تحميل بيانات المكان + حذف المجلد + تأكيد الحذف + هل أنت متأكد أنك تريد حذف المجلد %1$s الذي يحتوي على %2$d عنصرا؟ + حذف + إلغاء + تم حذف المجلد %1$s بنجاح + فشل حذف المجلد %1$s + خطأ في نقل محتويات المجلد إلى سلة المهملات: %1$s + فشل استرداد مسار المجلد لمعرف الدلو: %1$d هذا المكان ليس له صورة بعد، اذهب والتقط واحدة! هذا المكان لديه صورة بالفعل. الآن التحقق ما إذا كان هذا المكان لديه صورة. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 4d49ee632..5ec23c385 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -5,6 +5,7 @@ * Chmee2 * Clon * Dvorapa +* Fjuro * Frettie * Georg101 * Ilimanaq29 @@ -28,31 +29,58 @@ Zdrojový kód Commons na GitHubu Logo Wikimedia Commons Stránka Commons + Ukončit výběr polohy Odeslat + Přidat další popis + Přidat nový příspěvek + Přidat příspěvek z fotoaparátu + Přidat příspěvek z fotek + Přidat příspěvek z galerie předchozích příspěvků + Titulky + Popis jazyka + Titulek + Popis + Obrázek + Vše + Přepnout nahoru + Zobrazení vyhledávání + Stát místa Obrázek dne - - %1$d soubor se nahrává - %1$d souborů se nahrává + + Nahrávání %1$d souboru + Nahrávání %1$d souborů + Nahrávání %1$d souborů + Nahrávání %1$d souborů - - \@string/contributions_subtitle_zero + (%1$d) + (%1$d) + (%1$d) (%1$d) - - Spouští se nahrávání %1$d souboru - Spouští se nahrávání %1$d souborů + Spouštění nahrávání + + Zpracovávání %d nahrání + Zpracovávání %d nahrání + Zpracovávání %d nahrání + Zpracovávání %d nahrání - - %1$d nahrávání - %1$d nahrávání + + %d nahrávání + %d nahrávání + %d nahrávání + %d nahrávání - + Tento obrázek bude zveřejněn pod licencí %1$s + Tyto obrázky budou zveřejněny pod licencí %1$s + Tyto obrázky budou zveřejněny pod licencí %1$s Tyto obrázky budou zveřejněny pod licencí %1$s - + %1$d nahrání + %1$d nahrání + %1$d nahrání %1$d nahrání @@ -67,6 +95,7 @@ Commons Nastavení Nahrát na Commons + Probíhá nahrávání Uživatelské jméno Heslo Přihlásit se do svého Commons beta účtu @@ -75,7 +104,9 @@ Zaregistrovat se Přihlášení Čekejte prosím… - Přihlášení uspělo! + Nahrávání titulků a popisů + Čekejte prosím… + Úspěšně přihlášeni! Přihlášení se nezdařilo! Soubor nebyl nalezen. Prosím, zkuste jiný soubor. Ověření se nezdařilo, prosím přihlaste se znovu @@ -488,7 +519,10 @@ Při přihlášení nastala chyba, musíte si resetovat vaše heslo! Místo v okolí nalezeno Je toto fotka místa %1$s? + Záložky Nastavení + Odebráno ze záložek + Přidáno do záložek Něco se pokazilo. Tapetu se nepodařilo nastavit Nastavit jako tapetu Nastavování tapety. Prosím, čekejte… diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 3b6822c47..2b5dcc914 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -125,6 +125,7 @@ Søg kategorier Søg efter genstande, som dine medier afbilder (bjerg, Taj Mahal osv.) Gem + Overløbsmenu Opdater Liste (Ingen uploads endnu) @@ -789,6 +790,15 @@ Afventer Mislykkedes Kunne ikke indlæse steddata + Slet mappe + Bekræft sletning + Er du sikker på, at du vil slette mappen %1$s, der indeholder %2$d elementer? + Slet + Annuller + Mappen %1$s blev slettet + Kunne ikke slette mappen %1$s + Fejl ved sletning af mappeindhold: %1$s + Kunne ikke hente mappestien til bucket-id: %1$d Dette sted har endnu ikke noget billede, så gå hen og tag et! Dette sted har allerede et billede. Tjekker nu, om dette sted har et billede. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ae4dfb966..ecb27df91 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -161,6 +161,7 @@ Rechercher des catégories Rechercher les éléments que votre média représente (montagne, Taj Mahal, etc.). Enregistrer + Menu de débordement Actualiser Lister (Pas encore de téléversement) @@ -827,6 +828,15 @@ En attente Échec Les données du lieu n\'ont pas pu être chargées + Supprimer le dossier + Confirmer la suppression + Êtes-vous sûr de vouloir supprimer le dossier %1$s contenant %2$d éléments ? + Supprimer + Annuler + Le dossier %1$s a été supprimé avec succès + Impossible de supprimer le dossier %1$s + Erreur lors de la suppression du contenu du dossier : %1$s + Échec de la récupération du chemin d\'accès au dossier pour le bucket ID : %1$d Cet endroit n\'a pas encore de photo, allez en prendre une ! Cet endroit a déjà une photo. Je vérifie maintenant si cet endroit a une photo. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 50a04319b..55a378eaf 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -354,4 +354,7 @@ अपलोड लंबित विफल हुआ + फ़ोल्डर हटाएँ + हटाएँ + रद्द करें diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f40863870..538f6c884 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -760,6 +760,11 @@ %d immagine selezionata %d immagini selezionate + Cancella cartella + Conferma cancellazione + Cancella + Annulla + Cartella %1$s cancellata correttamente Questo posto non ha ancora una foto, scattane una! Questo posto ha già una foto. Ora controlliamo se questo posto ha una foto. diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index aa7ae98e7..1eaa3594c 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -129,6 +129,7 @@ 분류 검색 미디어가 서술한 항목을 검색하세요. (산, 타지마할 등) 저장 + 오버플로 메뉴 새로 고침 목록 (아직 올린 항목이 없음) @@ -686,6 +687,14 @@ 보류 중 실패 장소 데이터를 불러오지 못했습니다 + 폴더 삭제 + 삭제 확인 + 항목 %2$d개를 포함하는 %1$s 폴더를 삭제하시겠습니까? + 삭제 + 취소 + %1$s 폴더를 성공적으로 삭제했습니다 + %1$s 폴더를 삭제하지 못했습니다 + 버킷 ID의 폴더 경로를 검색하지 못했습니다: %1$d 이 장소에 아직 사진이 없습니다. 사진을 찍어보세요! 이 장소에 이미 사진이 있습니다. 지금 이 장소에 사진이 있는지 확인 중입니다. diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index aa6f8e58d..f80784c56 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -522,4 +522,7 @@ %d Biller ausgewielt \'%1$s\' gëtt et net méi, et kann keng Foto méi dovunner gemaach ginn. + Läsche confirméieren + Läschen + Ofbriechen diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index ef9ed130d..7e750d175 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -191,6 +191,7 @@ ਪ੍ਰਾਪਤੀਆਂ ਅੰਕੜੇ ਧੰਨਵਾਦ ਪ੍ਰਾਪਤ ਹੋਏ + ਆਪਣੀਆਂ ਪ੍ਰਾਪਤੀਆਂ ਨੂੰ ਆਪਣੇ ਦੋਸਤਾਂ ਨਾਲ ਸਾਂਝਾ ਕਰੋ! ਸੂਚਨਾਵਾਂ (ਪੜ੍ਹਿਆਂ) ਸੂਚੀ ਅੱਗੇ diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index bfbd64413..7ac8c92ea 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -117,6 +117,7 @@ Sërché dle categorìe Arserché j\'element che sò mojen a arpresenta (montagna, Taj Mahal, e via fòrt) Argistré + Mnu dë strabordament Agiorné Lista (Ancor gnun cariament) @@ -781,6 +782,14 @@ An atèisa Falì Impossìbil carié ij dàit dël pòst + Eliminé ëd dossié + Confirmé l\'eliminassion + É-lo sigur ëd vorèj eliminé ël dossié %1$s ch\'a content %2$d element? + Eliminé + Anulé + Ël dossié %1$s a l\'é stàit eliminà për da bin + Impossìbil eliminé ël dossié %1$s + Eror durant l\'eliminassion dël contnù dël dossié: %1$s Ës pòst a l\'ha ancor gnun-e fòto, ch\'a na pija un-a! Ës pòst a l\'ha già dle fòto. An camin ch\'as verìfica si cost pòst -sì a l\'ha dle fòto. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 370bf0915..646fb34c0 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -481,6 +481,7 @@ Du har inga olästa aviseringar Du har inga lästa aviseringar Dela loggar med hjälp av + Kontrollera inkorgen för din e-post Visa lästa Visa olästa Fel uppstod när bilder valdes ut @@ -788,4 +789,7 @@ Pågår Misslyckades Kunde inte läsa in platsdata + Det här platsen har ännu ingen bild. Gå och ta en! + Det här platsen har redan en bild. + Kollar nu om den här platsen har en bild. From 5c8c4032e969d5ba8699ff0c21d1e7bb60ebf890 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Sun, 17 Nov 2024 19:05:25 +0530 Subject: [PATCH 27/74] Migrated util module files from java to kotlin (#5935) * Rename .java to .kt * Migrated the following files in util module to Kotlin - AbstractTextWatcher - ActivityUtils - CommonsDateUtil - DateUtil * Rename .java to .kt * Migrated the following files in util module to Kotlin - DeviceInfoUtil - ExecutorUtils - FragmentUtils --- .../nrw/commons/campaigns/CampaignView.java | 4 +- .../commons/campaigns/CampaignsPresenter.java | 2 +- .../nrw/commons/delete/ReasonBuilder.java | 1 - .../nrw/commons/explore/ExploreFragment.java | 1 - .../commons/media/MediaDetailFragment.java | 1 - .../upload/UploadMediaDetailAdapter.java | 1 - .../UploadMediaDetailFragment.java | 4 +- .../commons/utils/AbstractTextWatcher.java | 31 ------- .../nrw/commons/utils/AbstractTextWatcher.kt | 25 +++++ .../free/nrw/commons/utils/ActivityUtils.java | 15 --- .../free/nrw/commons/utils/ActivityUtils.kt | 16 ++++ .../nrw/commons/utils/CommonsDateUtil.java | 44 --------- .../free/nrw/commons/utils/CommonsDateUtil.kt | 46 ++++++++++ .../fr/free/nrw/commons/utils/DateUtil.java | 53 ----------- .../fr/free/nrw/commons/utils/DateUtil.kt | 62 +++++++++++++ .../nrw/commons/utils/DeviceInfoUtil.java | 91 ------------------- .../free/nrw/commons/utils/DeviceInfoUtil.kt | 80 ++++++++++++++++ .../free/nrw/commons/utils/ExecutorUtils.java | 31 ------- .../free/nrw/commons/utils/ExecutorUtils.kt | 33 +++++++ .../free/nrw/commons/utils/FragmentUtils.java | 15 --- .../free/nrw/commons/utils/FragmentUtils.kt | 20 ++++ .../model/notifications/Notification.java | 2 +- 22 files changed, 288 insertions(+), 290 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/DateUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/DateUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.kt diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java index 4d1eb33ce..d1ee4c8b0 100644 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java @@ -12,15 +12,15 @@ import androidx.annotation.Nullable; import fr.free.nrw.commons.campaigns.models.Campaign; import fr.free.nrw.commons.databinding.LayoutCampaginBinding; import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.DateUtil; +import fr.free.nrw.commons.utils.CommonsDateUtil; +import fr.free.nrw.commons.utils.DateUtil; import java.text.ParseException; import java.util.Date; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.utils.CommonsDateUtil; import fr.free.nrw.commons.utils.SwipableCardView; import fr.free.nrw.commons.utils.ViewUtil; 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 51c841451..157047774 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 @@ -3,6 +3,7 @@ package fr.free.nrw.commons.campaigns; import android.annotation.SuppressLint; import fr.free.nrw.commons.campaigns.models.Campaign; +import fr.free.nrw.commons.utils.CommonsDateUtil; import java.text.ParseException; import java.util.Collections; import java.util.Date; @@ -14,7 +15,6 @@ import javax.inject.Singleton; import fr.free.nrw.commons.BasePresenter; import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import fr.free.nrw.commons.utils.CommonsDateUtil; import io.reactivex.Scheduler; import io.reactivex.Single; import io.reactivex.SingleObserver; diff --git a/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java b/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java index 35d682248..7912375a4 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java @@ -3,7 +3,6 @@ package fr.free.nrw.commons.delete; import android.content.Context; import fr.free.nrw.commons.utils.DateUtil; - import java.util.Date; import java.util.Locale; diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java index 26c8dd82b..d444148d4 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java @@ -11,7 +11,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.viewpager.widget.ViewPager.OnPageChangeListener; -import com.google.android.material.tabs.TabLayout; import fr.free.nrw.commons.R; import fr.free.nrw.commons.ViewPagerAdapter; import fr.free.nrw.commons.contributions.MainActivity; 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 ee905a5c5..edfa874fc 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 @@ -55,7 +55,6 @@ import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.actions.ThanksClient; import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.auth.csrf.CsrfTokenClient; import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; import fr.free.nrw.commons.category.CategoryClient; import fr.free.nrw.commons.category.CategoryDetailsActivity; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java index 6fc8b3266..a1a639a59 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java @@ -22,7 +22,6 @@ import android.widget.ListView; import android.widget.TextView; import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; 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 index 5581cfeb1..fb836445a 100644 --- 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 @@ -1,7 +1,6 @@ package fr.free.nrw.commons.upload.mediaDetails; import static android.app.Activity.RESULT_OK; -import static fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags; import android.annotation.SuppressLint; import android.app.Activity; @@ -45,6 +44,7 @@ import fr.free.nrw.commons.upload.UploadBaseFragment; import fr.free.nrw.commons.upload.UploadItem; import fr.free.nrw.commons.upload.UploadMediaDetail; import fr.free.nrw.commons.upload.UploadMediaDetailAdapter; +import fr.free.nrw.commons.utils.ActivityUtils; import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.NetworkUtils; @@ -208,7 +208,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements try { if(!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, getActivity())) { - startActivityWithFlags( + ActivityUtils.startActivityWithFlags( getActivity(), MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP); } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java deleted file mode 100644 index d5188027d..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java +++ /dev/null @@ -1,31 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.text.Editable; -import android.text.TextWatcher; - -import androidx.annotation.NonNull; - -public class AbstractTextWatcher implements TextWatcher { - private final TextChange textChange; - - public AbstractTextWatcher(@NonNull TextChange textChange) { - this.textChange = textChange; - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - textChange.onTextChanged(s.toString()); - } - - @Override - public void afterTextChanged(Editable s) { - } - - public interface TextChange { - void onTextChanged(String value); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt new file mode 100644 index 000000000..dd06452f9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.utils + +import android.text.Editable +import android.text.TextWatcher + +class AbstractTextWatcher( + private val textChange: TextChange +) : TextWatcher { + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + // No-op + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + textChange.onTextChanged(s.toString()) + } + + override fun afterTextChanged(s: Editable?) { + // No-op + } + + interface TextChange { + fun onTextChanged(value: String) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.java deleted file mode 100644 index 4806585dc..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.java +++ /dev/null @@ -1,15 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; -import android.content.Intent; - -public class ActivityUtils { - - public static void startActivityWithFlags(Context context, Class cls, int... flags) { - Intent intent = new Intent(context, cls); - for (int flag : flags) { - intent.addFlags(flag); - } - context.startActivity(intent); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.kt new file mode 100644 index 000000000..899daaf6b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.kt @@ -0,0 +1,16 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.content.Intent + +object ActivityUtils { + + @JvmStatic + fun startActivityWithFlags(context: Context, cls: Class, vararg flags: Int) { + val intent = Intent(context, cls) + for (flag in flags) { + intent.addFlags(flag) + } + context.startActivity(intent) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.java deleted file mode 100644 index 39ddca683..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.java +++ /dev/null @@ -1,44 +0,0 @@ -package fr.free.nrw.commons.utils; - -import java.text.SimpleDateFormat; -import java.util.Locale; -import java.util.TimeZone; - -/** - * Provides util functions for formatting date time - * Most of our formatting needs are addressed by the data library's DateUtil class - * Methods should be added here only if DateUtil class doesn't provide for it already - */ -public class CommonsDateUtil { - - /** - * Gets SimpleDateFormat for short date pattern - * @return simpledateformat - */ - public static SimpleDateFormat getIso8601DateFormatShort() { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ROOT); - simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - return simpleDateFormat; - } - - /** - * Gets SimpleDateFormat for date pattern returned by Media object - * @return simpledateformat - */ - public static SimpleDateFormat getMediaSimpleDateFormat() { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT); - simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - return simpleDateFormat; - } - - /** - * Gets the timestamp pattern for a date - * @return timestamp - */ - public static SimpleDateFormat getIso8601DateFormatTimestamp() { - final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", - Locale.ROOT); - simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - return simpleDateFormat; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.kt new file mode 100644 index 000000000..c076e19ce --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.kt @@ -0,0 +1,46 @@ +package fr.free.nrw.commons.utils + +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +/** + * Provides util functions for formatting date time. + * Most of our formatting needs are addressed by the data library's DateUtil class. + * Methods should be added here only if DateUtil class doesn't provide for it already. + */ +object CommonsDateUtil { + + /** + * Gets SimpleDateFormat for short date pattern. + * @return simpleDateFormat + */ + @JvmStatic + fun getIso8601DateFormatShort(): SimpleDateFormat { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) + simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC") + return simpleDateFormat + } + + /** + * Gets SimpleDateFormat for date pattern returned by Media object. + * @return simpleDateFormat + */ + @JvmStatic + fun getMediaSimpleDateFormat(): SimpleDateFormat { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT) + simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC") + return simpleDateFormat + } + + /** + * Gets the timestamp pattern for a date. + * @return timestamp + */ + @JvmStatic + fun getIso8601DateFormatTimestamp(): SimpleDateFormat { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT) + simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC") + return simpleDateFormat + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DateUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/DateUtil.java deleted file mode 100644 index 1d2a8fdf7..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/DateUtil.java +++ /dev/null @@ -1,53 +0,0 @@ -package fr.free.nrw.commons.utils; - -import static android.text.format.DateFormat.getBestDateTimePattern; - -import androidx.annotation.NonNull; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.TimeZone; - -public final class DateUtil { - private static Map DATE_FORMATS = new HashMap<>(); - - // TODO: Switch to DateTimeFormatter when minSdk = 26. - - public static synchronized String iso8601DateFormat(Date date) { - return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).format(date); - } - - public static synchronized Date iso8601DateParse(String date) throws ParseException { - return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).parse(date); - } - - public static String getMonthOnlyDateString(@NonNull Date date) { - return getDateStringWithSkeletonPattern(date, "MMMM d"); - } - - public static String getExtraShortDateString(@NonNull Date date) { - return getDateStringWithSkeletonPattern(date, "MMM d"); - } - - public static synchronized String getDateStringWithSkeletonPattern(@NonNull Date date, @NonNull String pattern) { - return getCachedDateFormat(getBestDateTimePattern(Locale.getDefault(), pattern), Locale.getDefault(), false).format(date); - } - - private static SimpleDateFormat getCachedDateFormat(String pattern, Locale locale, boolean utc) { - if (!DATE_FORMATS.containsKey(pattern)) { - SimpleDateFormat df = new SimpleDateFormat(pattern, locale); - if (utc) { - df.setTimeZone(TimeZone.getTimeZone("UTC")); - } - DATE_FORMATS.put(pattern, df); - } - return DATE_FORMATS.get(pattern); - } - - private DateUtil() { - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DateUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/DateUtil.kt new file mode 100644 index 000000000..bc33a1ede --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/DateUtil.kt @@ -0,0 +1,62 @@ +package fr.free.nrw.commons.utils + +import android.text.format.DateFormat.getBestDateTimePattern +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.HashMap +import java.util.Locale +import java.util.TimeZone + +/** + * Utility class for date formatting and parsing. + * TODO: Switch to DateTimeFormatter when minSdk = 26. + */ +object DateUtil { + + private val DATE_FORMATS: MutableMap = HashMap() + + @JvmStatic + @Synchronized + fun iso8601DateFormat(date: Date): String { + return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).format(date) + } + + @JvmStatic + @Synchronized + @Throws(ParseException::class) + fun iso8601DateParse(date: String): Date { + return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).parse(date) + } + + @JvmStatic + fun getMonthOnlyDateString(date: Date): String { + return getDateStringWithSkeletonPattern(date, "MMMM d") + } + + @JvmStatic + fun getExtraShortDateString(date: Date): String { + return getDateStringWithSkeletonPattern(date, "MMM d") + } + + @JvmStatic + @Synchronized + fun getDateStringWithSkeletonPattern(date: Date, pattern: String): String { + return getCachedDateFormat( + getBestDateTimePattern(Locale.getDefault(), pattern), + Locale.getDefault(), false + ).format(date) + } + + @JvmStatic + private fun getCachedDateFormat(pattern: String, locale: Locale, utc: Boolean): SimpleDateFormat { + if (!DATE_FORMATS.containsKey(pattern)) { + val df = SimpleDateFormat(pattern, locale) + if (utc) { + df.timeZone = TimeZone.getTimeZone("UTC") + } + DATE_FORMATS[pattern] = df + } + return DATE_FORMATS[pattern]!! + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.java deleted file mode 100644 index 5e01cc606..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.java +++ /dev/null @@ -1,91 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; -import android.os.Build; - -import java.util.HashMap; -import java.util.Map; - -import fr.free.nrw.commons.utils.model.ConnectionType; -import fr.free.nrw.commons.utils.model.NetworkConnectionType; - -import static fr.free.nrw.commons.utils.model.ConnectionType.CELLULAR; -import static fr.free.nrw.commons.utils.model.ConnectionType.CELLULAR_3G; -import static fr.free.nrw.commons.utils.model.ConnectionType.CELLULAR_4G; -import static fr.free.nrw.commons.utils.model.ConnectionType.NO_INTERNET; -import static fr.free.nrw.commons.utils.model.ConnectionType.WIFI_NETWORK; -import static fr.free.nrw.commons.utils.model.NetworkConnectionType.FOUR_G; -import static fr.free.nrw.commons.utils.model.NetworkConnectionType.THREE_G; -import static fr.free.nrw.commons.utils.model.NetworkConnectionType.TWO_G; -import static fr.free.nrw.commons.utils.model.NetworkConnectionType.UNKNOWN; -import static fr.free.nrw.commons.utils.model.NetworkConnectionType.WIFI; - -/** - * Util class to get any information about the user's device - * Ensure that any sensitive information like IMEI is not fetched/shared without user's consent - */ -public class DeviceInfoUtil { - private static final Map TYPE_MAPPING = new HashMap<>(); - - static { - TYPE_MAPPING.put(TWO_G, CELLULAR); - TYPE_MAPPING.put(THREE_G, CELLULAR_3G); - TYPE_MAPPING.put(FOUR_G, CELLULAR_4G); - TYPE_MAPPING.put(WIFI, WIFI_NETWORK); - TYPE_MAPPING.put(UNKNOWN, CELLULAR); - } - - /** - * Get network connection type - * @param context - * @return wifi/cellular-4g/cellular-3g/cellular-2g/no-internet - */ - public static ConnectionType getConnectionType(Context context) { - if (!NetworkUtils.isInternetConnectionEstablished(context)) { - return NO_INTERNET; - } - NetworkConnectionType networkType = NetworkUtils.getNetworkType(context); - ConnectionType deviceNetworkType = TYPE_MAPPING.get(networkType); - return deviceNetworkType == null ? CELLULAR : deviceNetworkType; - } - - /** - * Get Device manufacturer - * @return - */ - public static String getDeviceManufacturer() { - return Build.MANUFACTURER; - } - - /** - * Get Device model name - * @return - */ - public static String getDeviceModel() { - return Build.MODEL; - } - - /** - * Get Android version. Eg. 4.4.2 - * @return - */ - public static String getAndroidVersion() { - return Build.VERSION.RELEASE; - } - - /** - * Get API Level. Eg. 26 - * @return - */ - public static String getAPILevel() { - return Build.VERSION.SDK; - } - - /** - * Get Device. - * @return - */ - public static String getDevice() { - return Build.DEVICE; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.kt new file mode 100644 index 000000000..05d71c7e1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.kt @@ -0,0 +1,80 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.os.Build +import fr.free.nrw.commons.utils.model.ConnectionType +import fr.free.nrw.commons.utils.model.NetworkConnectionType + +/** + * Util class to get any information about the user's device + * Ensure that any sensitive information like IMEI is not fetched/shared without user's consent + */ +object DeviceInfoUtil { + private val TYPE_MAPPING = mapOf( + NetworkConnectionType.TWO_G to ConnectionType.CELLULAR, + NetworkConnectionType.THREE_G to ConnectionType.CELLULAR_3G, + NetworkConnectionType.FOUR_G to ConnectionType.CELLULAR_4G, + NetworkConnectionType.WIFI to ConnectionType.WIFI_NETWORK, + NetworkConnectionType.UNKNOWN to ConnectionType.CELLULAR + ) + + /** + * Get network connection type + * @param context + * @return wifi/cellular-4g/cellular-3g/cellular-2g/no-internet + */ + @JvmStatic + fun getConnectionType(context: Context): ConnectionType { + return if (!NetworkUtils.isInternetConnectionEstablished(context)) { + ConnectionType.NO_INTERNET + } else { + val networkType = NetworkUtils.getNetworkType(context) + TYPE_MAPPING[networkType] ?: ConnectionType.CELLULAR + } + } + + /** + * Get Device manufacturer + * @return + */ + @JvmStatic + fun getDeviceManufacturer(): String { + return Build.MANUFACTURER + } + + /** + * Get Device model name + * @return + */ + @JvmStatic + fun getDeviceModel(): String { + return Build.MODEL + } + + /** + * Get Android version. Eg. 4.4.2 + * @return + */ + @JvmStatic + fun getAndroidVersion(): String { + return Build.VERSION.RELEASE + } + + /** + * Get API Level. Eg. 26 + * @return + */ + @JvmStatic + fun getAPILevel(): String { + return Build.VERSION.SDK + } + + /** + * Get Device. + * @return + */ + @JvmStatic + fun getDevice(): String { + return Build.DEVICE + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java deleted file mode 100644 index 889b31f2d..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java +++ /dev/null @@ -1,31 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.os.Handler; -import android.os.Looper; - -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class ExecutorUtils { - - private static final Executor uiExecutor = command -> { - if (Looper.myLooper() == Looper.getMainLooper()) { - command.run(); - } else { - new Handler(Looper.getMainLooper()).post(command); - } - }; - - public static Executor uiExecutor() { - return uiExecutor; - } - - - private static final ExecutorService executor = Executors.newFixedThreadPool(3); - - public static ExecutorService get() { - return executor; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.kt new file mode 100644 index 000000000..981b19355 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.kt @@ -0,0 +1,33 @@ +package fr.free.nrw.commons.utils + +import android.os.Handler +import android.os.Looper + +import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +object ExecutorUtils { + + @JvmStatic + private val uiExecutor: Executor = Executor { command -> + if (Looper.myLooper() == Looper.getMainLooper()) { + command.run() + } else { + Handler(Looper.getMainLooper()).post(command) + } + } + + @JvmStatic + fun uiExecutor(): Executor { + return uiExecutor + } + + @JvmStatic + private val executor: ExecutorService = Executors.newFixedThreadPool(3) + + @JvmStatic + fun get(): ExecutorService { + return executor + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java deleted file mode 100644 index a01ff9251..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java +++ /dev/null @@ -1,15 +0,0 @@ -package fr.free.nrw.commons.utils; - -import androidx.fragment.app.Fragment; - -public class FragmentUtils { - - /** - * Utility function to check whether the fragment UI is still active or not - * @param fragment - * @return - */ - public static boolean isFragmentUIActive(Fragment fragment) { - return fragment!=null && fragment.getActivity() != null && fragment.isAdded() && !fragment.isDetached() && !fragment.isRemoving(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.kt new file mode 100644 index 000000000..4cdeecda2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.utils + +import androidx.fragment.app.Fragment + +object FragmentUtils { + + /** + * Utility function to check whether the fragment UI is still active or not + * @param fragment + * @return Boolean + */ + @JvmStatic + fun isFragmentUIActive(fragment: Fragment?): Boolean { + return fragment != null && + fragment.activity != null && + fragment.isAdded && + !fragment.isDetached && + !fragment.isRemoving + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java index 2b18669a4..2d1dbdf28 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java @@ -7,8 +7,8 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.annotations.SerializedName; -import org.apache.commons.lang3.StringUtils; import fr.free.nrw.commons.utils.DateUtil; +import org.apache.commons.lang3.StringUtils; import fr.free.nrw.commons.wikidata.GsonUtil; import java.text.ParseException; From c439143dd31c5c3d42f71de0fb4e4e51ba9fe4f6 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 18 Nov 2024 13:01:52 +0100 Subject: [PATCH 28/74] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-az/strings.xml | 2 ++ app/src/main/res/values-de/strings.xml | 13 +++++++++++++ app/src/main/res/values-io/strings.xml | 1 + app/src/main/res/values-iw/strings.xml | 6 +++--- app/src/main/res/values-krc/strings.xml | 10 ++++++++++ app/src/main/res/values-mk/strings.xml | 10 ++++++++++ app/src/main/res/values-nl/strings.xml | 10 ++++++++++ app/src/main/res/values-pa/strings.xml | 3 +++ app/src/main/res/values-pms/strings.xml | 1 + app/src/main/res/values-skr/strings.xml | 2 ++ app/src/main/res/values-zh/strings.xml | 12 ++++++++++++ 11 files changed, 67 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 1edbe43fc..d97d5ed20 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -10,6 +10,7 @@ * Toghrul Rahimli * Wertuose * Şeyx Şamil +* Əkrəm Cəfər --> Commons Facebook səhifəsi @@ -124,4 +125,5 @@ Bildiriş oxunmuş olaraq işarələndi Zəhmət olmasa tətbiqin cari məkanınızı göstərmək üçün məkan xidmətlərini aktiv edin Yaxınlıqdakı şəkilləri göstərmək üçün məkan icazəsi lazımdır + Qovluğu Sil diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a6471c1fe..215ecc985 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -19,6 +19,7 @@ * ManuelFranz * Mcliquid * Metalhead64 +* Mukeber * Nekky-chan * Ngschaider * Pyscowicz @@ -512,6 +513,7 @@ Du hast keine ungelesenen Benachrichtigungen Du hast keine gelesenen Benachrichtigungen Protokolle freigeben mit + Überprüfe deinen E-Mail-Posteingang Gelesene ansehen Ungelesene ansehen Beim Auswählen der Bilder ist ein Fehler aufgetreten @@ -816,4 +818,15 @@ Ausstehend Fehlgeschlagen Ortsdaten konnten nicht geladen werden + Ordner löschen + Löschung bestätigen + Bist du sicher, dass du den Ordner %1$s löschen möchten, die %2$d Datenobjekte enthalten? + Löschen + Abbrechen + Ordner %1$s erfolgreich gelöscht + Ordner %1$s konnte nicht gelöscht werden + Fehler beim Löschen des Ordnerinhalts: %1$s + Von diesem Ort gibt es noch kein Bild. Mach eins! + Dieser Ort hat bereits ein Bild. + Jetzt wird geprüft, ob dieser Ort bereits ein Bild hat. diff --git a/app/src/main/res/values-io/strings.xml b/app/src/main/res/values-io/strings.xml index 51fe16441..9a9f7d70e 100644 --- a/app/src/main/res/values-io/strings.xml +++ b/app/src/main/res/values-io/strings.xml @@ -525,6 +525,7 @@ SAVEZ PLUSE Bezonas permiso Vidar uzeropagino + Ka vu deziras informar la loko de ube vu obtenis ca imajo?\nInformo pri la lokizo helpos editeri trovar vua imajo, do ol divenos plu utila.\nDanko! Imajo selektita Ca imajo indikesis por ne sendesar Raporto diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 3389a227e..2ea71f748 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -114,7 +114,7 @@ ההעלאה התחילה! ההעלאות בתור (מופעל מצב חיבור מוגבל) הקובץ %1$s הועלה! - ללחוץ כאן כדי לצפות בהעלאה שלך + נא ללחוץ כאן כדי לצפות בהעלאה שלך העלאת קובץ: %s מתבצעת העלאת %1$s העלאת %1$s מסתיימת @@ -123,7 +123,7 @@ לחץ כדי להציג יש לגעת כדי לראות ההעלאות האחרונות שלי - בתור + הוכנסה בתור נכשלה %1$d%% הושלמו העלאה @@ -674,7 +674,7 @@ ההעלאה מושהית… ביטול ההעלאה... ביטול ההעלאה - הפעלת מצב חיבור מוגבל. כל ההעלאות מושהות ותמשכנה לאחר השבתת המצב הזה. + הפעלת מצב חיבור מוגבל. כל ההעלאות מושהות והן תימשכנה לאחר השבתת המצב הזה. מצב חיבור מוגבל פעיל. נא לכתוב תיאור קצר שמסביר מה מופיע בתמונה. בתיאור, כדאי לכתוב מה הופך את התמונה הזאת למעניינת, טיפוסית או נדירה ולהסביר את ההקשר, בין אם גלוי או סמוי. יש להשתמש במינוח מדויק ככל הניתן. נא למצוא ולבחור את כל העקרונות שהתמונה הזאת מתארת. נא לשמור על דיוק מרבי. אם התמונה מתארת מגוון פריטים, נא לבחור אותם בגבולות הסביר. לא לבחור תגיות גנריות אם יש תגיות יותר נקודתית זמינות. diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index eb9193e7f..13de1bb12 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -119,6 +119,7 @@ Категорияланы изле Медиагъызда суратланнган элементлени излегиз (тау, Тадж Махал э. а. к.) Сакъландыр + Къошакъ меню Джангырт Тизме (Алкъын джюклеуле джокъдула) @@ -781,6 +782,15 @@ Сакълауда Джетишимсиз Джерни юсюнден билгилени джюклеялмады + Папканы кетер + Кетериуню мюкюл эт + %2$d элементи болгъан %1$s папканы кетерирге излегенинге ишексизмисе? + Кетер + Ызына ал + %1$s папка джетишимли кетерилгенди + %1$s папканы кетериу джетишимисизди + Папканы ичиндегисини кетериуде халат: %1$s + Контейнерни идентификатору папкагъа джол табалмады: %1$d Бу джерни сураты джокъду, хайда бирин эт! Бу джерни алайсыз да сураты барды. Бу джерни сураты болуб-болмагъанын тинте турама. diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 916f4f420..61d71ea68 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -119,6 +119,7 @@ Пребарај категории Пребарајте ги предметите прикажани на сликата или снимката (планина, Лесновски манастир итн.) Зачувај + Преливно мени Превчитај Список (Сè уште нема подигања) @@ -785,6 +786,15 @@ Во исчекување Неуспешно Не можев да ги вчитам податоците за место + Избриши папка + Потврдете бришење + Дали сигурно сакате да ја избришете папката %1$s, која содржи %2$d ставки? + Избриши + Откажи + Папката %1$s е успешно избришана + Не успеав да ја избришам папката %1$s + Не можев да ја префрлам содржината на папката во ѓубре: %1$s + Не успеав да ја добијам патеката на папката за групата со назнака: %1$d Местово сè уште нема слика. Направете ја! Местово веќе има слика. Проверувам дали местово има слика. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 8cc3553a1..bc3016666 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -140,6 +140,7 @@ Categorieën zoeken Objecten zoeken die in uw bestand worden weergeven (berg, Taj Mahal, enz.) Opslaan + Overloopmenu Vernieuwen Lijst (Nog geen uploads) @@ -806,6 +807,15 @@ In behandeling Mislukt Plaatsgegevens konden niet geladen worden + Map verwijderen + Bevestig verwijdering + Weet u zeker dat u de map %1$s met %2$d onderdelen wilt verwijderen? + Verwijderen + Annuleren + De map %1$s is verwijderd + Het is niet gelukt de map %1$s te verwijderen + Fout bij het weggooien van de inhoud van de map: %1$s + Het is niet gelukt om het mappad voor bucket-ID %1$d op te halen Er is nog geen foto van deze plek, maak er eentje! Er is al een foto van deze plek. We controleren nu of er een foto van deze plek is. diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 7e750d175..2733093d4 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -168,7 +168,9 @@ ਵਿਕੀਪੀਡੀਆ ਲੇਖ ਤਸਵੀਰ ਬਹੁਤ ਗੂੜ੍ਹੀ ਹੈ। ਤਸਵੀਰ ਧੁੰਦਲੀ ਹੈ। + ਆਪਣੇ ਖਾਤੇ ਵਿੱਚ ਦਾਖ਼ਲ ਹੋਵੋ ਛੱਡੋ + ਦਾਖ਼ਲ ਹੋਵੋ ਵਿਕੀਡੇਟਾ ਵਿਕੀਪੀਡੀਆ ਅਕਸਰ ਪੁੱਛੇ ਜਾਂਦੇ ਸੁਆਲ @@ -185,6 +187,7 @@ ਸ਼੍ਰੇਣੀਆਂ ਨਕਸ਼ਾ ਸਵਾਲ + ਤੁਹਾਡੇ ਦਾਖਲੇ ਦੀ ਮਿਆਦ ਪੁੱਗ ਗਈ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ ਮੁੜ ਦਾਖਲ ਹੋਵੋ। ਜਾਰੀ ਰੱਖੋ ਕੋਈ ਤਾਜ਼ਾ ਖੋਜ ਨਹੀਂ ਮਿਟਾਓ diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index 7ac8c92ea..2ccd3c07a 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -790,6 +790,7 @@ Ël dossié %1$s a l\'é stàit eliminà për da bin Impossìbil eliminé ël dossié %1$s Eror durant l\'eliminassion dël contnù dël dossié: %1$s + Falì a arcuperé ël sënté d\'acess al dossié për ël sigilin d\'ID: %1$d Ës pòst a l\'ha ancor gnun-e fòto, ch\'a na pija un-a! Ës pòst a l\'ha già dle fòto. An camin ch\'as verìfica si cost pòst -sì a l\'ha dle fòto. diff --git a/app/src/main/res/values-skr/strings.xml b/app/src/main/res/values-skr/strings.xml index 4c68ee91b..f36e6f983 100644 --- a/app/src/main/res/values-skr/strings.xml +++ b/app/src/main/res/values-skr/strings.xml @@ -272,4 +272,6 @@ اپلوڈاں وچار ہیٹھ ناکام تھیا + مٹاؤ + منسوخ diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2adabec26..1ca6cc047 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -34,11 +34,13 @@ * SomeyaMako * Stang * StarrySky +* TFX202X * Tranve * U.T. * Vikarna * VulpesVulpes825 * Whym +* WiiUf * Willy1018 * Wxyveronica * XiaoGuoQuQ233 @@ -170,6 +172,7 @@ 搜索分类 搜索您的媒体描述的项目(如山、泰姬陵等) 保存 + 溢出菜单 刷新 列表 (尚无上传) @@ -835,6 +838,15 @@ 待处理 失败 无法加载地点数据 + 删除文件夹 + 确认删除 + 您确定要删除包含%2$d的文件夹%1$s吗? + 删除 + 撤消 + 文件夹%1$s已成功删除 + 无法删除文件夹%1$s + 删除文件夹内容时出错: %1$s + 无法检索存储桶ID的文件夹路径: %1$d 这个地点还没有照片,快去拍一张吧! 这个地点已经有照片了。 现在检查这个地点是否有照片。 From 0fdb0044b96041e0eed0262f49de02db2d4047a5 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Mon, 18 Nov 2024 19:10:35 +0530 Subject: [PATCH 29/74] Migrated util module from Java to Kotlin (#5938) * Rename .java to .kt * Migrated the following files in util module to Kotlin - AbstractTextWatcher - ActivityUtils - CommonsDateUtil - DateUtil * Rename .java to .kt * Migrated the following files in util module to Kotlin - DeviceInfoUtil - ExecutorUtils - FragmentUtils * Rename .java to .kt * Migrated the following files in util module to Kotlin - ImageUtils - ImageUtilsWrapper - LangCodeUtils - LayoutUtils - LengthUtils - LocationUtils - MapUtils * Rename .java to .kt * Migrated all remaining files in util module --- .../contributions/ContributionController.java | 4 +- .../contributions/ContributionsFragment.java | 4 - .../commons/contributions/MainActivity.java | 6 - .../free/nrw/commons/delete/DeleteHelper.java | 3 +- .../explore/map/ExploreMapFragment.java | 13 +- .../commons/media/MediaDetailFragment.java | 4 +- .../fragments/NearbyParentFragment.java | 7 +- .../NearbyParentFragmentPresenter.java | 7 - .../commons/settings/SettingsFragment.java | 5 +- .../nrw/commons/upload/UploadActivity.java | 7 +- .../fr/free/nrw/commons/utils/ImageUtils.java | 351 ----------------- .../fr/free/nrw/commons/utils/ImageUtils.kt | 363 ++++++++++++++++++ .../nrw/commons/utils/ImageUtilsWrapper.java | 30 -- .../nrw/commons/utils/ImageUtilsWrapper.kt | 29 ++ .../free/nrw/commons/utils/LangCodeUtils.java | 39 -- .../free/nrw/commons/utils/LangCodeUtils.kt | 40 ++ .../free/nrw/commons/utils/LayoutUtils.java | 38 -- .../fr/free/nrw/commons/utils/LayoutUtils.kt | 47 +++ .../free/nrw/commons/utils/LengthUtils.java | 145 ------- .../fr/free/nrw/commons/utils/LengthUtils.kt | 156 ++++++++ .../free/nrw/commons/utils/LocationUtils.java | 58 --- .../free/nrw/commons/utils/LocationUtils.kt | 63 +++ .../fr/free/nrw/commons/utils/MapUtils.java | 33 -- .../fr/free/nrw/commons/utils/MapUtils.kt | 39 ++ .../commons/utils/MediaDataExtractorUtil.java | 29 -- .../commons/utils/MediaDataExtractorUtil.kt | 29 ++ .../nrw/commons/utils/NearbyFABUtils.java | 51 --- .../free/nrw/commons/utils/NearbyFABUtils.kt | 55 +++ .../free/nrw/commons/utils/NetworkUtils.java | 94 ----- .../fr/free/nrw/commons/utils/NetworkUtils.kt | 85 ++++ .../nrw/commons/utils/PermissionUtils.java | 224 ----------- .../free/nrw/commons/utils/PermissionUtils.kt | 231 +++++++++++ .../fr/free/nrw/commons/utils/PlaceUtils.java | 55 --- .../fr/free/nrw/commons/utils/PlaceUtils.kt | 50 +++ .../nrw/commons/utils/StringSortingUtils.java | 90 ----- .../nrw/commons/utils/StringSortingUtils.kt | 86 +++++ .../fr/free/nrw/commons/utils/StringUtil.java | 38 -- .../fr/free/nrw/commons/utils/StringUtil.kt | 37 ++ .../nrw/commons/utils/SwipableCardView.java | 74 ---- .../nrw/commons/utils/SwipableCardView.kt | 64 +++ .../nrw/commons/utils/SystemThemeUtils.java | 49 --- .../nrw/commons/utils/SystemThemeUtils.kt | 52 +++ .../fr/free/nrw/commons/utils/UiUtils.java | 39 -- .../java/fr/free/nrw/commons/utils/UiUtils.kt | 41 ++ .../fr/free/nrw/commons/utils/ViewUtil.java | 143 ------- .../fr/free/nrw/commons/utils/ViewUtil.kt | 151 ++++++++ .../nrw/commons/utils/ViewUtilWrapper.java | 23 -- .../free/nrw/commons/utils/ViewUtilWrapper.kt | 17 + 48 files changed, 1651 insertions(+), 1647 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/MapUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/StringUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/UiUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/UiUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.kt diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index fcfd32974..e910799d0 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -87,7 +87,7 @@ public class ContributionController { }, R.string.storage_permission_title, R.string.write_storage_permission_rationale, - PermissionUtils.PERMISSIONS_STORAGE); + PermissionUtils.getPERMISSIONS_STORAGE()); } /** @@ -224,7 +224,7 @@ public class ContributionController { () -> FilePicker.openCustomSelector(activity, resultLauncher, 0), R.string.storage_permission_title, R.string.write_storage_permission_rationale, - PermissionUtils.PERMISSIONS_STORAGE); + PermissionUtils.getPERMISSIONS_STORAGE()); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index a840aa8e1..1699f35f0 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -5,7 +5,6 @@ import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED; import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL; import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME; -import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; import static fr.free.nrw.commons.utils.LengthUtils.computeBearing; import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; @@ -23,12 +22,10 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.MenuItem.OnMenuItemClickListener; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.activity.result.ActivityResultCallback; @@ -39,7 +36,6 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager.OnBackStackChangedListener; import androidx.fragment.app.FragmentTransaction; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.databinding.FragmentContributionsBinding; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index a9e9ee5c6..849ef3450 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -1,13 +1,10 @@ package fr.free.nrw.commons.contributions; -import android.Manifest.permission; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; @@ -16,10 +13,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; -import androidx.viewpager.widget.ViewPager; import androidx.work.ExistingWorkPolicy; import fr.free.nrw.commons.databinding.MainBinding; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.auth.SessionManager; @@ -41,7 +36,6 @@ import fr.free.nrw.commons.notification.NotificationController; import fr.free.nrw.commons.quiz.QuizChecker; import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.upload.UploadActivity; import fr.free.nrw.commons.upload.UploadProgressActivity; import fr.free.nrw.commons.upload.worker.WorkRequestHelper; import fr.free.nrw.commons.utils.PermissionUtils; 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 02f7d418e..134ee48d9 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 @@ -1,10 +1,10 @@ package fr.free.nrw.commons.delete; import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_DELETE; +import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources; import android.annotation.SuppressLint; import android.content.Context; -import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; @@ -16,6 +16,7 @@ import fr.free.nrw.commons.actions.PageEditClient; import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; import fr.free.nrw.commons.notification.NotificationHelper; import fr.free.nrw.commons.review.ReviewController; +import fr.free.nrw.commons.utils.LangCodeUtils; import fr.free.nrw.commons.utils.ViewUtilWrapper; import io.reactivex.Observable; import io.reactivex.Single; diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java index 52a5571e9..441f46e61 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java @@ -4,14 +4,12 @@ import static fr.free.nrw.commons.location.LocationServiceManager.LocationChange import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED; import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL; -import android.Manifest; import android.Manifest.permission; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Paint; @@ -21,22 +19,17 @@ import android.location.Location; import android.location.LocationManager; import android.os.Bundle; import android.preference.PreferenceManager; -import android.provider.Settings; import android.text.Html; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.widget.Toast; -import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatTextView; import androidx.core.content.ContextCompat; import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; import fr.free.nrw.commons.BaseMarker; import fr.free.nrw.commons.MapController; @@ -48,7 +41,6 @@ import fr.free.nrw.commons.databinding.FragmentExploreMapBinding; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.explore.ExploreMapRootFragment; import fr.free.nrw.commons.explore.paging.LiveDataConverter; -import fr.free.nrw.commons.filepicker.Constants; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationPermissionsHelper; @@ -60,7 +52,6 @@ import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.MapUtils; import fr.free.nrw.commons.utils.NetworkUtils; -import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.SystemThemeUtils; import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; @@ -310,7 +301,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment } private void startMapWithoutPermission() { - lastKnownLocation = MapUtils.defaultLatLng; + lastKnownLocation = MapUtils.getDefaultLatLng(); moveCameraToPosition( new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); presenter.onMapReady(exploreMapController); @@ -331,7 +322,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment !locationPermissionsHelper.checkLocationPermission(getActivity())) { isPermissionDenied = true; } - lastKnownLocation = MapUtils.defaultLatLng; + lastKnownLocation = MapUtils.getDefaultLatLng(); moveCameraToPosition( new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); presenter.onMapReady(exploreMapController); 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 edfa874fc..ed20809ac 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 @@ -318,7 +318,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements } public void launchZoomActivity(final View view) { - final boolean hasPermission = PermissionUtils.hasPermission(getActivity(), PermissionUtils.PERMISSIONS_STORAGE); + final boolean hasPermission = PermissionUtils.hasPermission(getActivity(), PermissionUtils.getPERMISSIONS_STORAGE()); if (hasPermission) { launchZoomActivityAfterPermissionCheck(view); } else { @@ -328,7 +328,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements }, R.string.storage_permission_title, R.string.read_storage_permission_rationale, - PermissionUtils.PERMISSIONS_STORAGE + PermissionUtils.getPERMISSIONS_STORAGE() ); } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index 6a2e5c3a9..fdbc727bc 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -43,7 +43,6 @@ import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.animation.Animation; import android.view.animation.AnimationUtils; -import android.widget.Button; import android.widget.Toast; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; @@ -701,7 +700,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment = new LatLng(Double.parseDouble(locationLatLng[0]), Double.parseDouble(locationLatLng[1]), 1f); } else { - lastKnownLocation = MapUtils.defaultLatLng; + lastKnownLocation = MapUtils.getDefaultLatLng(); } if (binding.map != null) { moveCameraToPosition( @@ -793,7 +792,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment hideBottomSheet(); binding.nearbyFilter.searchViewLayout.searchView.setOnQueryTextFocusChangeListener( (v, hasFocus) -> { - LayoutUtils.setLayoutHeightAllignedToWidth(1.25, + LayoutUtils.setLayoutHeightAlignedToWidth(1.25, binding.nearbyFilterList.getRoot()); if (hasFocus) { binding.nearbyFilterList.getRoot().setVisibility(View.VISIBLE); @@ -834,7 +833,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment .getLayoutParams().width = (int) LayoutUtils.getScreenWidth(getActivity(), 0.75); binding.nearbyFilterList.searchListView.setAdapter(nearbyFilterSearchRecyclerViewAdapter); - LayoutUtils.setLayoutHeightAllignedToWidth(1.25, binding.nearbyFilterList.getRoot()); + LayoutUtils.setLayoutHeightAlignedToWidth(1.25, binding.nearbyFilterList.getRoot()); compositeDisposable.add( RxSearchView.queryTextChanges(binding.nearbyFilter.searchViewLayout.searchView) .takeUntil(RxView.detaches(binding.nearbyFilter.searchViewLayout.searchView)) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java index 410aeb9f4..00a491e68 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java @@ -11,13 +11,10 @@ import static fr.free.nrw.commons.nearby.CheckBoxTriStates.UNKNOWN; import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; import android.location.Location; -import android.view.View; import androidx.annotation.MainThread; import androidx.annotation.Nullable; -import androidx.work.ExistingWorkPolicy; import fr.free.nrw.commons.BaseMarker; import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; -import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType; @@ -26,14 +23,10 @@ import fr.free.nrw.commons.nearby.CheckBoxTriStates; import fr.free.nrw.commons.nearby.Label; import fr.free.nrw.commons.nearby.MarkerPlaceGroup; import fr.free.nrw.commons.nearby.NearbyController; -import fr.free.nrw.commons.nearby.NearbyFilterState; import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.nearby.PlaceDao; import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract; -import fr.free.nrw.commons.upload.worker.WorkRequestHelper; import fr.free.nrw.commons.utils.LocationUtils; import fr.free.nrw.commons.wikidata.WikidataEditListener; -import io.reactivex.disposables.CompositeDisposable; import java.lang.reflect.Proxy; import java.util.List; import timber.log.Timber; diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 20fc831a8..d4ed379f0 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -12,7 +12,6 @@ import android.net.Uri; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; @@ -543,7 +542,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { * First checks for external storage permissions and then sends logs via email */ private void checkPermissionsAndSendLogs() { - if (PermissionUtils.hasPermission(getActivity(), PermissionUtils.PERMISSIONS_STORAGE)) { + if (PermissionUtils.hasPermission(getActivity(), PermissionUtils.getPERMISSIONS_STORAGE())) { commonsLogSender.send(getActivity(), null); } else { requestExternalStoragePermissions(); @@ -556,7 +555,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { */ private void requestExternalStoragePermissions() { Dexter.withActivity(getActivity()) - .withPermissions(PermissionUtils.PERMISSIONS_STORAGE) + .withPermissions(PermissionUtils.getPERMISSIONS_STORAGE()) .withListener(new MultiplePermissionsListener() { @Override public void onPermissionsChecked(MultiplePermissionsReport report) { 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 35906c3fb..ed65b05df 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,8 +1,8 @@ package fr.free.nrw.commons.upload; import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS; -import static fr.free.nrw.commons.utils.PermissionUtils.PERMISSIONS_STORAGE; import static fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction; +import static fr.free.nrw.commons.utils.PermissionUtils.getPERMISSIONS_STORAGE; import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE; import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY; @@ -32,7 +32,6 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import androidx.work.ExistingWorkPolicy; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.SessionManager; @@ -277,7 +276,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, public void checkStoragePermissions() { // Check if all required permissions are granted - final boolean hasAllPermissions = PermissionUtils.hasPermission(this, PERMISSIONS_STORAGE); + final boolean hasAllPermissions = PermissionUtils.hasPermission(this, getPERMISSIONS_STORAGE()); final boolean hasPartialAccess = PermissionUtils.hasPartialAccess(this); if (hasAllPermissions || hasPartialAccess) { // All required permissions are granted, so enable UI elements and perform actions @@ -297,7 +296,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, }, R.string.storage_permission_title, R.string.write_storage_permission_rationale_for_image_share, - PERMISSIONS_STORAGE); + getPERMISSIONS_STORAGE()); } } /* If all permissions are not granted and a dialog is already showing on screen diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java deleted file mode 100644 index 99155a5e3..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java +++ /dev/null @@ -1,351 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.ProgressDialog; -import android.app.WallpaperManager; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Color; -import android.net.Uri; -import android.os.Build; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.exifinterface.media.ExifInterface; -import androidx.work.Data; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; -import com.facebook.common.executors.CallerThreadExecutor; -import com.facebook.common.references.CloseableReference; -import com.facebook.datasource.DataSource; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.imagepipeline.core.ImagePipeline; -import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; -import com.facebook.imagepipeline.image.CloseableImage; -import com.facebook.imagepipeline.request.ImageRequest; -import com.facebook.imagepipeline.request.ImageRequestBuilder; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.SetWallpaperWorker; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import timber.log.Timber; - -/** - * Created by bluesir9 on 3/10/17. - */ - -public class ImageUtils { - - /** - * Set 0th bit as 1 for dark image ie. 0001 - */ - public static final int IMAGE_DARK = 1 << 0; // 1 - /** - * Set 1st bit as 1 for blurry image ie. 0010 - */ - public static final int IMAGE_BLURRY = 1 << 1; // 2 - /** - * Set 2nd bit as 1 for duplicate image ie. 0100 - */ - public static final int IMAGE_DUPLICATE = 1 << 2; //4 - /** - * Set 3rd bit as 1 for image with different geo location ie. 1000 - */ - public static final int IMAGE_GEOLOCATION_DIFFERENT = 1 << 3; //8 - /** - * The parameter FILE_FBMD is returned from the class ReadFBMD if the uploaded image contains FBMD data else returns IMAGE_OK - * ie. 10000 - */ - public static final int FILE_FBMD = 1 << 4; - /** - * The parameter FILE_NO_EXIF is returned from the class EXIFReader if the uploaded image does not contains EXIF data else returns IMAGE_OK - * ie. 100000 - */ - public static final int FILE_NO_EXIF = 1 << 5; - public static final int IMAGE_OK = 0; - public static final int IMAGE_KEEP = -1; - public static final int IMAGE_WAIT = -2; - public static final int EMPTY_CAPTION = -3; - public static final int FILE_NAME_EXISTS = 1 << 6; - static final int NO_CATEGORY_SELECTED = -5; - - private static ProgressDialog progressDialogWallpaper; - - private static ProgressDialog progressDialogAvatar; - - @IntDef( - flag = true, - value = { - IMAGE_DARK, - IMAGE_BLURRY, - IMAGE_DUPLICATE, - IMAGE_OK, - IMAGE_KEEP, - IMAGE_WAIT, - EMPTY_CAPTION, - FILE_NAME_EXISTS, - NO_CATEGORY_SELECTED, - IMAGE_GEOLOCATION_DIFFERENT - } - ) - @Retention(RetentionPolicy.SOURCE) - public @interface Result { - } - - /** - * @return IMAGE_OK if image is not too dark - * IMAGE_DARK if image is too dark - */ - static @Result int checkIfImageIsTooDark(String imagePath) { - long millis = System.currentTimeMillis(); - try { - Bitmap bmp = new ExifInterface(imagePath).getThumbnailBitmap(); - if (bmp == null) { - bmp = BitmapFactory.decodeFile(imagePath); - } - - if (checkIfImageIsDark(bmp)) { - return IMAGE_DARK; - } - - } catch (Exception e) { - Timber.d(e, "Error while checking image darkness."); - } finally { - Timber.d("Checking image darkness took " + (System.currentTimeMillis() - millis) + " ms."); - } - return IMAGE_OK; - } - - /** - * @param geolocationOfFileString Geolocation of image. If geotag doesn't exists, then this will be an empty string - * @param latLng Location of wikidata item will be edited after upload - * @return false if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null - * true if geolocation of the image and wikidata item are different - */ - static boolean checkImageGeolocationIsDifferent(String geolocationOfFileString, LatLng latLng) { - Timber.d("Comparing geolocation of file with nearby place location"); - if (latLng == null) { // Means that geolocation for this image is not given - return false; // Since we don't know geolocation of file, we choose letting upload - } - - String[] geolocationOfFile = geolocationOfFileString.split("\\|"); - Double distance = LengthUtils.computeDistanceBetween( - new LatLng(Double.parseDouble(geolocationOfFile[0]),Double.parseDouble(geolocationOfFile[1]),0) - , latLng); - // Distance is more than 1 km, means that geolocation is wrong - return distance >= 1000; - } - - private static boolean checkIfImageIsDark(Bitmap bitmap) { - if (bitmap == null) { - Timber.e("Expected bitmap was null"); - return true; - } - - int bitmapWidth = bitmap.getWidth(); - int bitmapHeight = bitmap.getHeight(); - - int allPixelsCount = bitmapWidth * bitmapHeight; - int numberOfBrightPixels = 0; - int numberOfMediumBrightnessPixels = 0; - double brightPixelThreshold = 0.025 * allPixelsCount; - double mediumBrightPixelThreshold = 0.3 * allPixelsCount; - - for (int x = 0; x < bitmapWidth; x++) { - for (int y = 0; y < bitmapHeight; y++) { - int pixel = bitmap.getPixel(x, y); - int r = Color.red(pixel); - int g = Color.green(pixel); - int b = Color.blue(pixel); - - int secondMax = r > g ? r : g; - double max = (secondMax > b ? secondMax : b) / 255.0; - - int secondMin = r < g ? r : g; - double min = (secondMin < b ? secondMin : b) / 255.0; - - double luminance = ((max + min) / 2.0) * 100; - - int highBrightnessLuminance = 40; - int mediumBrightnessLuminance = 26; - - if (luminance < highBrightnessLuminance) { - if (luminance > mediumBrightnessLuminance) { - numberOfMediumBrightnessPixels++; - } - } else { - numberOfBrightPixels++; - } - - if (numberOfBrightPixels >= brightPixelThreshold || numberOfMediumBrightnessPixels >= mediumBrightPixelThreshold) { - return false; - } - } - } - return true; - } - - /** - * Downloads the image from the URL and sets it as the phone's wallpaper - * Fails silently if download or setting wallpaper fails. - * - * @param context context - * @param imageUrl Url of the image - */ - public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) { - - enqueueSetWallpaperWork(context, imageUrl); - - } - - private static void createNotificationChannel(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - CharSequence name = "Wallpaper Setting"; - String description = "Notifications for wallpaper setting progress"; - int importance = NotificationManager.IMPORTANCE_DEFAULT; - NotificationChannel channel = new NotificationChannel("set_wallpaper_channel", name, importance); - channel.setDescription(description); - NotificationManager notificationManager = context.getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); - } - } - - - /** - * Calls the set avatar api to set the image url as user's avatar - * @param context - * @param url - * @param username - * @param okHttpJsonApiClient - * @param compositeDisposable - */ - public static void setAvatarFromImageUrl(Context context, String url, String username, - OkHttpJsonApiClient okHttpJsonApiClient, CompositeDisposable compositeDisposable) { - showSettingAvatarProgressBar(context); - - try { - compositeDisposable.add(okHttpJsonApiClient - .setAvatar(username, url) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null && response.getStatus().equals("200")) { - ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_successfully)); - if (progressDialogAvatar != null && progressDialogAvatar.isShowing()) { - progressDialogAvatar.dismiss(); - } - } - }, - t -> { - Timber.e(t, "Setting Avatar Failed"); - ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully)); - if (progressDialogAvatar != null) { - progressDialogAvatar.cancel(); - } - } - )); - } - catch (Exception e){ - Timber.d(e+"success"); - ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully)); - if (progressDialogAvatar != null) { - progressDialogAvatar.cancel(); - } - } - - } - - public static void enqueueSetWallpaperWork(Context context, Uri imageUrl) { - createNotificationChannel(context); // Ensure the notification channel is created - - Data inputData = new Data.Builder() - .putString("imageUrl", imageUrl.toString()) - .build(); - - OneTimeWorkRequest setWallpaperWork = new OneTimeWorkRequest.Builder(SetWallpaperWorker.class) - .setInputData(inputData) - .build(); - - WorkManager.getInstance(context).enqueue(setWallpaperWork); - } - - - private static void showSettingWallpaperProgressBar(Context context) { - progressDialogWallpaper = ProgressDialog.show(context, context.getString(R.string.setting_wallpaper_dialog_title), - context.getString(R.string.setting_wallpaper_dialog_message), true); - } - - private static void showSettingAvatarProgressBar(Context context) { - progressDialogAvatar = ProgressDialog.show(context, context.getString(R.string.setting_avatar_dialog_title), - context.getString(R.string.setting_avatar_dialog_message), true); - } - - /** - * Result variable is a result of an or operation of all possible problems. Ie. if result - * is 0001 means IMAGE_DARK - * if result is 1100 IMAGE_DUPLICATE and IMAGE_GEOLOCATION_DIFFERENT - */ - public static String getErrorMessageForResult(Context context, @Result int result) { - StringBuilder errorMessage = new StringBuilder(); - if (result <= 0 ) { - Timber.d("No issues to warn user is found"); - } else { - Timber.d("Issues found to warn user"); - - errorMessage.append(context.getResources().getString(R.string.upload_problem_exist)); - - if ((IMAGE_DARK & result) != 0 ) { // We are checking image dark bit to see if that bit is set or not - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_dark)); - } - - if ((IMAGE_BLURRY & result) != 0 ) { - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_blurry)); - } - - if ((IMAGE_DUPLICATE & result) != 0 ) { - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_duplicate)); - } - - if ((IMAGE_GEOLOCATION_DIFFERENT & result) != 0 ) { - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_different_geolocation)); - } - - if ((FILE_FBMD & result) != 0) { - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_fbmd)); - } - - if ((FILE_NO_EXIF & result) != 0){ - errorMessage.append("\n - ").append(context.getResources().getString(R.string.internet_downloaded)); - } - - errorMessage.append("\n\n").append(context.getResources().getString(R.string.upload_problem_do_you_continue)); - } - - return errorMessage.toString(); - } - - /** - * Adds red border to a bitmap - * @param bitmap - * @param borderSize - * @param context - * @return - */ - public static Bitmap addRedBorder(Bitmap bitmap, int borderSize, Context context) { - Bitmap bmpWithBorder = Bitmap.createBitmap(bitmap.getWidth() + borderSize * 2, bitmap.getHeight() + borderSize * 2, bitmap.getConfig()); - Canvas canvas = new Canvas(bmpWithBorder); - canvas.drawColor(ContextCompat.getColor(context, R.color.deleteRed)); - canvas.drawBitmap(bitmap, borderSize, borderSize, null); - return bmpWithBorder; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt new file mode 100644 index 000000000..78a877600 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt @@ -0,0 +1,363 @@ +package fr.free.nrw.commons.utils + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.ProgressDialog +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color +import android.net.Uri +import android.os.Build +import androidx.annotation.IntDef +import androidx.core.content.ContextCompat +import androidx.exifinterface.media.ExifInterface +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import fr.free.nrw.commons.R +import fr.free.nrw.commons.contributions.SetWallpaperWorker +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + +/** + * Created by blueSir9 on 3/10/17. + */ + + +object ImageUtils { + + /** + * Set 0th bit as 1 for dark image ie. 0001 + */ + const val IMAGE_DARK = 1 shl 0 // 1 + + /** + * Set 1st bit as 1 for blurry image ie. 0010 + */ + const val IMAGE_BLURRY = 1 shl 1 // 2 + + /** + * Set 2nd bit as 1 for duplicate image ie. 0100 + */ + const val IMAGE_DUPLICATE = 1 shl 2 // 4 + + /** + * Set 3rd bit as 1 for image with different geo location ie. 1000 + */ + const val IMAGE_GEOLOCATION_DIFFERENT = 1 shl 3 // 8 + + /** + * The parameter FILE_FBMD is returned from the class ReadFBMD if the uploaded image contains + * FBMD data else returns IMAGE_OK + * ie. 10000 + */ + const val FILE_FBMD = 1 shl 4 // 16 + + /** + * The parameter FILE_NO_EXIF is returned from the class EXIFReader if the uploaded image does + * not contains EXIF data else returns IMAGE_OK + * ie. 100000 + */ + const val FILE_NO_EXIF = 1 shl 5 // 32 + + const val IMAGE_OK = 0 + const val IMAGE_KEEP = -1 + const val IMAGE_WAIT = -2 + const val EMPTY_CAPTION = -3 + const val FILE_NAME_EXISTS = 1 shl 6 // 64 + const val NO_CATEGORY_SELECTED = -5 + + private var progressDialogWallpaper: ProgressDialog? = null + + private var progressDialogAvatar: ProgressDialog? = null + + @IntDef( + flag = true, + value = [ + IMAGE_DARK, + IMAGE_BLURRY, + IMAGE_DUPLICATE, + IMAGE_OK, + IMAGE_KEEP, + IMAGE_WAIT, + EMPTY_CAPTION, + FILE_NAME_EXISTS, + NO_CATEGORY_SELECTED, + IMAGE_GEOLOCATION_DIFFERENT + ] + ) + @Retention + annotation class Result + + /** + * @return IMAGE_OK if image is not too dark + * IMAGE_DARK if image is too dark + */ + @JvmStatic + fun checkIfImageIsTooDark(imagePath: String): Int { + val millis = System.currentTimeMillis() + return try { + var bmp = ExifInterface(imagePath).thumbnailBitmap + if (bmp == null) { + bmp = BitmapFactory.decodeFile(imagePath) + } + + if (checkIfImageIsDark(bmp)) { + IMAGE_DARK + } else { + IMAGE_OK + } + } catch (e: Exception) { + Timber.d(e, "Error while checking image darkness.") + IMAGE_OK + } finally { + Timber.d("Checking image darkness took ${System.currentTimeMillis() - millis} ms.") + } + } + + /** + * @param geolocationOfFileString Geolocation of image. If geotag doesn't exists, then this will + * be an empty string + * @param latLng Location of wikidata item will be edited after upload + * @return false if image is neither dark nor blurry or if the input bitmapRegionDecoder provide + * d is null true if geolocation of the image and wikidata item are different + */ + @JvmStatic + fun checkImageGeolocationIsDifferent(geolocationOfFileString: String, latLng: LatLng?): Boolean { + Timber.d("Comparing geolocation of file with nearby place location") + if (latLng == null) { // Means that geolocation for this image is not given + return false // Since we don't know geolocation of file, we choose letting upload + } + + val geolocationOfFile = geolocationOfFileString.split("|") + val distance = LengthUtils.computeDistanceBetween( + LatLng(geolocationOfFile[0].toDouble(), geolocationOfFile[1].toDouble(), 0.0F), + latLng + ) + // Distance is more than 1 km, means that geolocation is wrong + return distance >= 1000 + } + + @JvmStatic + private fun checkIfImageIsDark(bitmap: Bitmap?): Boolean { + if (bitmap == null) { + Timber.e("Expected bitmap was null") + return true + } + + val bitmapWidth = bitmap.width + val bitmapHeight = bitmap.height + + val allPixelsCount = bitmapWidth * bitmapHeight + var numberOfBrightPixels = 0 + var numberOfMediumBrightnessPixels = 0 + val brightPixelThreshold = 0.025 * allPixelsCount + val mediumBrightPixelThreshold = 0.3 * allPixelsCount + + for (x in 0 until bitmapWidth) { + for (y in 0 until bitmapHeight) { + val pixel = bitmap.getPixel(x, y) + val r = Color.red(pixel) + val g = Color.green(pixel) + val b = Color.blue(pixel) + + val max = maxOf(r, g, b) / 255.0 + val min = minOf(r, g, b) / 255.0 + + val luminance = ((max + min) / 2.0) * 100 + + val highBrightnessLuminance = 40 + val mediumBrightnessLuminance = 26 + + if (luminance < highBrightnessLuminance) { + if (luminance > mediumBrightnessLuminance) { + numberOfMediumBrightnessPixels++ + } + } else { + numberOfBrightPixels++ + } + + if (numberOfBrightPixels >= brightPixelThreshold || numberOfMediumBrightnessPixels >= mediumBrightPixelThreshold) { + return false + } + } + } + return true + } + + /** + * Downloads the image from the URL and sets it as the phone's wallpaper + * Fails silently if download or setting wallpaper fails. + * + * @param context context + * @param imageUrl Url of the image + */ + @JvmStatic + fun setWallpaperFromImageUrl(context: Context, imageUrl: Uri) { + enqueueSetWallpaperWork(context, imageUrl) + } + + @JvmStatic + private fun createNotificationChannel(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = "Wallpaper Setting" + val description = "Notifications for wallpaper setting progress" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel("set_wallpaper_channel", name, importance).apply { + this.description = description + } + val notificationManager = context.getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + } + + /** + * Calls the set avatar api to set the image url as user's avatar + * @param context + * @param url + * @param username + * @param okHttpJsonApiClient + * @param compositeDisposable + */ + @JvmStatic + fun setAvatarFromImageUrl( + context: Context, + url: String, + username: String, + okHttpJsonApiClient: OkHttpJsonApiClient, + compositeDisposable: CompositeDisposable + ) { + showSettingAvatarProgressBar(context) + + try { + compositeDisposable.add( + okHttpJsonApiClient + .setAvatar(username, url) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response -> + if (response?.status == "200") { + ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_successfully)) + progressDialogAvatar?.dismiss() + } + }, + { t -> + Timber.e(t, "Setting Avatar Failed") + ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully)) + progressDialogAvatar?.cancel() + } + ) + ) + } catch (e: Exception) { + Timber.d("$e success") + ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully)) + progressDialogAvatar?.cancel() + } + } + + @JvmStatic + fun enqueueSetWallpaperWork(context: Context, imageUrl: Uri) { + createNotificationChannel(context) // Ensure the notification channel is created + + val inputData = Data.Builder() + .putString("imageUrl", imageUrl.toString()) + .build() + + val setWallpaperWork = OneTimeWorkRequest.Builder(SetWallpaperWorker::class.java) + .setInputData(inputData) + .build() + + WorkManager.getInstance(context).enqueue(setWallpaperWork) + } + + @JvmStatic + private fun showSettingWallpaperProgressBar(context: Context) { + progressDialogWallpaper = ProgressDialog.show( + context, + context.getString(R.string.setting_wallpaper_dialog_title), + context.getString(R.string.setting_wallpaper_dialog_message), + true + ) + } + + @JvmStatic + private fun showSettingAvatarProgressBar(context: Context) { + progressDialogAvatar = ProgressDialog.show( + context, + context.getString(R.string.setting_avatar_dialog_title), + context.getString(R.string.setting_avatar_dialog_message), + true + ) + } + + /** + * Adds red border to bitmap with specified border size + * * @param bitmap + * * @param borderSize + * * @param context + * * @return + */ + @JvmStatic + fun addRedBorder(bitmap: Bitmap, borderSize: Int, context: Context): Bitmap { + val bmpWithBorder = Bitmap.createBitmap( + bitmap.width + borderSize * 2, + bitmap.height + borderSize * 2, + bitmap.config + ) + val canvas = Canvas(bmpWithBorder) + canvas.drawColor(ContextCompat.getColor(context, R.color.deleteRed)) + canvas.drawBitmap(bitmap, borderSize.toFloat(), borderSize.toFloat(), null) + return bmpWithBorder + } + + /** + * Result variable is a result of an or operation of all possible problems. Ie. if result + * is 0001 means IMAGE_DARK + * if result is 1100 IMAGE_DUPLICATE and IMAGE_GEOLOCATION_DIFFERENT + */ + @JvmStatic + fun getErrorMessageForResult(context: Context, @Result result: Int): String { + val errorMessage = StringBuilder() + if (result <= 0) { + Timber.d("No issues to warn user are found") + } else { + Timber.d("Issues found to warn user") + errorMessage.append(context.getString(R.string.upload_problem_exist)) + + if (result and IMAGE_DARK != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.upload_problem_image_dark)) + } + if (result and IMAGE_BLURRY != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.upload_problem_image_blurry)) + } + if (result and IMAGE_DUPLICATE != 0) { + errorMessage.append("\n - "). + append(context.getString(R.string.upload_problem_image_duplicate)) + } + if (result and IMAGE_GEOLOCATION_DIFFERENT != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.upload_problem_different_geolocation)) + } + if (result and FILE_FBMD != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.upload_problem_fbmd)) + } + if (result and FILE_NO_EXIF != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.internet_downloaded)) + } + errorMessage.append("\n\n") + .append(context.getString(R.string.upload_problem_do_you_continue)) + } + return errorMessage.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java deleted file mode 100644 index 634a73ad2..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java +++ /dev/null @@ -1,30 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.location.LatLng; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class ImageUtilsWrapper { - - @Inject - public ImageUtilsWrapper() { - - } - - public Single checkIfImageIsTooDark(String bitmapPath) { - return Single.fromCallable(() -> ImageUtils.checkIfImageIsTooDark(bitmapPath)) - .subscribeOn(Schedulers.computation()); - } - - public Single checkImageGeolocationIsDifferent(String geolocationOfFileString, - LatLng latLng) { - return Single.fromCallable( - () -> ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng)) - .subscribeOn(Schedulers.computation()) - .map(isDifferent -> isDifferent ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT - : ImageUtils.IMAGE_OK); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt new file mode 100644 index 000000000..2e0efc690 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.location.LatLng +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ImageUtilsWrapper @Inject constructor() { + + fun checkIfImageIsTooDark(bitmapPath: String): Single { + return Single.fromCallable { ImageUtils.checkIfImageIsTooDark(bitmapPath) } + .subscribeOn(Schedulers.computation()) + } + + fun checkImageGeolocationIsDifferent( + geolocationOfFileString: String, + latLng: LatLng + ): Single { + return Single.fromCallable { + ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng) + } + .subscribeOn(Schedulers.computation()) + .map { isDifferent -> + if (isDifferent) ImageUtils.IMAGE_GEOLOCATION_DIFFERENT else ImageUtils.IMAGE_OK + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java deleted file mode 100644 index 73bd5c02b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -package fr.free.nrw.commons.utils; -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import java.util.Locale; - -/** - * Utilities class for miscellaneous strings - */ -public class LangCodeUtils { - /** - * Replaces the deprecated ISO-639 language codes used by Android with the updated ISO-639-1. - * @param code Language code you want to update. - * @return Updated language code. If not in the "deprecated list" returns the same code. - */ - public static String fixLanguageCode(String code) { - if (code.equalsIgnoreCase("iw")) { - return "he"; - } else if (code.equalsIgnoreCase("in")) { - return "id"; - } else if (code.equalsIgnoreCase("ji")) { - return "yi"; - } else { - return code; - } - } - - /** - * Returns configuration for locale of - * our choice regardless of user's device settings - */ - public static Resources getLocalizedResources(Context context, Locale desiredLocale) { - Configuration conf = context.getResources().getConfiguration(); - conf = new Configuration(conf); - conf.setLocale(desiredLocale); - Context localizedContext = context.createConfigurationContext(conf); - return localizedContext.getResources(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.kt new file mode 100644 index 000000000..5ef21a735 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.kt @@ -0,0 +1,40 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import java.util.Locale + +/** + * Utilities class for miscellaneous strings + */ +object LangCodeUtils { + + /** + * Replaces the deprecated ISO-639 language codes used by Android with the updated ISO-639-1. + * @param code Language code you want to update. + * @return Updated language code. If not in the "deprecated list" returns the same code. + */ + @JvmStatic + fun fixLanguageCode(code: String): String { + return when (code.lowercase()) { + "iw" -> "he" + "in" -> "id" + "ji" -> "yi" + else -> code + } + } + + /** + * Returns configuration for locale of + * our choice regardless of user's device settings + */ + @JvmStatic + fun getLocalizedResources(context: Context, desiredLocale: Locale): Resources { + val conf = Configuration(context.resources.configuration).apply { + setLocale(desiredLocale) + } + val localizedContext = context.createConfigurationContext(conf) + return localizedContext.resources + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.java deleted file mode 100644 index 76c52527b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.app.Activity; -import android.content.Context; -import android.util.DisplayMetrics; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; - -public class LayoutUtils { - - /** - * Can be used for keeping aspect radios suggested by material guidelines. See: - * https://material.io/design/layout/spacing-methods.html#containers-aspect-ratios - * In some cases we don't know exact width, for such cases this method measures - * width and sets height by multiplying the width with height. - * @param rate Aspect ratios, ie 1 for 1:1. (width * rate = height) - * @param view view to change height - */ - public static void setLayoutHeightAllignedToWidth(double rate, View view) { - ViewTreeObserver vto = view.getViewTreeObserver(); - vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - view.getViewTreeObserver().removeOnGlobalLayoutListener(this); - ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); - layoutParams.height = (int) (view.getWidth() * rate); - view.setLayoutParams(layoutParams); - } - }); - } - - public static double getScreenWidth(Context context, double rate) { - DisplayMetrics displayMetrics = new DisplayMetrics(); - ((Activity)context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - return displayMetrics.widthPixels * rate; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.kt new file mode 100644 index 000000000..71e6697f7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.kt @@ -0,0 +1,47 @@ +package fr.free.nrw.commons.utils + +import android.app.Activity +import android.content.Context +import android.util.DisplayMetrics +import android.view.View +import android.view.ViewTreeObserver + +/** + * Utility class for layout-related operations. + */ +object LayoutUtils { + + /** + * Can be used for keeping aspect ratios suggested by material guidelines. See: + * https://material.io/design/layout/spacing-methods.html#containers-aspect-ratios + * In some cases, we don't know the exact width, for such cases this method measures + * width and sets height by multiplying the width with height. + * @param rate Aspect ratios, i.e., 1 for 1:1 (width * rate = height) + * @param view View to change height + */ + @JvmStatic + fun setLayoutHeightAlignedToWidth(rate: Double, view: View) { + val vto = view.viewTreeObserver + vto.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + view.viewTreeObserver.removeOnGlobalLayoutListener(this) + val layoutParams = view.layoutParams + layoutParams.height = (view.width * rate).toInt() + view.layoutParams = layoutParams + } + }) + } + + /** + * Calculates and returns the screen width multiplied by the provided rate. + * @param context Context used to access display metrics. + * @param rate Multiplier for screen width. + * @return Calculated screen width multiplied by the rate. + */ + @JvmStatic + fun getScreenWidth(context: Context, rate: Double): Double { + val displayMetrics = DisplayMetrics() + (context as Activity).windowManager.defaultDisplay.getMetrics(displayMetrics) + return displayMetrics.widthPixels * rate + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java deleted file mode 100644 index 0ca61a1d9..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java +++ /dev/null @@ -1,145 +0,0 @@ -package fr.free.nrw.commons.utils; - -import androidx.annotation.NonNull; - -import java.text.NumberFormat; - -import fr.free.nrw.commons.location.LatLng; - -public class LengthUtils { - /** - * Returns a formatted distance string between two points. - * - * @param point1 LatLng type point1 - * @param point2 LatLng type point2 - * @return string distance - */ - public static String formatDistanceBetween(LatLng point1, LatLng point2) { - if (point1 == null || point2 == null) { - return null; - } - - int distance = (int) Math.round(computeDistanceBetween(point1, point2)); - return formatDistance(distance); - } - - /** - * Format a distance (in meters) as a string - * Example: 140 -> "140m" - * 3841 -> "3.8km" - * - * @param distance Distance, in meters - * @return A string representing the distance - * @throws IllegalArgumentException If distance is negative - */ - public static String formatDistance(int distance) { - if (distance < 0) { - throw new IllegalArgumentException("Distance must be non-negative"); - } - - NumberFormat numberFormat = NumberFormat.getNumberInstance(); - - // Adjust to km if distance is over 1000m (1km) - if (distance >= 1000) { - numberFormat.setMaximumFractionDigits(1); - return numberFormat.format(distance / 1000.0) + "km"; - } - - // Otherwise just return in meters - return numberFormat.format(distance) + "m"; - } - - /** - * Computes the distance between two points. - * - * @param point1 LatLng type point1 - * @param point2 LatLng type point2 - * @return distance between the points in meters - * @throws NullPointerException if one or both the points are null - */ - public static double computeDistanceBetween(@NonNull LatLng point1, @NonNull LatLng point2) { - return computeAngleBetween(point1, point2) * 6371009.0D; // Earth's radius in meter - } - - /** - * Computes angle between two points - * - * @param point1 one of the two end points - * @param point2 one of the two end points - * @return Angle in radius - * @throws NullPointerException if one or both the points are null - */ - private static double computeAngleBetween(@NonNull LatLng point1, @NonNull LatLng point2) { - return distanceRadians( - Math.toRadians(point1.getLatitude()), - Math.toRadians(point1.getLongitude()), - Math.toRadians(point2.getLatitude()), - Math.toRadians(point2.getLongitude()) - ); - } - - /** - * Computes arc length between 2 points - * - * @param lat1 Latitude of point A - * @param lng1 Longitude of point A - * @param lat2 Latitude of point B - * @param lng2 Longitude of point B - * @return Arc length between the points - */ - private static double distanceRadians(double lat1, double lng1, double lat2, double lng2) { - return arcHav(havDistance(lat1, lat2, lng1 - lng2)); - } - - /** - * Computes inverse of haversine - * - * @param x Angle in radian - * @return Inverse of haversine - */ - private static double arcHav(double x) { - return 2.0D * Math.asin(Math.sqrt(x)); - } - - /** - * Computes distance between two points that are on same Longitude - * - * @param lat1 Latitude of point A - * @param lat2 Latitude of point B - * @param longitude Longitude on which they lie - * @return Arc length between points - */ - private static double havDistance(double lat1, double lat2, double longitude) { - return hav(lat1 - lat2) + hav(longitude) * Math.cos(lat1) * Math.cos(lat2); - } - - /** - * Computes haversine - * - * @param x Angle in radians - * @return Haversine of x - */ - private static double hav(double x) { - double sinHalf = Math.sin(x * 0.5D); - return sinHalf * sinHalf; - } - - /** - * Computes bearing between the two given points - * - * @see Bearing - * @param point1 Coordinates of first point - * @param point2 Coordinates of second point - * @return Bearing between the two end points in degrees - * @throws NullPointerException if one or both the points are null - */ - public static double computeBearing(@NonNull LatLng point1, @NonNull LatLng point2) { - double diffLongitute = Math.toRadians(point2.getLongitude() - point1.getLongitude()); - double lat1 = Math.toRadians(point1.getLatitude()); - double lat2 = Math.toRadians(point2.getLatitude()); - double y = Math.sin(diffLongitute) * Math.cos(lat2); - double x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(diffLongitute); - double bearing = Math.atan2(y, x); - return (Math.toDegrees(bearing) + 360) % 360; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.kt new file mode 100644 index 000000000..48cf1a020 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.kt @@ -0,0 +1,156 @@ +package fr.free.nrw.commons.utils + +import java.text.NumberFormat +import fr.free.nrw.commons.location.LatLng +import kotlin.math.asin +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.roundToInt +import kotlin.math.sin +import kotlin.math.sqrt + +object LengthUtils { + /** + * Returns a formatted distance string between two points. + * + * @param point1 LatLng type point1 + * @param point2 LatLng type point2 + * @return string distance + */ + @JvmStatic + fun formatDistanceBetween(point1: LatLng?, point2: LatLng?): String? { + if (point1 == null || point2 == null) { + return null + } + + val distance = computeDistanceBetween(point1, point2).roundToInt() + return formatDistance(distance) + } + + /** + * Format a distance (in meters) as a string + * Example: 140 -> "140m" + * 3841 -> "3.8km" + * + * @param distance Distance, in meters + * @return A string representing the distance + * @throws IllegalArgumentException If distance is negative + */ + @JvmStatic + fun formatDistance(distance: Int): String { + if (distance < 0) { + throw IllegalArgumentException("Distance must be non-negative") + } + + val numberFormat = NumberFormat.getNumberInstance() + + // Adjust to km if distance is over 1000m (1km) + return if (distance >= 1000) { + numberFormat.maximumFractionDigits = 1 + "${numberFormat.format(distance / 1000.0)}km" + } else { + "${numberFormat.format(distance)}m" + } + } + + /** + * Computes the distance between two points. + * + * @param point1 LatLng type point1 + * @param point2 LatLng type point2 + * @return distance between the points in meters + * @throws NullPointerException if one or both the points are null + */ + @JvmStatic + fun computeDistanceBetween(point1: LatLng, point2: LatLng): Double { + return computeAngleBetween(point1, point2) * 6371009.0 // Earth's radius in meters + } + + /** + * Computes angle between two points + * + * @param point1 one of the two end points + * @param point2 one of the two end points + * @return Angle in radians + * @throws NullPointerException if one or both the points are null + */ + @JvmStatic + private fun computeAngleBetween(point1: LatLng, point2: LatLng): Double { + return distanceRadians( + Math.toRadians(point1.latitude), + Math.toRadians(point1.longitude), + Math.toRadians(point2.latitude), + Math.toRadians(point2.longitude) + ) + } + + /** + * Computes arc length between 2 points + * + * @param lat1 Latitude of point A + * @param lng1 Longitude of point A + * @param lat2 Latitude of point B + * @param lng2 Longitude of point B + * @return Arc length between the points + */ + @JvmStatic + private fun distanceRadians(lat1: Double, lng1: Double, lat2: Double, lng2: Double): Double { + return arcHav(havDistance(lat1, lat2, lng1 - lng2)) + } + + /** + * Computes inverse of haversine + * + * @param x Angle in radian + * @return Inverse of haversine + */ + @JvmStatic + private fun arcHav(x: Double): Double { + return 2.0 * asin(sqrt(x)) + } + + /** + * Computes distance between two points that are on same Longitude + * + * @param lat1 Latitude of point A + * @param lat2 Latitude of point B + * @param longitude Longitude on which they lie + * @return Arc length between points + */ + @JvmStatic + private fun havDistance(lat1: Double, lat2: Double, longitude: Double): Double { + return hav(lat1 - lat2) + hav(longitude) * cos(lat1) * cos(lat2) + } + + /** + * Computes haversine + * + * @param x Angle in radians + * @return Haversine of x + */ + @JvmStatic + private fun hav(x: Double): Double { + val sinHalf = sin(x * 0.5) + return sinHalf * sinHalf + } + + /** + * Computes bearing between the two given points + * + * @see Bearing + * @param point1 Coordinates of first point + * @param point2 Coordinates of second point + * @return Bearing between the two end points in degrees + * @throws NullPointerException if one or both the points are null + */ + @JvmStatic + fun computeBearing(point1: LatLng, point2: LatLng): Double { + val diffLongitude = Math.toRadians(point2.longitude - point1.longitude) + val lat1 = Math.toRadians(point1.latitude) + val lat2 = Math.toRadians(point2.latitude) + val y = sin(diffLongitude) * cos(lat2) + val x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(diffLongitude) + val bearing = atan2(y, x) + return (Math.toDegrees(bearing) + 360) % 360 + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java deleted file mode 100644 index 01a885538..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java +++ /dev/null @@ -1,58 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.location.LatLng; -import timber.log.Timber; - -public class LocationUtils { - public static final double RADIUS_OF_EARTH_KM = 6371.0; // Earth's radius in kilometers - - public static LatLng deriveUpdatedLocationFromSearchQuery(String customQuery) { - LatLng latLng = null; - final int indexOfPrefix = customQuery.indexOf("Point("); - if (indexOfPrefix == -1) { - Timber.e("Invalid prefix index - Seems like user has entered an invalid query"); - return latLng; - } - final int indexOfSuffix = customQuery.indexOf(")\"", indexOfPrefix); - if (indexOfSuffix == -1) { - Timber.e("Invalid suffix index - Seems like user has entered an invalid query"); - return latLng; - } - String latLngString = customQuery.substring(indexOfPrefix+"Point(".length(), indexOfSuffix); - if (latLngString.isEmpty()) { - return null; - } - - String latLngArray[] = latLngString.split(" "); - if (latLngArray.length != 2) { - return null; - } - - try { - latLng = new LatLng(Double.parseDouble(latLngArray[1].trim()), - Double.parseDouble(latLngArray[0].trim()), 1f); - }catch (Exception e){ - Timber.e("Error while parsing user entered lat long: %s", e); - } - - return latLng; - } - - - public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) { - double lat1Rad = Math.toRadians(lat1); - double lon1Rad = Math.toRadians(lon1); - double lat2Rad = Math.toRadians(lat2); - double lon2Rad = Math.toRadians(lon2); - - // Haversine formula - double dlon = lon2Rad - lon1Rad; - double dlat = lat2Rad - lat1Rad; - double a = Math.pow(Math.sin(dlat / 2), 2) + Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.pow(Math.sin(dlon / 2), 2); - double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - double distance = RADIUS_OF_EARTH_KM * c; - - return distance; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.kt new file mode 100644 index 000000000..2df42270e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.kt @@ -0,0 +1,63 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.location.LatLng +import timber.log.Timber +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt + +object LocationUtils { + const val RADIUS_OF_EARTH_KM = 6371.0 // Earth's radius in kilometers + + @JvmStatic + fun deriveUpdatedLocationFromSearchQuery(customQuery: String): LatLng? { + var latLng: LatLng? = null + val indexOfPrefix = customQuery.indexOf("Point(") + if (indexOfPrefix == -1) { + Timber.e("Invalid prefix index - Seems like user has entered an invalid query") + return latLng + } + val indexOfSuffix = customQuery.indexOf(")\"", indexOfPrefix) + if (indexOfSuffix == -1) { + Timber.e("Invalid suffix index - Seems like user has entered an invalid query") + return latLng + } + val latLngString = customQuery.substring(indexOfPrefix + "Point(".length, indexOfSuffix) + if (latLngString.isEmpty()) { + return null + } + + val latLngArray = latLngString.split(" ") + if (latLngArray.size != 2) { + return null + } + + try { + latLng = LatLng(latLngArray[1].trim().toDouble(), + latLngArray[0].trim().toDouble(), 1f) + } catch (e: Exception) { + Timber.e("Error while parsing user entered lat long: %s", e) + } + + return latLng + } + + @JvmStatic + fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double { + val lat1Rad = Math.toRadians(lat1) + val lon1Rad = Math.toRadians(lon1) + val lat2Rad = Math.toRadians(lat2) + val lon2Rad = Math.toRadians(lon2) + + // Haversine formula + val dlon = lon2Rad - lon1Rad + val dlat = lat2Rad - lat1Rad + val a = Math.pow( + sin(dlat / 2), 2.0) + cos(lat1Rad) * cos(lat2Rad) * Math.pow(sin(dlon / 2), 2.0 + ) + val c = 2 * atan2(sqrt(a), sqrt(1 - a)) + + return RADIUS_OF_EARTH_KM * c + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java deleted file mode 100644 index d3b5bd0e2..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java +++ /dev/null @@ -1,33 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.location.LocationUpdateListener; -import timber.log.Timber; - -public class MapUtils { - public static final float ZOOM_LEVEL = 14f; - public static final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.005; - public static final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.004; - public static final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; - public static final float ZOOM_OUT = 0f; - - public static final LatLng defaultLatLng = new fr.free.nrw.commons.location.LatLng(51.50550,-0.07520,1f); - - public static void registerUnregisterLocationListener(final boolean removeLocationListener, LocationServiceManager locationManager, LocationUpdateListener locationUpdateListener) { - try { - if (removeLocationListener) { - locationManager.unregisterLocationManager(); - locationManager.removeLocationListener(locationUpdateListener); - Timber.d("Location service manager unregistered and removed"); - } else { - locationManager.addLocationListener(locationUpdateListener); - locationManager.registerLocationManager(); - Timber.d("Location service manager added and registered"); - } - }catch (final Exception e){ - Timber.e(e); - //Broadcasts are tricky, should be catchedonR - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.kt new file mode 100644 index 000000000..adc3a5d90 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.kt @@ -0,0 +1,39 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.location.LocationServiceManager +import fr.free.nrw.commons.location.LocationUpdateListener +import timber.log.Timber + +object MapUtils { + const val ZOOM_LEVEL = 14f + const val CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.005 + const val CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.004 + const val NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE" + const val ZOOM_OUT = 0f + + @JvmStatic + val defaultLatLng = LatLng(51.50550, -0.07520, 1f) + + @JvmStatic + fun registerUnregisterLocationListener( + removeLocationListener: Boolean, + locationManager: LocationServiceManager, + locationUpdateListener: LocationUpdateListener + ) { + try { + if (removeLocationListener) { + locationManager.unregisterLocationManager() + locationManager.removeLocationListener(locationUpdateListener) + Timber.d("Location service manager unregistered and removed") + } else { + locationManager.addLocationListener(locationUpdateListener) + locationManager.registerLocationManager() + Timber.d("Location service manager added and registered") + } + } catch (e: Exception) { + Timber.e(e) + // Broadcasts are tricky, should be caught on onR + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java deleted file mode 100644 index 8eb875bb5..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java +++ /dev/null @@ -1,29 +0,0 @@ -package fr.free.nrw.commons.utils; - -import org.apache.commons.lang3.StringUtils; - -import java.util.ArrayList; -import java.util.List; - -public class MediaDataExtractorUtil { - /** - * Extracts a list of categories from | separated category string - * - * @param source - * @return - */ - public static List extractCategoriesFromList(String source) { - if (StringUtils.isBlank(source)) { - return new ArrayList<>(); - } - String[] cats = source.split("\\|"); - List categories = new ArrayList<>(); - for (String category : cats) { - if (!StringUtils.isBlank(category.trim())) { - categories.add(category); - } - } - return categories; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt new file mode 100644 index 000000000..9e46525da --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.utils + +import org.apache.commons.lang3.StringUtils + +import java.util.ArrayList + +object MediaDataExtractorUtil { + + /** + * Extracts a list of categories from | separated category string + * + * @param source + * @return + */ + @JvmStatic + fun extractCategoriesFromList(source: String): List { + if (source.isBlank()) { + return emptyList() + } + val cats = source.split("|") + val categories = mutableListOf() + for (category in cats) { + if (category.trim().isNotBlank()) { + categories.add(category) + } + } + return categories + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.java deleted file mode 100644 index bc6e6883f..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.java +++ /dev/null @@ -1,51 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; - -import androidx.coordinatorlayout.widget.CoordinatorLayout; - -import com.google.android.material.floatingactionbutton.FloatingActionButton; - -public class NearbyFABUtils { - /* - * Add anchors back before making them visible again. - * */ - public static void addAnchorToBigFABs(FloatingActionButton floatingActionButton, int anchorID) { - CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams - (ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); - params.setAnchorId(anchorID); - params.anchorGravity = Gravity.TOP|Gravity.RIGHT|Gravity.END; - floatingActionButton.setLayoutParams(params); - } - - /* - * Add anchors back before making them visible again. Big and small fabs have different anchor - * gravities, therefore the are two methods. - * */ - public static void addAnchorToSmallFABs(FloatingActionButton floatingActionButton, int anchorID) { - CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams - (ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); - params.setAnchorId(anchorID); - params.anchorGravity = Gravity.CENTER_HORIZONTAL; - floatingActionButton.setLayoutParams(params); - } - - /* - * We are not able to hide FABs without removing anchors, this method removes anchors - * */ - public static void removeAnchorFromFAB(FloatingActionButton floatingActionButton) { - //get rid of anchors - //Somehow this was the only way https://stackoverflow.com/questions/32732932 - // /floatingactionbutton-visible-for-sometime-even-if-visibility-is-set-to-gone - CoordinatorLayout.LayoutParams param = (CoordinatorLayout.LayoutParams) floatingActionButton - .getLayoutParams(); - param.setAnchorId(View.NO_ID); - // If we don't set them to zero, then they become visible for a moment on upper left side - param.width = 0; - param.height = 0; - floatingActionButton.setLayoutParams(param); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.kt new file mode 100644 index 000000000..61b95a413 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.kt @@ -0,0 +1,55 @@ +package fr.free.nrw.commons.utils + +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.floatingactionbutton.FloatingActionButton + +object NearbyFABUtils { + + /* + * Add anchors back before making them visible again. + */ + @JvmStatic + fun addAnchorToBigFABs(floatingActionButton: FloatingActionButton, anchorID: Int) { + val params = CoordinatorLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + params.anchorId = anchorID + params.anchorGravity = Gravity.TOP or Gravity.RIGHT or Gravity.END + floatingActionButton.layoutParams = params + } + + /* + * Add anchors back before making them visible again. Big and small fabs have different anchor + * gravities, therefore there are two methods. + */ + @JvmStatic + fun addAnchorToSmallFABs(floatingActionButton: FloatingActionButton, anchorID: Int) { + val params = CoordinatorLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + params.anchorId = anchorID + params.anchorGravity = Gravity.CENTER_HORIZONTAL + floatingActionButton.layoutParams = params + } + + /* + * We are not able to hide FABs without removing anchors, this method removes anchors. + */ + @JvmStatic + fun removeAnchorFromFAB(floatingActionButton: FloatingActionButton) { + // get rid of anchors + // Somehow this was the only way https://stackoverflow.com/questions/32732932 + // floatingactionbutton-visible-for-sometime-even-if-visibility-is-set-to-gone + val params = floatingActionButton.layoutParams as CoordinatorLayout.LayoutParams + params.anchorId = View.NO_ID + // If we don't set them to zero, then they become visible for a moment on upper left side + params.width = 0 + params.height = 0 + floatingActionButton.layoutParams = params + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java deleted file mode 100644 index ce64cb031..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java +++ /dev/null @@ -1,94 +0,0 @@ -package fr.free.nrw.commons.utils; - - -import android.annotation.SuppressLint; -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.telephony.TelephonyManager; - -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.utils.model.NetworkConnectionType; - -public class NetworkUtils { - - /** - * https://developer.android.com/training/monitoring-device-state/connectivity-monitoring#java - * Check if internet connection is established. - * - * @param context context passed to this method could be null. - * @return Returns current internet connection status. Returns false if null context was passed. - */ - @SuppressLint("MissingPermission") - public static boolean isInternetConnectionEstablished(@Nullable Context context) { - if (context == null) { - return false; - } - - NetworkInfo activeNetwork = getNetworkInfo(context); - return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); - } - - /** - * Detect network connection type - */ - static NetworkConnectionType getNetworkType(Context context) { - TelephonyManager telephonyManager = (TelephonyManager) context.getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE); - if (telephonyManager == null) { - return NetworkConnectionType.UNKNOWN; - } - - NetworkInfo networkInfo = getNetworkInfo(context); - if (networkInfo == null) { - return NetworkConnectionType.UNKNOWN; - } - - int network = networkInfo.getType(); - if (network == ConnectivityManager.TYPE_WIFI) { - return NetworkConnectionType.WIFI; - } - - // TODO for Android 12+ request permission from user is mandatory - /* - int mobileNetwork = telephonyManager.getNetworkType(); - switch (mobileNetwork) { - case TelephonyManager.NETWORK_TYPE_GPRS: - case TelephonyManager.NETWORK_TYPE_EDGE: - case TelephonyManager.NETWORK_TYPE_CDMA: - case TelephonyManager.NETWORK_TYPE_1xRTT: - return NetworkConnectionType.TWO_G; - case TelephonyManager.NETWORK_TYPE_HSDPA: - case TelephonyManager.NETWORK_TYPE_UMTS: - case TelephonyManager.NETWORK_TYPE_HSUPA: - case TelephonyManager.NETWORK_TYPE_HSPA: - case TelephonyManager.NETWORK_TYPE_EHRPD: - case TelephonyManager.NETWORK_TYPE_EVDO_0: - case TelephonyManager.NETWORK_TYPE_EVDO_A: - case TelephonyManager.NETWORK_TYPE_EVDO_B: - return NetworkConnectionType.THREE_G; - case TelephonyManager.NETWORK_TYPE_LTE: - case TelephonyManager.NETWORK_TYPE_HSPAP: - return NetworkConnectionType.FOUR_G; - default: - return NetworkConnectionType.UNKNOWN; - } - */ - return NetworkConnectionType.UNKNOWN; - } - - /** - * Extracted private method to get nullable network info - */ - @Nullable - private static NetworkInfo getNetworkInfo(Context context) { - ConnectivityManager connectivityManager = - (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); - - if (connectivityManager == null) { - return null; - } - - return connectivityManager.getActiveNetworkInfo(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.kt new file mode 100644 index 000000000..98fde9ef7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.kt @@ -0,0 +1,85 @@ +package fr.free.nrw.commons.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkInfo +import android.telephony.TelephonyManager + +import fr.free.nrw.commons.utils.model.NetworkConnectionType + +object NetworkUtils { + + /** + * https://developer.android.com/training/monitoring-device-state/connectivity-monitoring#java + * Check if internet connection is established. + * + * @param context context passed to this method could be null. + * @return Returns current internet connection status. Returns false if null context was passed. + */ + @SuppressLint("MissingPermission") + @JvmStatic + fun isInternetConnectionEstablished(context: Context?): Boolean { + if (context == null) { + return false + } + + val activeNetwork = getNetworkInfo(context) + return activeNetwork != null && activeNetwork.isConnectedOrConnecting + } + + /** + * Detect network connection type + */ + @JvmStatic + fun getNetworkType(context: Context): NetworkConnectionType { + val telephonyManager = context.applicationContext.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager + ?: return NetworkConnectionType.UNKNOWN + + val networkInfo = getNetworkInfo(context) + ?: return NetworkConnectionType.UNKNOWN + + val network = networkInfo.type + if (network == ConnectivityManager.TYPE_WIFI) { + return NetworkConnectionType.WIFI + } + + // TODO for Android 12+ request permission from user is mandatory + /* + val mobileNetwork = telephonyManager.networkType + return when (mobileNetwork) { + TelephonyManager.NETWORK_TYPE_GPRS, + TelephonyManager.NETWORK_TYPE_EDGE, + TelephonyManager.NETWORK_TYPE_CDMA, + TelephonyManager.NETWORK_TYPE_1xRTT -> NetworkConnectionType.TWO_G + + TelephonyManager.NETWORK_TYPE_HSDPA, + TelephonyManager.NETWORK_TYPE_UMTS, + TelephonyManager.NETWORK_TYPE_HSUPA, + TelephonyManager.NETWORK_TYPE_HSPA, + TelephonyManager.NETWORK_TYPE_EHRPD, + TelephonyManager.NETWORK_TYPE_EVDO_0, + TelephonyManager.NETWORK_TYPE_EVDO_A, + TelephonyManager.NETWORK_TYPE_EVDO_B -> NetworkConnectionType.THREE_G + + TelephonyManager.NETWORK_TYPE_LTE, + TelephonyManager.NETWORK_TYPE_HSPAP -> NetworkConnectionType.FOUR_G + + else -> NetworkConnectionType.UNKNOWN + } + */ + return NetworkConnectionType.UNKNOWN + } + + /** + * Extracted private method to get nullable network info + */ + @JvmStatic + private fun getNetworkInfo(context: Context): NetworkInfo? { + val connectivityManager = + context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager + ?: return null + + return connectivityManager.activeNetworkInfo + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java deleted file mode 100644 index 692194234..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java +++ /dev/null @@ -1,224 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.Manifest; -import android.Manifest.permission; -import android.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.provider.Settings; -import android.widget.Toast; -import androidx.annotation.StringRes; -import androidx.core.content.ContextCompat; -import com.karumi.dexter.Dexter; -import com.karumi.dexter.MultiplePermissionsReport; -import com.karumi.dexter.PermissionToken; -import com.karumi.dexter.listener.PermissionRequest; -import com.karumi.dexter.listener.multi.MultiplePermissionsListener; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.upload.UploadActivity; -import java.util.List; - -public class PermissionUtils { - public static String[] PERMISSIONS_STORAGE = getPermissionsStorage(); - - static String[] getPermissionsStorage() { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - return new String[]{ Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.ACCESS_MEDIA_LOCATION }; - } - if(Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) { - return new String[]{ Manifest.permission.READ_MEDIA_IMAGES, - Manifest. permission.ACCESS_MEDIA_LOCATION }; - } - if(Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { - return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.ACCESS_MEDIA_LOCATION }; - } - if(Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.ACCESS_MEDIA_LOCATION }; - } - return new String[]{ - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE }; - } - - /** - * This method can be used by any activity which requires a permission which has been - * blocked(marked never ask again by the user) It open the app settings from where the user can - * manually give us the required permission. - * - * @param activity The Activity which requires a permission which has been blocked - */ - private static void askUserToManuallyEnablePermissionFromSettings(final Activity activity) { - final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - final Uri uri = Uri.fromParts("package", activity.getPackageName(), null); - intent.setData(uri); - activity.startActivity(intent); - } - - /** - * Checks whether the app already has a particular permission - * - * @param activity The Activity context to check permissions against - * @param permissions An array of permission strings to check - * @return `true if the app has all the specified permissions, `false` otherwise - */ - public static boolean hasPermission(final Activity activity, final String[] permissions) { - boolean hasPermission = true; - for(final String permission : permissions) { - hasPermission = hasPermission && - ContextCompat.checkSelfPermission(activity, permission) - == PackageManager.PERMISSION_GRANTED; - } - return hasPermission; - } - - public static boolean hasPartialAccess(final Activity activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - return ContextCompat.checkSelfPermission(activity, - permission.READ_MEDIA_VISUAL_USER_SELECTED - ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission( - activity, permission.READ_MEDIA_IMAGES - ) == PackageManager.PERMISSION_DENIED; - } - return false; - } - - /** - * Checks for a particular permission and runs the runnable to perform an action when the - * permission is granted Also, it shows a rationale if needed - *

- * rationaleTitle and rationaleMessage can be invalid @StringRes. If the value is -1 then no - * permission rationale will be displayed and permission would be requested - *

- * Sample usage: - *

- * PermissionUtils.checkPermissionsAndPerformAction(activity, - * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), - * R.string.storage_permission_title, R.string.write_storage_permission_rationale); - *

- * If you don't want the permission rationale to be shown then use: - *

- * PermissionUtils.checkPermissionsAndPerformAction(activity, - * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), - 1, -1); - * - * @param activity activity requesting permissions - * @param permissions the permissions array being requests - * @param onPermissionGranted the runnable to be executed when the permission is granted - * @param rationaleTitle rationale title to be displayed when permission was denied. It can - * be an invalid @StringRes - * @param rationaleMessage rationale message to be displayed when permission was denied. It - * can be an invalid @StringRes - */ - public static void checkPermissionsAndPerformAction( - final Activity activity, - final Runnable onPermissionGranted, - final @StringRes int rationaleTitle, - final @StringRes int rationaleMessage, - final String... permissions - ) { - if (hasPartialAccess(activity)) { - onPermissionGranted.run(); - return; - } - checkPermissionsAndPerformAction(activity, onPermissionGranted, null, - rationaleTitle, rationaleMessage, permissions); - } - - /** - * Checks for a particular permission and runs the corresponding runnables to perform an action - * when the permission is granted/denied Also, it shows a rationale if needed - *

- * Sample usage: - *

- * PermissionUtils.checkPermissionsAndPerformAction(activity, - * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), () -> - * showMessage(), R.string.storage_permission_title, - * R.string.write_storage_permission_rationale); - * - * @param activity activity requesting permissions - * @param permissions the permissions array being requested - * @param onPermissionGranted the runnable to be executed when the permission is granted - * @param onPermissionDenied the runnable to be executed when the permission is denied(but not - * permanently) - * @param rationaleTitle rationale title to be displayed when permission was denied - * @param rationaleMessage rationale message to be displayed when permission was denied - */ - public static void checkPermissionsAndPerformAction( - final Activity activity, - final Runnable onPermissionGranted, - final Runnable onPermissionDenied, - final @StringRes int rationaleTitle, - final @StringRes int rationaleMessage, - final String... permissions - ) { - Dexter.withActivity(activity) - .withPermissions(permissions) - .withListener(new MultiplePermissionsListener() { - @Override - public void onPermissionsChecked(final MultiplePermissionsReport report) { - if (report.areAllPermissionsGranted() || hasPartialAccess(activity)) { - onPermissionGranted.run(); - return; - } - if (report.isAnyPermissionPermanentlyDenied()) { - // permission is denied permanently, we will show user a dialog message. - DialogUtil.showAlertDialog( - activity, activity.getString(rationaleTitle), - activity.getString(rationaleMessage), - activity.getString(R.string.navigation_item_settings), - null, () -> { - askUserToManuallyEnablePermissionFromSettings(activity); - if (activity instanceof UploadActivity) { - ((UploadActivity) activity).setShowPermissionsDialog(true); - } - }, null, null, - !(activity instanceof UploadActivity)); - } else { - if (null != onPermissionDenied) { - onPermissionDenied.run(); - } - } - } - - @Override - public void onPermissionRationaleShouldBeShown( - final List permissions, - final PermissionToken token - ) { - if (rationaleTitle == -1 && rationaleMessage == -1) { - token.continuePermissionRequest(); - return; - } - DialogUtil.showAlertDialog( - activity, activity.getString(rationaleTitle), - activity.getString(rationaleMessage), - activity.getString(android.R.string.ok), - activity.getString(android.R.string.cancel), - () -> { - if (activity instanceof UploadActivity) { - ((UploadActivity) activity).setShowPermissionsDialog(true); - } - token.continuePermissionRequest(); - }, - () -> { - Toast.makeText(activity.getApplicationContext(), - R.string.permissions_are_required_for_functionality, - Toast.LENGTH_LONG - ).show(); - token.cancelPermissionRequest(); - if (activity instanceof UploadActivity) { - activity.finish(); - } - }, null, false - ); - } - }).onSameThread().check(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt new file mode 100644 index 000000000..305388fab --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt @@ -0,0 +1,231 @@ +package fr.free.nrw.commons.utils + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.provider.Settings +import android.widget.Toast +import androidx.core.content.ContextCompat +import com.karumi.dexter.Dexter +import com.karumi.dexter.MultiplePermissionsReport +import com.karumi.dexter.PermissionToken +import com.karumi.dexter.listener.PermissionRequest +import com.karumi.dexter.listener.multi.MultiplePermissionsListener +import fr.free.nrw.commons.R +import fr.free.nrw.commons.upload.UploadActivity + + +object PermissionUtils { + + @JvmStatic + val PERMISSIONS_STORAGE: Array = getPermissionsStorage() + + @JvmStatic + private fun getPermissionsStorage(): Array { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> arrayOf( + Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.ACCESS_MEDIA_LOCATION + ) + Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU -> arrayOf( + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.ACCESS_MEDIA_LOCATION + ) + Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.ACCESS_MEDIA_LOCATION + ) + Build.VERSION.SDK_INT == Build.VERSION_CODES.Q -> arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.ACCESS_MEDIA_LOCATION + ) + else -> arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + } + } + + /** + * This method can be used by any activity which requires a permission which has been + * blocked(marked never ask again by the user) It open the app settings from where the user can + * manually give us the required permission. + * + * @param activity The Activity which requires a permission which has been blocked + */ + @JvmStatic + private fun askUserToManuallyEnablePermissionFromSettings(activity: Activity) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", activity.packageName, null) + } + activity.startActivity(intent) + } + + /** + * Checks whether the app already has a particular permission + * + * @param activity The Activity context to check permissions against + * @param permissions An array of permission strings to check + * @return `true if the app has all the specified permissions, `false` otherwise + */ + @JvmStatic + fun hasPermission(activity: Activity, permissions: Array): Boolean { + return permissions.all { permission -> + ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED + } + } + + /** + * Check if the app has partial access permissions. + */ + @JvmStatic + fun hasPartialAccess(activity: Activity): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + ContextCompat.checkSelfPermission( + activity, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED + ) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + activity, Manifest.permission.READ_MEDIA_IMAGES + ) == PackageManager.PERMISSION_DENIED + } else false + } + + /** + * Checks for a particular permission and runs the runnable to perform an action when the + * permission is granted Also, it shows a rationale if needed + *

+ * rationaleTitle and rationaleMessage can be invalid @StringRes. If the value is -1 then no + * permission rationale will be displayed and permission would be requested + *

+ * Sample usage: + *

+ * PermissionUtils.checkPermissionsAndPerformAction(activity, + * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), + * R.string.storage_permission_title, R.string.write_storage_permission_rationale); + *

+ * If you don't want the permission rationale to be shown then use: + *

+ * PermissionUtils.checkPermissionsAndPerformAction(activity, + * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), - 1, -1); + * + * @param activity activity requesting permissions + * @param permissions the permissions array being requests + * @param onPermissionGranted the runnable to be executed when the permission is granted + * @param rationaleTitle rationale title to be displayed when permission was denied. It can + * be an invalid @StringRes + * @param rationaleMessage rationale message to be displayed when permission was denied. It + * can be an invalid @StringRes + */ + @JvmStatic + fun checkPermissionsAndPerformAction( + activity: Activity, + onPermissionGranted: Runnable, + rationaleTitle: Int, + rationaleMessage: Int, + vararg permissions: String + ) { + if (hasPartialAccess(activity)) { + Thread(onPermissionGranted).start() + return + } + checkPermissionsAndPerformAction( + activity, onPermissionGranted, null, rationaleTitle, rationaleMessage, *permissions + ) + } + + /** + * Checks for a particular permission and runs the corresponding runnables to perform an action + * when the permission is granted/denied Also, it shows a rationale if needed + *

+ * Sample usage: + *

+ * PermissionUtils.checkPermissionsAndPerformAction(activity, + * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), () -> + * showMessage(), R.string.storage_permission_title, + * R.string.write_storage_permission_rationale); + * + * @param activity activity requesting permissions + * @param permissions the permissions array being requested + * @param onPermissionGranted the runnable to be executed when the permission is granted + * @param onPermissionDenied the runnable to be executed when the permission is denied(but not + * permanently) + * @param rationaleTitle rationale title to be displayed when permission was denied + * @param rationaleMessage rationale message to be displayed when permission was denied + */ + @JvmStatic + fun checkPermissionsAndPerformAction( + activity: Activity, + onPermissionGranted: Runnable, + onPermissionDenied: Runnable? = null, + rationaleTitle: Int, + rationaleMessage: Int, + vararg permissions: String + ) { + Dexter.withActivity(activity) + .withPermissions(*permissions) + .withListener(object : MultiplePermissionsListener { + override fun onPermissionsChecked(report: MultiplePermissionsReport) { + when { + report.areAllPermissionsGranted() || hasPartialAccess(activity) -> + Thread(onPermissionGranted).start() + report.isAnyPermissionPermanentlyDenied -> { + DialogUtil.showAlertDialog( + activity, + activity.getString(rationaleTitle), + activity.getString(rationaleMessage), + activity.getString(R.string.navigation_item_settings), + null, + { + askUserToManuallyEnablePermissionFromSettings(activity) + if (activity is UploadActivity) { + activity.isShowPermissionsDialog = true + } + }, + null, null, activity !is UploadActivity + ) + } + else -> Thread(onPermissionDenied).start() + } + } + + override fun onPermissionRationaleShouldBeShown( + permissions: List, token: PermissionToken + ) { + if (rationaleTitle == -1 && rationaleMessage == -1) { + token.continuePermissionRequest() + return + } + DialogUtil.showAlertDialog( + activity, + activity.getString(rationaleTitle), + activity.getString(rationaleMessage), + activity.getString(android.R.string.ok), + activity.getString(android.R.string.cancel), + { + if (activity is UploadActivity) { + activity.setShowPermissionsDialog(true) + } + token.continuePermissionRequest() + }, + { + Toast.makeText( + activity.applicationContext, + R.string.permissions_are_required_for_functionality, + Toast.LENGTH_LONG + ).show() + token.cancelPermissionRequest() + if (activity is UploadActivity) { + activity.finish() + } + }, + null, false + ) + } + }).onSameThread().check() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java deleted file mode 100644 index f1022a041..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java +++ /dev/null @@ -1,55 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.nearby.Sitelinks; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import fr.free.nrw.commons.location.LatLng; - -public class PlaceUtils { - - public static LatLng latLngFromPointString(String pointString) { - double latitude; - double longitude; - Matcher matcher = Pattern.compile("Point\\(([^ ]+) ([^ ]+)\\)").matcher(pointString); - if (!matcher.find()) { - return null; - } - try { - longitude = Double.parseDouble(matcher.group(1)); - latitude = Double.parseDouble(matcher.group(2)); - } catch (NumberFormatException e) { - return null; - } - - return new LatLng(latitude, longitude, 0); - } - - /** - * Turns a Media list to a Place list by creating a new list in Place type - * @param mediaList - * @return - */ - public static List mediaToExplorePlace( List mediaList) { - List explorePlaceList = new ArrayList<>(); - for (Media media :mediaList) { - explorePlaceList.add(new Place(media.getFilename(), - media.getFallbackDescription(), - media.getCoordinates(), - media.getCategories().toString(), - new Sitelinks.Builder() - .setCommonsLink(media.getPageTitle().getCanonicalUri()) - .setWikipediaLink("") // we don't necessarily have them, can be fetched later - .setWikidataLink("") // we don't necessarily have them, can be fetched later - .build(), - media.getImageUrl(), - media.getThumbUrl(), - "")); - } - return explorePlaceList; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.kt new file mode 100644 index 000000000..907420f21 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.kt @@ -0,0 +1,50 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.nearby.Sitelinks + +object PlaceUtils { + + @JvmStatic + fun latLngFromPointString(pointString: String): LatLng? { + val matcher = Regex("Point\\(([^ ]+) ([^ ]+)\\)").find(pointString) ?: return null + return try { + val longitude = matcher.groupValues[1].toDouble() + val latitude = matcher.groupValues[2].toDouble() + LatLng(latitude, longitude, 0.0F) + } catch (e: NumberFormatException) { + null + } + } + + /** + * Turns a Media list to a Place list by creating a new list in Place type + * @param mediaList + * @return + */ + @JvmStatic + fun mediaToExplorePlace(mediaList: List): List { + val explorePlaceList = mutableListOf() + for (media in mediaList) { + explorePlaceList.add( + Place( + media.filename, + media.fallbackDescription, + media.coordinates, + media.categories.toString(), + Sitelinks.Builder() + .setCommonsLink(media.pageTitle.canonicalUri) + .setWikipediaLink("") // we don't necessarily have them, can be fetched later + .setWikidataLink("") // we don't necessarily have them, can be fetched later + .build(), + media.imageUrl, + media.thumbUrl, + "" + ) + ) + } + return explorePlaceList + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java deleted file mode 100644 index 314467972..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java +++ /dev/null @@ -1,90 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.category.CategoryItem; -import java.util.Comparator; - -public class StringSortingUtils { - - private StringSortingUtils() { - //no-op - } - - /** - * Returns Comparator for sorting strings by their similarity to the filter. - * By using this Comparator we get results - * from the highest to the lowest similarity with the filter. - * - * @param filter String to compare similarity with - * @return Comparator with string similarity - */ - public static Comparator sortBySimilarity(final String filter) { - return (firstItem, secondItem) -> { - double firstItemSimilarity = calculateSimilarity(firstItem.getName(), filter); - double secondItemSimilarity = calculateSimilarity(secondItem.getName(), filter); - return (int) Math.signum(secondItemSimilarity - firstItemSimilarity); - }; - } - - - /** - * Determines String similarity between str1 and str2 on scale from 0.0 to 1.0 - * @param str1 String 1 - * @param str2 String 2 - * @return Double between 0.0 and 1.0 that reflects string similarity - */ - private static double calculateSimilarity(String str1, String str2) { - int longerLength = Math.max(str1.length(), str2.length()); - - if (longerLength == 0) return 1.0; - - int distanceBetweenStrings = levenshteinDistance(str1, str2); - return (longerLength - distanceBetweenStrings) / (double) longerLength; - } - - /** - * Levershtein distance algorithm - * https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Java - * - * @param str1 String 1 - * @param str2 String 2 - * @return Number of characters the strings differ by - */ - private static int levenshteinDistance(String str1, String str2) { - if (str1.equals(str2)) return 0; - if (str1.length() == 0) return str2.length(); - if (str2.length() == 0) return str1.length(); - - int[] cost = new int[str1.length() + 1]; - int[] newcost = new int[str1.length() + 1]; - - // initial cost of skipping prefix in str1 - for (int i = 0; i < cost.length; i++) cost[i] = i; - - // transformation cost for each letter in str2 - for (int j = 1; j <= str2.length(); j++) { - // initial cost of skipping prefix in String str2 - newcost[0] = j; - - // transformation cost for each letter in str1 - for(int i = 1; i < cost.length; i++) { - // matching current letters in both strings - int match = (str1.charAt(i - 1) == str2.charAt(j - 1)) ? 0 : 1; - - // computing cost for each transformation - int cost_replace = cost[i - 1] + match; - int cost_insert = cost[i] + 1; - int cost_delete = newcost[i - 1] + 1; - - // keep minimum cost - newcost[i] = Math.min(Math.min(cost_insert, cost_delete), cost_replace); - } - - int[] tmp = cost; - cost = newcost; - newcost = tmp; - } - - // the distance is the cost for transforming all letters in both strings - return cost[str1.length()]; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.kt new file mode 100644 index 000000000..d9f813ae0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.kt @@ -0,0 +1,86 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.category.CategoryItem +import java.lang.Math.signum +import java.util.Comparator + + +object StringSortingUtils { + + /** + * Returns Comparator for sorting strings by their similarity to the filter. + * By using this Comparator we get results + * from the highest to the lowest similarity with the filter. + * + * @param filter String to compare similarity with + * @return Comparator with string similarity + */ + @JvmStatic + fun sortBySimilarity(filter: String): Comparator { + return Comparator { firstItem, secondItem -> + val firstItemSimilarity = calculateSimilarity(firstItem.name, filter) + val secondItemSimilarity = calculateSimilarity(secondItem.name, filter) + signum(secondItemSimilarity - firstItemSimilarity).toInt() + } + } + + /** + * Determines String similarity between str1 and str2 on scale from 0.0 to 1.0 + * @param str1 String 1 + * @param str2 String 2 + * @return Double between 0.0 and 1.0 that reflects string similarity + */ + private fun calculateSimilarity(str1: String, str2: String): Double { + val longerLength = maxOf(str1.length, str2.length) + + if (longerLength == 0) return 1.0 + + val distanceBetweenStrings = levenshteinDistance(str1, str2) + return (longerLength - distanceBetweenStrings) / longerLength.toDouble() + } + + /** + * Levenshtein distance algorithm + * https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Java + * + * @param str1 String 1 + * @param str2 String 2 + * @return Number of characters the strings differ by + */ + private fun levenshteinDistance(str1: String, str2: String): Int { + if (str1 == str2) return 0 + if (str1.isEmpty()) return str2.length + if (str2.isEmpty()) return str1.length + + var cost = IntArray(str1.length + 1) { it } + var newCost = IntArray(str1.length + 1) + + // transformation cost for each letter in str2 + for (j in 1..str2.length) { + // initial cost of skipping prefix in String str2 + newCost[0] = j + + // transformation cost for each letter in str1 + for (i in 1..str1.length) { + // matching current letters in both strings + val match = if (str1[i - 1] == str2[j - 1]) 0 else 1 + + // computing cost for each transformation + val costReplace = cost[i - 1] + match + val costInsert = cost[i] + 1 + val costDelete = newCost[i - 1] + 1 + + // keep minimum cost + newCost[i] = minOf(costInsert, costDelete, costReplace) + } + + // swap cost arrays + val tmp = cost + cost = newCost + newCost = tmp + } + + // the distance is the cost for transforming all letters in both strings + return cost[str1.length] + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.java deleted file mode 100644 index a5bb6038e..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.java +++ /dev/null @@ -1,38 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.os.Build; -import android.text.Html; -import android.text.Spanned; -import android.text.SpannedString; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public final class StringUtil { - - /** - * @param source String that may contain HTML tags. - * @return returned Spanned string that may contain spans parsed from the HTML source. - */ - @NonNull public static Spanned fromHtml(@Nullable String source) { - if (source == null) { - return new SpannedString(""); - } - if (!source.contains("<") && !source.contains("&")) { - // If the string doesn't contain any hints of HTML entities, then skip the expensive - // processing that fromHtml() performs. - return new SpannedString(source); - } - source = source.replaceAll("‎", "\u200E") - .replaceAll("‏", "\u200F") - .replaceAll("&", "&"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY); - } else { - //noinspection deprecation - return Html.fromHtml(source); - } - } - - private StringUtil() { - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt new file mode 100644 index 000000000..b3c58d8b2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt @@ -0,0 +1,37 @@ +package fr.free.nrw.commons.utils + +import android.os.Build +import android.text.Html +import android.text.Spanned +import android.text.SpannedString + +object StringUtil { + + /** + * @param source String that may contain HTML tags. + * @return returned Spanned string that may contain spans parsed from the HTML source. + */ + @JvmStatic + fun fromHtml(source: String?): Spanned { + if (source == null) { + return SpannedString("") + } + if (!source.contains("<") && !source.contains("&")) { + // If the string doesn't contain any hints of HTML entities, then skip the expensive + // processing that fromHtml() performs. + return SpannedString(source) + } + val processedSource = source + .replace("‎", "\u200E") + .replace("‏", "\u200F") + .replace("&", "&") + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(processedSource, Html.FROM_HTML_MODE_LEGACY) + } else { + //noinspection deprecation + @Suppress("DEPRECATION") + Html.fromHtml(processedSource) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java deleted file mode 100644 index 7ea7ef467..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java +++ /dev/null @@ -1,74 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; -import android.content.res.Resources; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.cardview.widget.CardView; - -import timber.log.Timber; - -/** - * A card view which informs onSwipe events to its child - */ -public abstract class SwipableCardView extends CardView { - float x1, x2; - private static final float MINIMUM_THRESHOLD_FOR_SWIPE = 100; - - public SwipableCardView(@NonNull Context context) { - super(context); - interceptOnTouchListener(); - } - - public SwipableCardView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - interceptOnTouchListener(); - } - - public SwipableCardView(@NonNull Context context, @Nullable AttributeSet attrs, - int defStyleAttr) { - super(context, attrs, defStyleAttr); - interceptOnTouchListener(); - } - - private void interceptOnTouchListener() { - this.setOnTouchListener((v, event) -> { - boolean isSwipe = false; - float deltaX = 0.0f; - Timber.e(event.getAction() + ""); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - x1 = event.getX(); - break; - case MotionEvent.ACTION_UP: - x2 = event.getX(); - deltaX = x2 - x1; - if (deltaX < 0) { - //Right to left swipe - isSwipe = true; - } else if (deltaX > 0) { - //Left to right swipe - isSwipe = true; - } - break; - } - if (isSwipe && (pixelToDp(Math.abs(deltaX)) > MINIMUM_THRESHOLD_FOR_SWIPE)) { - return onSwipe(v); - } - return false; - }); - } - - /** - * abstract function which informs swipe events to those who have inherited from it - */ - public abstract boolean onSwipe(View view); - - private float pixelToDp(float pixels) { - return (pixels / Resources.getSystem().getDisplayMetrics().density); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt new file mode 100644 index 000000000..5a8261c24 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt @@ -0,0 +1,64 @@ +package fr.free.nrw.commons.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Resources +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View + +import androidx.cardview.widget.CardView + +import timber.log.Timber +import kotlin.math.abs + +/** + * A card view which informs onSwipe events to its child + */ +abstract class SwipableCardView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : CardView(context, attrs, defStyleAttr) { + + private var x1 = 0f + private var x2 = 0f + private val MINIMUM_THRESHOLD_FOR_SWIPE = 100f + + init { + interceptOnTouchListener() + } + + @SuppressLint("ClickableViewAccessibility") + private fun interceptOnTouchListener() { + this.setOnTouchListener { v, event -> + var isSwipe = false + var deltaX = 0f + Timber.e(event.action.toString()) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + x1 = event.x + } + MotionEvent.ACTION_UP -> { + x2 = event.x + deltaX = x2 - x1 + isSwipe = deltaX != 0f + } + } + if (isSwipe && pixelToDp(abs(deltaX)) > MINIMUM_THRESHOLD_FOR_SWIPE) { + onSwipe(v) + return@setOnTouchListener true + } + false + } + } + + /** + * abstract function which informs swipe events to those who have inherited from it + */ + abstract fun onSwipe(view: View): Boolean + + private fun pixelToDp(pixels: Float): Float { + return pixels / Resources.getSystem().displayMetrics.density + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.java deleted file mode 100644 index aa60a7aa8..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.java +++ /dev/null @@ -1,49 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; -import android.content.res.Configuration; - -import javax.inject.Inject; -import javax.inject.Named; - -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.settings.Prefs; - -public class SystemThemeUtils { - - private Context context; - private JsonKvStore applicationKvStore; - - public static final String THEME_MODE_DEFAULT = "0"; - public static final String THEME_MODE_DARK = "1"; - public static final String THEME_MODE_LIGHT = "2"; - - @Inject - public SystemThemeUtils(Context context, @Named("default_preferences") JsonKvStore applicationKvStore) { - this.context = context; - this.applicationKvStore = applicationKvStore; - } - - // Return true is system wide dark theme is enabled else false - public boolean getSystemDefaultThemeBool(String theme) { - if (theme.equals(THEME_MODE_DARK)) { - return true; - } else if (theme.equals(THEME_MODE_DEFAULT)) { - return getSystemDefaultThemeBool(getSystemDefaultTheme()); - } - return false; - } - - // Returns the default system wide theme - public String getSystemDefaultTheme() { - return (context.getResources().getConfiguration().uiMode & - Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES ? THEME_MODE_DARK : THEME_MODE_LIGHT; - } - - // Returns true if the device is in night mode or false otherwise - public boolean isDeviceInNightMode() { - return getSystemDefaultThemeBool( - applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme())); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt new file mode 100644 index 000000000..f4b1f2625 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt @@ -0,0 +1,52 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.content.res.Configuration + +import javax.inject.Inject +import javax.inject.Named + +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.settings.Prefs + + +class SystemThemeUtils @Inject constructor( + private val context: Context, + @Named("default_preferences") private val applicationKvStore: JsonKvStore +) { + + companion object { + const val THEME_MODE_DEFAULT = "0" + const val THEME_MODE_DARK = "1" + const val THEME_MODE_LIGHT = "2" + } + + // Return true if system wide dark theme is enabled else false + private fun getSystemDefaultThemeBool(theme: String): Boolean { + return when (theme) { + THEME_MODE_DARK -> true + THEME_MODE_DEFAULT -> getSystemDefaultThemeBool(getSystemDefaultTheme()) + else -> false + } + } + + // Returns the default system wide theme + private fun getSystemDefaultTheme(): String { + return if ( + ( + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES + ) { + THEME_MODE_DARK + } else { + THEME_MODE_LIGHT + } + } + + // Returns true if the device is in night mode or false otherwise + fun isDeviceInNightMode(): Boolean { + return getSystemDefaultThemeBool( + applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme()) + ) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.java deleted file mode 100644 index acb6afbaa..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.util.DisplayMetrics; - -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; - -import java.util.ArrayList; -import java.util.List; - -public class UiUtils { - - /** - * Draws a vectorial image onto a bitmap. - * @param vectorDrawable vectorial image - * @return bitmap representation of the vectorial image - */ - public static Bitmap getBitmap(VectorDrawableCompat vectorDrawable) { - Bitmap bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(), - vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - vectorDrawable.draw(canvas); - return bitmap; - } - - /** - * Converts dp unit to equivalent pixels. - * @param dp density independent pixels - * @param context Context to access display metrics - * @return px equivalent to dp value - */ - public static float convertDpToPixel(float dp, Context context) { - DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - return dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.kt new file mode 100644 index 000000000..9ff069ebc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.kt @@ -0,0 +1,41 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.util.DisplayMetrics +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat + + +object UiUtils { + + /** + * Draws a vectorial image onto a bitmap. + * @param vectorDrawable vectorial image + * @return bitmap representation of the vectorial image + */ + @JvmStatic + fun getBitmap(vectorDrawable: VectorDrawableCompat): Bitmap { + val bitmap = Bitmap.createBitmap( + vectorDrawable.intrinsicWidth, + vectorDrawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + vectorDrawable.setBounds(0, 0, canvas.width, canvas.height) + vectorDrawable.draw(canvas) + return bitmap + } + + /** + * Converts dp unit to equivalent pixels. + * @param dp density independent pixels + * @param context Context to access display metrics + * @return px equivalent to dp value + */ + @JvmStatic + fun convertDpToPixel(dp: Float, context: Context): Float { + val metrics = context.resources.displayMetrics + return dp * (metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT.toFloat()) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java deleted file mode 100644 index 1272dc4f1..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java +++ /dev/null @@ -1,143 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.app.Activity; -import android.content.Context; -import android.graphics.Color; -import android.view.Display; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.StringRes; - -import androidx.core.content.ContextCompat; -import com.google.android.material.snackbar.Snackbar; - -import fr.free.nrw.commons.R; -import timber.log.Timber; - -public class ViewUtil { - /** - * Utility function to show short snack bar - * @param view - * @param messageResourceId - */ - public static void showShortSnackbar(View view, int messageResourceId) { - if (view.getContext() == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> { - try { - Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show(); - }catch (IllegalStateException e){ - Timber.e(e.getMessage()); - } - }); - } - public static void showLongSnackbar(View view, String text) { - if(view.getContext() == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(()-> { - try { - Snackbar snackbar = Snackbar.make(view, text, Snackbar.LENGTH_SHORT); - - View snack_view = snackbar.getView(); - TextView snack_text = snack_view.findViewById(R.id.snackbar_text); - - snack_view.setBackgroundColor(Color.LTGRAY); - snack_text.setTextColor(ContextCompat.getColor(view.getContext(), R.color.primaryColor)); - snackbar.setActionTextColor(Color.RED); - - snackbar.setAction("Dismiss", new View.OnClickListener() { - @Override - public void onClick(View v) { - // Handle the action click - snackbar.dismiss(); - } - }); - - snackbar.show(); - - }catch (IllegalStateException e) { - Timber.e(e.getMessage()); - } - }); - } - - public static void showLongToast(Context context, String text) { - if (context == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_LONG).show()); - } - - public static void showLongToast(Context context, @StringRes int stringResourceId) { - if (context == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_LONG).show()); - } - - public static void showShortToast(Context context, String text) { - if (context == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_SHORT).show()); - } - - public static void showShortToast(Context context, @StringRes int stringResourceId) { - if (context == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_SHORT).show()); - } - - public static boolean isPortrait(Context context) { - Display orientation = ((Activity)context).getWindowManager().getDefaultDisplay(); - if (orientation.getWidth() < orientation.getHeight()){ - return true; - } else { - return false; - } - } - - public static void hideKeyboard(View view){ - if (view != null) { - InputMethodManager manager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - view.clearFocus(); - if (manager != null) { - manager.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - } - } - - /** - * A snack bar which has an action button which on click dismisses the snackbar and invokes the - * listener passed - */ - public static void showDismissibleSnackBar(View view, - int messageResourceId, - int actionButtonResourceId, - View.OnClickListener onClickListener) { - if (view.getContext() == null) { - return; - } - ExecutorUtils.uiExecutor().execute(() -> { - Snackbar snackbar = Snackbar.make(view, view.getContext().getString(messageResourceId), - Snackbar.LENGTH_INDEFINITE); - snackbar.setAction(view.getContext().getString(actionButtonResourceId), v -> { - snackbar.dismiss(); - onClickListener.onClick(v); - }); - snackbar.show(); - }); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.kt new file mode 100644 index 000000000..64970ecf6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.kt @@ -0,0 +1,151 @@ +package fr.free.nrw.commons.utils + +import android.app.Activity +import android.content.Context +import android.graphics.Color +import android.view.Display +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.TextView +import android.widget.Toast + +import androidx.annotation.StringRes + +import androidx.core.content.ContextCompat +import com.google.android.material.snackbar.Snackbar + +import fr.free.nrw.commons.R +import timber.log.Timber + + +object ViewUtil { + + /** + * Utility function to show short snack bar + * @param view + * @param messageResourceId + */ + @JvmStatic + fun showShortSnackbar(view: View, messageResourceId: Int) { + if (view.context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + try { + Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show() + } catch (e: IllegalStateException) { + Timber.e(e.message) + } + } + } + + @JvmStatic + fun showLongSnackbar(view: View, text: String) { + if (view.context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + try { + val snackbar = Snackbar.make(view, text, Snackbar.LENGTH_SHORT) + val snackView = snackbar.view + val snackText: TextView = snackView.findViewById(R.id.snackbar_text) + + snackView.setBackgroundColor(Color.LTGRAY) + snackText.setTextColor(ContextCompat.getColor(view.context, R.color.primaryColor)) + snackbar.setActionTextColor(Color.RED) + + snackbar.setAction("Dismiss") { snackbar.dismiss() } + snackbar.show() + + } catch (e: IllegalStateException) { + Timber.e(e.message) + } + } + } + + @JvmStatic + fun showLongToast(context: Context, text: String) { + if (context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + Toast.makeText(context, text, Toast.LENGTH_LONG).show() + } + } + + @JvmStatic + fun showLongToast(context: Context, @StringRes stringResourceId: Int) { + if (context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_LONG).show() + } + } + + @JvmStatic + fun showShortToast(context: Context, text: String) { + if (context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + Toast.makeText(context, text, Toast.LENGTH_SHORT).show() + } + } + + @JvmStatic + fun showShortToast(context: Context?, @StringRes stringResourceId: Int) { + if (context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_SHORT).show() + } + } + + @JvmStatic + fun isPortrait(context: Context): Boolean { + val orientation = (context as Activity).windowManager.defaultDisplay + return orientation.width < orientation.height + } + + @JvmStatic + fun hideKeyboard(view: View?) { + view?.let { + val manager = it.context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + it.clearFocus() + manager?.hideSoftInputFromWindow(it.windowToken, 0) + } + } + + /** + * A snack bar which has an action button which on click dismisses the snackbar and invokes the + * listener passed + */ + @JvmStatic + fun showDismissibleSnackBar( + view: View, + messageResourceId: Int, + actionButtonResourceId: Int, + onClickListener: View.OnClickListener + ) { + if (view.context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + val snackbar = Snackbar.make(view, view.context.getString(messageResourceId), Snackbar.LENGTH_INDEFINITE) + snackbar.setAction(view.context.getString(actionButtonResourceId)) { + snackbar.dismiss() + onClickListener.onClick(it) + } + snackbar.show() + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.java deleted file mode 100644 index 2721ef98d..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.java +++ /dev/null @@ -1,23 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; - -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class ViewUtilWrapper { - - @Inject - public ViewUtilWrapper() { - - } - - public void showShortToast(Context context, String text) { - ViewUtil.showShortToast(context, text); - } - - public void showLongToast(Context context, String text) { - ViewUtil.showLongToast(context, text); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.kt b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.kt new file mode 100644 index 000000000..b5ead3041 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.kt @@ -0,0 +1,17 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ViewUtilWrapper @Inject constructor() { + + fun showShortToast(context: Context, text: String) { + ViewUtil.showShortToast(context, text) + } + + fun showLongToast(context: Context, text: String) { + ViewUtil.showLongToast(context, text) + } +} From cb4ffd8ca87baf1177bd56ac0d2b9935b0f8e19e Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Wed, 20 Nov 2024 09:11:50 +0530 Subject: [PATCH 30/74] Migrated widget module from Java to Kotlin (#5940) * Rename .java to .kt * Migrated widget module to Kotlin --- .../di/CommonsApplicationComponent.java | 1 - .../widget/HeightLimitedRecyclerView.java | 48 ----- .../widget/HeightLimitedRecyclerView.kt | 43 +++++ .../nrw/commons/widget/PicOfDayAppWidget.java | 181 ------------------ .../nrw/commons/widget/PicOfDayAppWidget.kt | 174 +++++++++++++++++ .../free/nrw/commons/widget/ViewHolder.java | 7 - .../fr/free/nrw/commons/widget/ViewHolder.kt | 7 + 7 files changed, 224 insertions(+), 237 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java create mode 100644 app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.java create mode 100644 app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.kt 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 1390bd8ef..0d847b649 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 @@ -2,7 +2,6 @@ package fr.free.nrw.commons.di; import com.google.gson.Gson; -import fr.free.nrw.commons.actions.PageEditClient; import fr.free.nrw.commons.explore.categories.CategoriesModule; import fr.free.nrw.commons.navtab.MoreBottomSheetFragment; import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment; diff --git a/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java b/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java deleted file mode 100644 index 5c6dde5fd..000000000 --- a/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java +++ /dev/null @@ -1,48 +0,0 @@ -package fr.free.nrw.commons.widget; - -import android.app.Activity; -import android.content.Context; -import android.util.AttributeSet; -import android.util.DisplayMetrics; - -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; - -/** - * Created by Ilgaz Er on 8/7/2018. - */ -public class HeightLimitedRecyclerView extends RecyclerView { - int height; - public HeightLimitedRecyclerView(Context context) { - super(context); - DisplayMetrics displayMetrics = new DisplayMetrics(); - ((Activity) getContext()).getWindowManager() - .getDefaultDisplay() - .getMetrics(displayMetrics); - height=displayMetrics.heightPixels; - } - - public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - DisplayMetrics displayMetrics = new DisplayMetrics(); - ((Activity) getContext()).getWindowManager() - .getDefaultDisplay() - .getMetrics(displayMetrics); - height=displayMetrics.heightPixels; - } - - public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - DisplayMetrics displayMetrics = new DisplayMetrics(); - ((Activity) getContext()).getWindowManager() - .getDefaultDisplay() - .getMetrics(displayMetrics); - height=displayMetrics.heightPixels; - } - - @Override - protected void onMeasure(int widthSpec, int heightSpec) { - heightSpec = MeasureSpec.makeMeasureSpec((int) (height*0.3), MeasureSpec.AT_MOST); - super.onMeasure(widthSpec, heightSpec); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.kt b/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.kt new file mode 100644 index 000000000..b86455243 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.kt @@ -0,0 +1,43 @@ +package fr.free.nrw.commons.widget + +import android.app.Activity +import android.content.Context +import android.util.AttributeSet +import android.util.DisplayMetrics + +import androidx.annotation.Nullable +import androidx.recyclerview.widget.RecyclerView + + +/** + * Created by Ilgaz Er on 8/7/2018. + */ +class HeightLimitedRecyclerView : RecyclerView { + private var height: Int = 0 + + constructor(context: Context) : super(context) { + initializeHeight(context) + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + initializeHeight(context) + } + + constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) { + initializeHeight(context) + } + + private fun initializeHeight(context: Context) { + val displayMetrics = DisplayMetrics() + (context as Activity).windowManager.defaultDisplay.getMetrics(displayMetrics) + height = displayMetrics.heightPixels + } + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + val limitedHeightSpec = MeasureSpec.makeMeasureSpec( + (height * 0.3).toInt(), + MeasureSpec.AT_MOST + ) + super.onMeasure(widthSpec, limitedHeightSpec) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java deleted file mode 100644 index 273452078..000000000 --- a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java +++ /dev/null @@ -1,181 +0,0 @@ -package fr.free.nrw.commons.widget; - -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.net.Uri; -import android.os.Build; -import android.widget.RemoteViews; -import androidx.annotation.Nullable; -import com.facebook.common.executors.CallerThreadExecutor; -import com.facebook.common.references.CloseableReference; -import com.facebook.datasource.DataSource; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.imagepipeline.core.ImagePipeline; -import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; -import com.facebook.imagepipeline.image.CloseableImage; -import com.facebook.imagepipeline.request.ImageRequest; -import com.facebook.imagepipeline.request.ImageRequestBuilder; -import fr.free.nrw.commons.media.MediaClient; -import javax.inject.Inject; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; - -import static android.content.Intent.ACTION_VIEW; - -/** - * Implementation of App Widget functionality. - */ -public class PicOfDayAppWidget extends AppWidgetProvider { - - private final CompositeDisposable compositeDisposable = new CompositeDisposable(); - - @Inject - MediaClient mediaClient; - - void updateAppWidget( - final Context context, - final AppWidgetManager appWidgetManager, - final int appWidgetId - ) { - final RemoteViews views = new RemoteViews( - context.getPackageName(), R.layout.pic_of_day_app_widget); - - // Launch App on Button Click - final Intent viewIntent = new Intent(context, MainActivity.class); - int flags = PendingIntent.FLAG_UPDATE_CURRENT; - if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.M) { - flags |= PendingIntent.FLAG_IMMUTABLE; - } - final PendingIntent pendingIntent = PendingIntent.getActivity( - context, 0, viewIntent, flags); - - views.setOnClickPendingIntent(R.id.camera_button, pendingIntent); - appWidgetManager.updateAppWidget(appWidgetId, views); - - loadPictureOfTheDay(context, views, appWidgetManager, appWidgetId); - } - - /** - * Loads the picture of the day using media wiki API - * @param context The application context. - * @param views The RemoteViews object used to update the App Widget UI. - * @param appWidgetManager The AppWidgetManager instance for managing the widget. - * @param appWidgetId he ID of the App Widget to update. - */ - private void loadPictureOfTheDay( - final Context context, - final RemoteViews views, - final AppWidgetManager appWidgetManager, - final int appWidgetId - ) { - compositeDisposable.add(mediaClient.getPictureOfTheDay() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null) { - views.setTextViewText(R.id.appwidget_title, response.getDisplayTitle()); - - // View in browser - final Intent viewIntent = new Intent(); - viewIntent.setAction(ACTION_VIEW); - viewIntent.setData(Uri.parse(response.getPageTitle().getMobileUri())); - - int flags = PendingIntent.FLAG_UPDATE_CURRENT; - if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.M) { - flags |= PendingIntent.FLAG_IMMUTABLE; - } - final PendingIntent pendingIntent = PendingIntent.getActivity( - context, 0, viewIntent, flags); - - views.setOnClickPendingIntent(R.id.appwidget_image, pendingIntent); - loadImageFromUrl(response.getThumbUrl(), - context, views, appWidgetManager, appWidgetId); - } - }, - t -> Timber.e(t, "Fetching picture of the day failed") - )); - } - - /** - * Uses Fresco to load an image from Url - * @param imageUrl The URL of the image to load. - * @param context The application context. - * @param views The RemoteViews object used to update the App Widget UI. - * @param appWidgetManager The AppWidgetManager instance for managing the widget. - * @param appWidgetId he ID of the App Widget to update. - */ - private void loadImageFromUrl( - final String imageUrl, - final Context context, - final RemoteViews views, - final AppWidgetManager appWidgetManager, - final int appWidgetId - ) { - final ImageRequest request = ImageRequestBuilder - .newBuilderWithSource(Uri.parse(imageUrl)).build(); - final ImagePipeline imagePipeline = Fresco.getImagePipeline(); - final DataSource> dataSource = imagePipeline - .fetchDecodedImage(request, context); - - dataSource.subscribe(new BaseBitmapDataSubscriber() { - @Override - protected void onNewResultImpl(@Nullable final Bitmap tempBitmap) { - Bitmap bitmap = null; - if (tempBitmap != null) { - bitmap = Bitmap.createBitmap( - tempBitmap.getWidth(), tempBitmap.getHeight(), Bitmap.Config.ARGB_8888 - ); - final Canvas canvas = new Canvas(bitmap); - canvas.drawBitmap(tempBitmap, 0f, 0f, new Paint()); - } - views.setImageViewBitmap(R.id.appwidget_image, bitmap); - appWidgetManager.updateAppWidget(appWidgetId, views); - } - - @Override - protected void onFailureImpl( - final DataSource> dataSource - ) { - // Ignore failure for now. - } - }, CallerThreadExecutor.getInstance()); - } - - @Override - public void onUpdate( - final Context context, - final AppWidgetManager appWidgetManager, - final int[] appWidgetIds - ) { - ApplicationlessInjection - .getInstance(context.getApplicationContext()) - .getCommonsApplicationComponent() - .inject(this); - // There may be multiple widgets active, so update all of them - for (final int appWidgetId : appWidgetIds) { - updateAppWidget(context, appWidgetManager, appWidgetId); - } - } - - @Override - public void onEnabled(final Context context) { - // Enter relevant functionality for when the first widget is created - } - - @Override - public void onDisabled(final Context context) { - // Enter relevant functionality for when the last widget is disabled - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.kt b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.kt new file mode 100644 index 000000000..ab6a45b85 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.kt @@ -0,0 +1,174 @@ +package fr.free.nrw.commons.widget + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.net.Uri +import android.os.Build +import android.widget.RemoteViews +import androidx.annotation.Nullable +import com.facebook.common.executors.CallerThreadExecutor +import com.facebook.common.references.CloseableReference +import com.facebook.datasource.DataSource +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.imagepipeline.core.ImagePipeline +import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber +import com.facebook.imagepipeline.image.CloseableImage +import com.facebook.imagepipeline.request.ImageRequest +import com.facebook.imagepipeline.request.ImageRequestBuilder +import fr.free.nrw.commons.media.MediaClient +import javax.inject.Inject +import fr.free.nrw.commons.R +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.di.ApplicationlessInjection +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + +/** + * Implementation of App Widget functionality. + */ +class PicOfDayAppWidget : AppWidgetProvider() { + + private val compositeDisposable = CompositeDisposable() + + @Inject + lateinit var mediaClient: MediaClient + + private fun updateAppWidget( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int + ) { + val views = RemoteViews(context.packageName, R.layout.pic_of_day_app_widget) + + // Launch App on Button Click + val viewIntent = Intent(context, MainActivity::class.java) + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags = flags or PendingIntent.FLAG_IMMUTABLE + } + val pendingIntent = PendingIntent.getActivity(context, 0, viewIntent, flags) + views.setOnClickPendingIntent(R.id.camera_button, pendingIntent) + + appWidgetManager.updateAppWidget(appWidgetId, views) + + loadPictureOfTheDay(context, views, appWidgetManager, appWidgetId) + } + + /** + * Loads the picture of the day using media wiki API + * @param context The application context. + * @param views The RemoteViews object used to update the App Widget UI. + * @param appWidgetManager The AppWidgetManager instance for managing the widget. + * @param appWidgetId The ID of the App Widget to update. + */ + private fun loadPictureOfTheDay( + context: Context, + views: RemoteViews, + appWidgetManager: AppWidgetManager, + appWidgetId: Int + ) { + compositeDisposable.add( + mediaClient.getPictureOfTheDay() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response -> + if (response != null) { + views.setTextViewText(R.id.appwidget_title, response.displayTitle) + + // View in browser + val viewIntent = Intent().apply { + action = Intent.ACTION_VIEW + data = Uri.parse(response.pageTitle.mobileUri) + } + + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags = flags or PendingIntent.FLAG_IMMUTABLE + } + val pendingIntent = PendingIntent.getActivity( + context, + 0, + viewIntent, + flags + ) + + views.setOnClickPendingIntent(R.id.appwidget_image, pendingIntent) + loadImageFromUrl( + response.thumbUrl, + context, + views, + appWidgetManager, + appWidgetId + ) + } + }, + { t -> Timber.e(t, "Fetching picture of the day failed") } + ) + ) + } + + /** + * Uses Fresco to load an image from Url + * @param imageUrl The URL of the image to load. + * @param context The application context. + * @param views The RemoteViews object used to update the App Widget UI. + * @param appWidgetManager The AppWidgetManager instance for managing the widget. + * @param appWidgetId The ID of the App Widget to update. + */ + private fun loadImageFromUrl( + imageUrl: String?, + context: Context, + views: RemoteViews, + appWidgetManager: AppWidgetManager, + appWidgetId: Int + ) { + val request = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageUrl)).build() + val imagePipeline = Fresco.getImagePipeline() + val dataSource = imagePipeline.fetchDecodedImage(request, context) + + dataSource.subscribe(object : BaseBitmapDataSubscriber() { + override fun onNewResultImpl(tempBitmap: Bitmap?) { + val bitmap = tempBitmap?.let { + Bitmap.createBitmap(it.width, it.height, Bitmap.Config.ARGB_8888).apply { + Canvas(this).drawBitmap(it, 0f, 0f, Paint()) + } + } + views.setImageViewBitmap(R.id.appwidget_image, bitmap) + appWidgetManager.updateAppWidget(appWidgetId, views) + } + + override fun onFailureImpl(dataSource: DataSource>) { + // Ignore failure for now. + } + }, CallerThreadExecutor.getInstance()) + } + + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + ApplicationlessInjection + .getInstance(context.applicationContext) + .commonsApplicationComponent + .inject(this) + + // There may be multiple widgets active, so update all of them + for (appWidgetId in appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId) + } + } + + override fun onEnabled(context: Context) { + // Enter relevant functionality for when the first widget is created + } + + override fun onDisabled(context: Context) { + // Enter relevant functionality for when the last widget is disabled + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.java b/app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.java deleted file mode 100644 index e2dd8d680..000000000 --- a/app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.java +++ /dev/null @@ -1,7 +0,0 @@ -package fr.free.nrw.commons.widget; - -import android.content.Context; - -public interface ViewHolder { - void bindModel(Context context, T model); -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.kt b/app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.kt new file mode 100644 index 000000000..f9f598b3e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.kt @@ -0,0 +1,7 @@ +package fr.free.nrw.commons.widget + +import android.content.Context + +interface ViewHolder { + fun bindModel(context: Context, model: T) +} \ No newline at end of file From ed18a37577dd08d56cbacb6da6050c4a107acfbb Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Wed, 20 Nov 2024 19:25:13 +0530 Subject: [PATCH 31/74] Migrated ui and theme modules from Java to Kotlin (#5942) * Rename .java to .kt * Migrated ui and theme module to Kotlin --- .../LocationPickerActivity.java | 4 +- .../ui/selector/ImageFragment.kt | 3 +- .../nrw/commons/explore/SearchActivity.java | 4 +- .../notification/NotificationActivity.java | 4 +- .../nrw/commons/profile/ProfileActivity.java | 2 +- .../free/nrw/commons/theme/BaseActivity.java | 66 ----------------- .../fr/free/nrw/commons/theme/BaseActivity.kt | 65 +++++++++++++++++ .../ui/PasteSensitiveTextInputEditText.java | 65 ----------------- .../ui/PasteSensitiveTextInputEditText.kt | 60 ++++++++++++++++ .../nrw/commons/ui/widget/HtmlTextView.java | 36 ---------- .../nrw/commons/ui/widget/HtmlTextView.kt | 32 +++++++++ .../nrw/commons/ui/widget/OverlayDialog.java | 70 ------------------- .../nrw/commons/ui/widget/OverlayDialog.kt | 64 +++++++++++++++++ 13 files changed, 230 insertions(+), 245 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.java create mode 100644 app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java create mode 100644 app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.kt diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java index 2f05705ba..40f360a24 100644 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java @@ -367,7 +367,7 @@ public class LocationPickerActivity extends BaseActivity implements */ private void removeLocationFromImage() { if (media != null) { - compositeDisposable.add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext() + getCompositeDisposable().add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext() , media, "0.0", "0.0", "0.0f") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -479,7 +479,7 @@ public class LocationPickerActivity extends BaseActivity implements } try { - compositeDisposable.add( + getCompositeDisposable().add( coordinateEditHelper.makeCoordinatesEdit(getApplicationContext(), media, Latitude, Longitude, Accuracy) .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index dbab629ff..3912a4d12 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -1,6 +1,7 @@ package fr.free.nrw.commons.customselector.ui.selector import android.app.Activity +import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.SharedPreferences import android.os.Bundle @@ -346,7 +347,7 @@ class ImageFragment : context .getSharedPreferences( "CustomSelector", - BaseActivity.MODE_PRIVATE, + MODE_PRIVATE, )?.let { prefs -> prefs.edit()?.let { editor -> editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply() 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 abb27184f..934bff6ec 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 @@ -104,7 +104,7 @@ public class SearchActivity extends BaseActivity viewPagerAdapter.setTabData(fragmentList, titleList); viewPagerAdapter.notifyDataSetChanged(); - compositeDisposable.add(RxSearchView.queryTextChanges(binding.searchBox) + getCompositeDisposable().add(RxSearchView.queryTextChanges(binding.searchBox) .takeUntil(RxView.detaches(binding.searchBox)) .debounce(500, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) @@ -284,7 +284,7 @@ public class SearchActivity extends BaseActivity @Override protected void onDestroy() { super.onDestroy(); //Dispose the disposables when the activity is destroyed - compositeDisposable.dispose(); + getCompositeDisposable().dispose(); binding = null; } } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java index 572dd0317..b57df4948 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -133,7 +133,7 @@ public class NotificationActivity extends BaseActivity { } binding.progressBar.setVisibility(View.GONE); }); - compositeDisposable.add(disposable); + getCompositeDisposable().add(disposable); } @@ -178,7 +178,7 @@ public class NotificationActivity extends BaseActivity { Timber.d("Add notifications"); if (mNotificationWorkerFragment == null) { binding.progressBar.setVisibility(View.VISIBLE); - compositeDisposable.add(controller.getNotifications(archived) + getCompositeDisposable().add(controller.getNotifications(archived) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(notificationList -> { diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java index c6d09fdc6..60a0f47a1 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java @@ -157,7 +157,7 @@ public class ProfileActivity extends BaseActivity { @Override public void onDestroy() { super.onDestroy(); - compositeDisposable.clear(); + getCompositeDisposable().clear(); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java deleted file mode 100644 index 95ec00dc6..000000000 --- a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java +++ /dev/null @@ -1,66 +0,0 @@ -package fr.free.nrw.commons.theme; - -import android.content.res.Configuration; -import android.os.Bundle; -import android.util.DisplayMetrics; -import android.view.WindowManager; -import javax.inject.Inject; -import javax.inject.Named; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.utils.SystemThemeUtils; -import io.reactivex.disposables.CompositeDisposable; - -public abstract class BaseActivity extends CommonsDaggerAppCompatActivity { - @Inject - @Named("default_preferences") - public JsonKvStore defaultKvStore; - - @Inject - SystemThemeUtils systemThemeUtils; - - protected CompositeDisposable compositeDisposable = new CompositeDisposable(); - protected boolean wasPreviouslyDarkTheme; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - wasPreviouslyDarkTheme = systemThemeUtils.isDeviceInNightMode(); - setTheme(wasPreviouslyDarkTheme ? R.style.DarkAppTheme : R.style.LightAppTheme); - float fontScale = android.provider.Settings.System.getFloat( - getBaseContext().getContentResolver(), - android.provider.Settings.System.FONT_SCALE, - 1f); - adjustFontScale(getResources().getConfiguration(), fontScale); - } - - @Override - protected void onResume() { - // Restart activity if theme is changed - if (wasPreviouslyDarkTheme != systemThemeUtils.isDeviceInNightMode()) { - recreate(); - } - - super.onResume(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - compositeDisposable.clear(); - } - - /** - * Apply fontScale on device - */ - public void adjustFontScale(Configuration configuration, float scale) { - configuration.fontScale = scale; - final DisplayMetrics metrics = getResources().getDisplayMetrics(); - final WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); - wm.getDefaultDisplay().getMetrics(metrics); - metrics.scaledDensity = configuration.fontScale * metrics.density; - getBaseContext().getResources().updateConfiguration(configuration, metrics); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt new file mode 100644 index 000000000..d2d936460 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt @@ -0,0 +1,65 @@ +package fr.free.nrw.commons.theme + +import android.content.res.Configuration +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.WindowManager +import javax.inject.Inject +import javax.inject.Named +import fr.free.nrw.commons.R +import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.utils.SystemThemeUtils +import io.reactivex.disposables.CompositeDisposable + + +abstract class BaseActivity : CommonsDaggerAppCompatActivity() { + + @Inject + @field:Named("default_preferences") + lateinit var defaultKvStore: JsonKvStore + + @Inject + lateinit var systemThemeUtils: SystemThemeUtils + + protected val compositeDisposable = CompositeDisposable() + protected var wasPreviouslyDarkTheme: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + wasPreviouslyDarkTheme = systemThemeUtils.isDeviceInNightMode() + setTheme(if (wasPreviouslyDarkTheme) R.style.DarkAppTheme else R.style.LightAppTheme) + + val fontScale = android.provider.Settings.System.getFloat( + baseContext.contentResolver, + android.provider.Settings.System.FONT_SCALE, + 1f + ) + adjustFontScale(resources.configuration, fontScale) + } + + override fun onResume() { + // Restart activity if theme is changed + if (wasPreviouslyDarkTheme != systemThemeUtils.isDeviceInNightMode()) { + recreate() + } + super.onResume() + } + + override fun onDestroy() { + super.onDestroy() + compositeDisposable.clear() + } + + /** + * Apply fontScale on device + */ + fun adjustFontScale(configuration: Configuration, scale: Float) { + configuration.fontScale = scale + val metrics = resources.displayMetrics + val wm = getSystemService(WINDOW_SERVICE) as WindowManager + wm.defaultDisplay.getMetrics(metrics) + metrics.scaledDensity = configuration.fontScale * metrics.density + baseContext.resources.updateConfiguration(configuration, metrics) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.java b/app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.java deleted file mode 100644 index 0b82baf64..000000000 --- a/app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.java +++ /dev/null @@ -1,65 +0,0 @@ -package fr.free.nrw.commons.ui; - -import android.content.Context; -import android.content.res.TypedArray; -import android.os.Build.VERSION; -import android.util.AttributeSet; -import com.google.android.material.textfield.TextInputEditText; -import fr.free.nrw.commons.R; - -public class PasteSensitiveTextInputEditText extends TextInputEditText { - - private boolean formattingAllowed = true; - - public PasteSensitiveTextInputEditText(final Context context) { - super(context); - } - - public PasteSensitiveTextInputEditText(final Context context, final AttributeSet attrs) { - super(context, attrs); - formattingAllowed = extractFormattingAttribute(context, attrs); - } - - @Override - public boolean onTextContextMenuItem(int id) { - - // if not paste command, or formatting is allowed, return default - if(id != android.R.id.paste || formattingAllowed){ - return super.onTextContextMenuItem(id); - } - - // if its paste and formatting not allowed - boolean proceeded; - if(VERSION.SDK_INT >= 23) { - proceeded = super.onTextContextMenuItem(android.R.id.pasteAsPlainText); - }else { - proceeded = super.onTextContextMenuItem(id); - if (proceeded && getText() != null) { - // rewrite with plain text so formatting is lost - setText(getText().toString()); - setSelection(getText().length()); - } - } - return proceeded; - } - - private boolean extractFormattingAttribute(Context context, AttributeSet attrs){ - - boolean formatAllowed = true; - - TypedArray a = context.getTheme().obtainStyledAttributes( - attrs, R.styleable.PasteSensitiveTextInputEditText, 0, 0); - - try { - formatAllowed = a.getBoolean( - R.styleable.PasteSensitiveTextInputEditText_allowFormatting, true); - } finally { - a.recycle(); - } - return formatAllowed; - } - - public void setFormattingAllowed(boolean formattingAllowed){ - this.formattingAllowed = formattingAllowed; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.kt b/app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.kt new file mode 100644 index 000000000..56f795485 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.kt @@ -0,0 +1,60 @@ +package fr.free.nrw.commons.ui + +import android.content.Context +import android.content.res.TypedArray +import android.os.Build +import android.os.Build.VERSION +import android.util.AttributeSet +import com.google.android.material.textfield.TextInputEditText +import fr.free.nrw.commons.R + + +class PasteSensitiveTextInputEditText @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : TextInputEditText(context, attrs) { + + private var formattingAllowed: Boolean = true + + init { + if (attrs != null) { + formattingAllowed = extractFormattingAttribute(context, attrs) + } + } + + override fun onTextContextMenuItem(id: Int): Boolean { + // if not paste command, or formatting is allowed, return default + if (id != android.R.id.paste || formattingAllowed) { + return super.onTextContextMenuItem(id) + } + + // if it's paste and formatting not allowed + val proceeded: Boolean = if (VERSION.SDK_INT >= 23) { + super.onTextContextMenuItem(android.R.id.pasteAsPlainText) + } else { + val success = super.onTextContextMenuItem(id) + if (success && text != null) { + // rewrite with plain text so formatting is lost + setText(text.toString()) + setSelection(text?.length ?: 0) + } + success + } + return proceeded + } + + private fun extractFormattingAttribute(context: Context, attrs: AttributeSet): Boolean { + val a = context.theme.obtainStyledAttributes( + attrs, R.styleable.PasteSensitiveTextInputEditText, 0, 0 + ) + return try { + a.getBoolean(R.styleable.PasteSensitiveTextInputEditText_allowFormatting, true) + } finally { + a.recycle() + } + } + + fun setFormattingAllowed(formattingAllowed: Boolean) { + this.formattingAllowed = formattingAllowed + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java b/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java deleted file mode 100644 index 21af5ee78..000000000 --- a/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java +++ /dev/null @@ -1,36 +0,0 @@ -package fr.free.nrw.commons.ui.widget; - -import android.content.Context; -import android.text.method.LinkMovementMethod; -import android.util.AttributeSet; - -import androidx.appcompat.widget.AppCompatTextView; - -import fr.free.nrw.commons.utils.StringUtil; - -/** - * An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any - * links clickable. - */ -public class HtmlTextView extends AppCompatTextView { - - /** - * Constructs a new instance of HtmlTextView - * @param context the context of the view - * @param attrs the set of attributes for the view - */ - public HtmlTextView(Context context, AttributeSet attrs) { - super(context, attrs); - - setMovementMethod(LinkMovementMethod.getInstance()); - setText(StringUtil.fromHtml(getText().toString())); - } - - /** - * Sets the text to be displayed - * @param newText the text to be displayed - */ - public void setHtmlText(String newText) { - setText(StringUtil.fromHtml(newText)); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.kt b/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.kt new file mode 100644 index 000000000..48433136f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.kt @@ -0,0 +1,32 @@ +package fr.free.nrw.commons.ui.widget + +import android.content.Context +import android.text.method.LinkMovementMethod +import android.util.AttributeSet + +import androidx.appcompat.widget.AppCompatTextView + +import fr.free.nrw.commons.utils.StringUtil + +/** + * An [AppCompatTextView] which formats the text to HTML displayable text and makes any + * links clickable. + */ +class HtmlTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatTextView(context, attrs) { + + init { + movementMethod = LinkMovementMethod.getInstance() + text = StringUtil.fromHtml(text.toString()) + } + + /** + * Sets the text to be displayed + * @param newText the text to be displayed + */ + fun setHtmlText(newText: String) { + text = StringUtil.fromHtml(newText) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java b/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java deleted file mode 100644 index f36219040..000000000 --- a/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java +++ /dev/null @@ -1,70 +0,0 @@ -package fr.free.nrw.commons.ui.widget; - -import android.app.Dialog; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.os.Bundle; -import android.view.Gravity; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; - -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; - -/** - * a formatted dialog fragment - * This class is used by NearbyInfoDialog - */ -public abstract class OverlayDialog extends DialogFragment { - - /** - * creates a DialogFragment with the correct style and theme - * @param savedInstanceState bundle re-constructed from a previous saved state - */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light); - } - - /** - * When the view is created, sets the dialog layout to full screen - * - * @param view the view being used - * @param savedInstanceState bundle re-constructed from a previous saved state - */ - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - setDialogLayoutToFullScreen(); - super.onViewCreated(view, savedInstanceState); - } - - /** - * sets the dialog layout to fullscreen - */ - private void setDialogLayoutToFullScreen() { - Window window = getDialog().getWindow(); - WindowManager.LayoutParams wlp = window.getAttributes(); - window.requestFeature(Window.FEATURE_NO_TITLE); - wlp.gravity = Gravity.BOTTOM; - wlp.width = WindowManager.LayoutParams.MATCH_PARENT; - wlp.height = WindowManager.LayoutParams.MATCH_PARENT; - window.setAttributes(wlp); - } - - /** - * builds custom dialog container - * - * @param savedInstanceState the previously saved state - * @return the dialog - */ - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Dialog dialog = super.onCreateDialog(savedInstanceState); - Window window = dialog.getWindow(); - window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); - return dialog; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.kt b/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.kt new file mode 100644 index 000000000..5d4bc2b46 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.kt @@ -0,0 +1,64 @@ +package fr.free.nrw.commons.ui.widget + +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.Gravity +import android.view.View +import android.view.Window +import android.view.WindowManager + +import androidx.fragment.app.DialogFragment + +/** + * A formatted dialog fragment + * This class is used by NearbyInfoDialog + */ +abstract class OverlayDialog : DialogFragment() { + + /** + * Creates a DialogFragment with the correct style and theme + * @param savedInstanceState bundle re-constructed from a previous saved state + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light) + } + + /** + * When the view is created, sets the dialog layout to full screen + * + * @param view the view being used + * @param savedInstanceState bundle re-constructed from a previous saved state + */ + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + setDialogLayoutToFullScreen() + super.onViewCreated(view, savedInstanceState) + } + + /** + * Sets the dialog layout to fullscreen + */ + private fun setDialogLayoutToFullScreen() { + val window = dialog?.window ?: return + val wlp = window.attributes + window.requestFeature(Window.FEATURE_NO_TITLE) + wlp.gravity = Gravity.BOTTOM + wlp.width = WindowManager.LayoutParams.MATCH_PARENT + wlp.height = WindowManager.LayoutParams.MATCH_PARENT + window.attributes = wlp + } + + /** + * Builds custom dialog container + * + * @param savedInstanceState the previously saved state + * @return the dialog + */ + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + return dialog + } +} From 5f1d284309bc4bc46913cb4a2d418618c01a5bf4 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 21 Nov 2024 13:01:41 +0100 Subject: [PATCH 32/74] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-br/strings.xml | 32 ++++++++++++++++++++++++-- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-sv/strings.xml | 10 ++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 1c7d09617..5d86b1fcf 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -53,6 +53,13 @@ gant an aotre-implijout %1$s e vo ar skeudenn-mañ gant an aotreoù-implijout %1$s e vo ar skeudenn-mañ + + %1$d enporzhiadur + %1$d enporzhiadur + %1$d enporzhiadur + %1$d enporzhiadur + %1$d enporzhiadur + Ergerzhout Neuz Hollek @@ -73,7 +80,7 @@ Kevreet oc\'h ! N\'haller ket kevreañ! N\'eo ket bet kavet ar restr. Klask gant unan all. - Dilesadur c\'hwitet, kevreit en-dro mar plij + Dilesadur c\'hwitet. Kevreit en-dro mar plij. Kroget da enporzhiañ! %1$s bet pellgaset ! Pouezit evit gwelet hoc\'h enporzhiadenn @@ -179,6 +186,7 @@ Aotre ret ; skrivañ war al lec\'h stokañ diavaez. Ne c\'hall ket an arload tizhout ho kamera hep an dra-se. Mat eo Diwallit + Anv ar restr doubl kavet Enporzhiañ Ya Ket @@ -253,6 +261,7 @@ Commons Ho priziadur FAG + Sturlevr an implijer Lezel an tutorial a-gostez Kenrouedad dihegerz Fazi war enklask kemennoù @@ -293,6 +302,7 @@ Respont fall Rannañ an arloadoù C\'hwelañ + Skeudenn ebet en takad-mañ Klask nevez ebet Istor klask diverket Dilemel @@ -308,6 +318,7 @@ Skeudennoù implijet Ur fazi zo bet ! Implijit un anv aozer personelaet + Anv aozer personelaet Degasadennoù Nepell Kemennoù @@ -329,12 +340,14 @@ Nullañ an enporzhiadur Kenderc\'hel an enporzhiadur (Evit holl skeudennoù an hollad) + Furchal en takad-mañ Goulenn aotre Arabat goulenn en-dro Aotren Disteurel Graet Trugarekaet eo bet %1$s gant berzh + Ur fazi zo bet en ur drugarekaat %1$s Trugarekaat %1$s Skeudenn da-heul Ya, perak pas @@ -352,6 +365,7 @@ Lec\'hiadur Doare kamera Meziant + Rannañ an arload gant... Titouroù ar skeudenn Rummad ebet kavet Deskrivadur ebet kavet @@ -365,13 +379,18 @@ Berzh Hizivaat ar rummadoù Berzh + Ouzhpennet eo bet an daveennoù %1$s. Kont krouet! Testenn eilet er golver Ur fazi zo bet! Bez\' ez eus anezhañ + Ur skeudenn zo ezhomm + Seurt lec\'h: + Pont, mirdi, leti h.a. MEDIA KLASOÙ BUGALE KLASOÙ KERENT + Ur skeudenn eus %1$s eo? Sinedoù Arventennoù Lamet eus ar sinedoù @@ -400,8 +419,10 @@ Enporzhiañ Nepell Implijet + Ma renk Skeudennoù a-zoare Nullañ an enporzhiadur + Taolenniñ a ra Yezh etrefas an arload Lenn muioc\'h En holl yezhoù @@ -416,7 +437,7 @@ GOUZOUT HIROC\'H Degasadennoù an implijer: %s Taolioù-kaer an implijer: %s - Gwelet pajenn an implijer + Gwelet profil an implijer Kemmañ ar rummadoù Dibarzhioù araokaet Lec\'hiadur ebet kavet @@ -432,4 +453,11 @@ Lemel al lec\'hiadur Lec\'hiadur lamet! Trugarekaat an aozer + Kaozeadenn + C\'hwitet + Dilemel + Nullañ + Restr %1$s dilamet gant berzh + Ur fazi zo bet en ur zilemel ar restr %1$s + Al lec\'h-mañ en deus ur skeudenn dija. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 215ecc985..c19772f4d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -820,7 +820,7 @@ Ortsdaten konnten nicht geladen werden Ordner löschen Löschung bestätigen - Bist du sicher, dass du den Ordner %1$s löschen möchten, die %2$d Datenobjekte enthalten? + Bist du sicher, dass du den Ordner %1$s löschen möchten, der %2$d Datenobjekte enthält? Löschen Abbrechen Ordner %1$s erfolgreich gelöscht diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 646fb34c0..5ffde0b59 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -125,6 +125,7 @@ Sök kategorier Sök efter objekt som din mediafil skildrar (berg, Taj Mahal, etc.) Spara + Överflödesmeny Uppdatera Lista (Inga uppladdningar ännu) @@ -789,6 +790,15 @@ Pågår Misslyckades Kunde inte läsa in platsdata + Radera mapp + Bekräfta radering + Vill du verkligen radera mappen %1$s som innehåller %2$d objekt? + Radera + Avbryt + Mappen %1$s har raderats + Kunde inte radera mappen %1$s + Fel vid kassering av mappinnehållet: %1$s + Kunde inte hämta mappsökväg för bucket-ID: %1$d Det här platsen har ännu ingen bild. Gå och ta en! Det här platsen har redan en bild. Kollar nu om den här platsen har en bild. From cf88f9b796c23b460ce46ba4c42e3266b35329a9 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Thu, 21 Nov 2024 17:46:42 +0530 Subject: [PATCH 33/74] Migrated settings modules from Java to Kotlin (#5944) * Rename .java to .kt * Migrated settings module to Kotlin --- .../fr/free/nrw/commons/settings/Prefs.java | 21 - .../fr/free/nrw/commons/settings/Prefs.kt | 21 + .../commons/settings/SettingsActivity.java | 69 --- .../nrw/commons/settings/SettingsActivity.kt | 63 ++ .../commons/settings/SettingsFragment.java | 575 ------------------ .../nrw/commons/settings/SettingsFragment.kt | 556 +++++++++++++++++ 6 files changed, 640 insertions(+), 665 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/settings/Prefs.java create mode 100644 app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt diff --git a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java deleted file mode 100644 index 334214347..000000000 --- a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java +++ /dev/null @@ -1,21 +0,0 @@ -package fr.free.nrw.commons.settings; - -public class Prefs { - public static String GLOBAL_PREFS = "fr.free.nrw.commons.preferences"; - - public static String TRACKING_ENABLED = "eventLogging"; - public static final String DEFAULT_LICENSE = "defaultLicense"; - public static final String UPLOADS_SHOWING = "uploadsshowing"; - public static final String MANAGED_EXIF_TAGS = "managed_exif_tags"; - public static final String DESCRIPTION_LANGUAGE = "languageDescription"; - public static final String APP_UI_LANGUAGE = "appUiLanguage"; - public static final String KEY_THEME_VALUE = "appThemePref"; - - public static class Licenses { - public static final String CC_BY_SA_3 = "CC BY-SA 3.0"; - public static final String CC_BY_3 = "CC BY 3.0"; - public static final String CC_BY_SA_4 = "CC BY-SA 4.0"; - public static final String CC_BY_4 = "CC BY 4.0"; - public static final String CC0 = "CC0"; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt new file mode 100644 index 000000000..13e8efb57 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt @@ -0,0 +1,21 @@ +package fr.free.nrw.commons.settings + +object Prefs { + const val GLOBAL_PREFS = "fr.free.nrw.commons.preferences" + + const val TRACKING_ENABLED = "eventLogging" + const val DEFAULT_LICENSE = "defaultLicense" + const val UPLOADS_SHOWING = "uploadsShowing" + const val MANAGED_EXIF_TAGS = "managed_exif_tags" + const val DESCRIPTION_LANGUAGE = "languageDescription" + const val APP_UI_LANGUAGE = "appUiLanguage" + const val KEY_THEME_VALUE = "appThemePref" + + object Licenses { + const val CC_BY_SA_3 = "CC BY-SA 3.0" + const val CC_BY_3 = "CC BY 3.0" + const val CC_BY_SA_4 = "CC BY-SA 4.0" + const val CC_BY_4 = "CC BY 4.0" + const val CC0 = "CC0" + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java deleted file mode 100644 index ff5024b32..000000000 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java +++ /dev/null @@ -1,69 +0,0 @@ -package fr.free.nrw.commons.settings; - -import android.os.Bundle; -import android.view.MenuItem; - -import android.view.View; -import androidx.appcompat.app.AppCompatDelegate; - -import fr.free.nrw.commons.databinding.ActivitySettingsBinding; -import fr.free.nrw.commons.theme.BaseActivity; - -/** - * allows the user to change the settings - */ -public class SettingsActivity extends BaseActivity { - - private ActivitySettingsBinding binding; -// private AppCompatDelegate settingsDelegate; - /** - * to be called when the activity starts - * @param savedInstanceState the previously saved state - */ - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivitySettingsBinding.inflate(getLayoutInflater()); - final View view = binding.getRoot(); - setContentView(view); - - setSupportActionBar(binding.toolbarBinding.toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - // Get an action bar - /** - * takes care of actions taken after the creation has happened - * @param savedInstanceState the saved state - */ - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); -// if (settingsDelegate == null) { -// settingsDelegate = AppCompatDelegate.create(this, null); -// } -// settingsDelegate.onPostCreate(savedInstanceState); - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - /** - * Handle action-bar clicks - * @param item the selected item - * @return true on success, false on failure - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt new file mode 100644 index 000000000..da79244bc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt @@ -0,0 +1,63 @@ +package fr.free.nrw.commons.settings + +import android.os.Bundle +import android.view.MenuItem +import fr.free.nrw.commons.databinding.ActivitySettingsBinding +import fr.free.nrw.commons.theme.BaseActivity + + +/** + * allows the user to change the settings + */ +class SettingsActivity : BaseActivity() { + + private lateinit var binding: ActivitySettingsBinding +// private var settingsDelegate: AppCompatDelegate? = null + + /** + * to be called when the activity starts + * @param savedInstanceState the previously saved state + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySettingsBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + setSupportActionBar(binding.toolbarBinding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + // Get an action bar + /** + * takes care of actions taken after the creation has happened + * @param savedInstanceState the saved state + */ + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) +// if (settingsDelegate == null) { +// settingsDelegate = AppCompatDelegate.create(this, null) +// } +// settingsDelegate?.onPostCreate(savedInstanceState) + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + /** + * Handle action-bar clicks + * @param item the selected item + * @return true on success, false on failure + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + finish() + true + } + else -> super.onOptionsItemSelected(item) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java deleted file mode 100644 index d4ed379f0..000000000 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ /dev/null @@ -1,575 +0,0 @@ -package fr.free.nrw.commons.settings; - -import static android.content.Context.MODE_PRIVATE; - -import android.Manifest.permission; -import android.app.Activity; -import android.app.Dialog; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.net.Uri; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.EditText; -import android.widget.ListView; -import android.widget.TextView; -import androidx.activity.result.ActivityResultCallback; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; -import androidx.preference.ListPreference; -import androidx.preference.MultiSelectListPreference; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceClickListener; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceGroupAdapter; -import androidx.preference.PreferenceScreen; -import androidx.preference.PreferenceViewHolder; -import androidx.recyclerview.widget.RecyclerView.Adapter; -import com.karumi.dexter.Dexter; -import com.karumi.dexter.MultiplePermissionsReport; -import com.karumi.dexter.PermissionToken; -import com.karumi.dexter.listener.PermissionRequest; -import com.karumi.dexter.listener.multi.MultiplePermissionsListener; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.campaigns.CampaignView; -import fr.free.nrw.commons.contributions.ContributionController; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.logging.CommonsLogSender; -import fr.free.nrw.commons.recentlanguages.Language; -import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter; -import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao; -import fr.free.nrw.commons.upload.LanguagesAdapter; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.PermissionUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import javax.inject.Inject; -import javax.inject.Named; -import timber.log.Timber; - -public class SettingsFragment extends PreferenceFragmentCompat { - - @Inject - @Named("default_preferences") - JsonKvStore defaultKvStore; - - @Inject - CommonsLogSender commonsLogSender; - - @Inject - RecentLanguagesDao recentLanguagesDao; - - @Inject - ContributionController contributionController; - - @Inject - LocationServiceManager locationManager; - - private ListPreference themeListPreference; - private Preference descriptionLanguageListPreference; - private Preference appUiLanguageListPreference; - private Preference showDeletionButtonPreference; - private String keyLanguageListPreference; - private TextView recentLanguagesTextView; - private View separator; - private ListView languageHistoryListView; - private static final String GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content"; - - private final ActivityResultLauncher cameraPickLauncherForResult = - registerForActivityResult(new StartActivityForResult(), - result -> { - contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> { - contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks); - }); - }); - - private ActivityResultLauncher inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { - @Override - public void onActivityResult(Map result) { - boolean areAllGranted = true; - for (final boolean b : result.values()) { - areAllGranted = areAllGranted && b; - } - if (!areAllGranted && shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { - contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); - } - } - }); - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - ApplicationlessInjection - .getInstance(getActivity().getApplicationContext()) - .getCommonsApplicationComponent() - .inject(this); - - // Set the preferences from an XML resource - setPreferencesFromResource(R.xml.preferences, rootKey); - - themeListPreference = findPreference(Prefs.KEY_THEME_VALUE); - prepareTheme(); - - MultiSelectListPreference multiSelectListPref = findPreference(Prefs.MANAGED_EXIF_TAGS); - if (multiSelectListPref != null) { - multiSelectListPref.setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof HashSet && !((HashSet) newValue).contains(getString(R.string.exif_tag_location))) { - defaultKvStore.putBoolean("has_user_manually_removed_location", true); - } - return true; - }); - } - - Preference inAppCameraLocationPref = findPreference("inAppCameraLocationPref"); - - inAppCameraLocationPref.setOnPreferenceChangeListener( - (preference, newValue) -> { - boolean isInAppCameraLocationTurnedOn = (boolean) newValue; - if (isInAppCameraLocationTurnedOn) { - createDialogsAndHandleLocationPermissions(getActivity()); - } - return true; - } - ); - - // Gets current language code from shared preferences - String languageCode; - - appUiLanguageListPreference = findPreference("appUiDefaultLanguagePref"); - assert appUiLanguageListPreference != null; - keyLanguageListPreference = appUiLanguageListPreference.getKey(); - languageCode = getCurrentLanguageCode(keyLanguageListPreference); - assert languageCode != null; - if (languageCode.equals("")) { - // If current language code is empty, means none selected by user yet so use phone local - appUiLanguageListPreference.setSummary(Locale.getDefault().getDisplayLanguage()); - } else { - // If any language is selected by user previously, use it - Locale defLocale = createLocale(languageCode); - appUiLanguageListPreference.setSummary((defLocale).getDisplayLanguage(defLocale)); - } - appUiLanguageListPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - prepareAppLanguages(appUiLanguageListPreference.getKey()); - return true; - } - }); - - descriptionLanguageListPreference = findPreference("descriptionDefaultLanguagePref"); - assert descriptionLanguageListPreference != null; - keyLanguageListPreference = descriptionLanguageListPreference.getKey(); - languageCode = getCurrentLanguageCode(keyLanguageListPreference); - assert languageCode != null; - if (languageCode.equals("")) { - // If current language code is empty, means none selected by user yet so use phone local - descriptionLanguageListPreference.setSummary(Locale.getDefault().getDisplayLanguage()); - } else { - // If any language is selected by user previously, use it - Locale defLocale = createLocale(languageCode); - descriptionLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - } - descriptionLanguageListPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - prepareAppLanguages(descriptionLanguageListPreference.getKey()); - return true; - } - }); - - // - showDeletionButtonPreference = findPreference("displayDeletionButton"); - if (showDeletionButtonPreference != null) { - showDeletionButtonPreference.setOnPreferenceChangeListener((preference, newValue) -> { - boolean isEnabled = (boolean) newValue; - // Save preference when user toggles the button - defaultKvStore.putBoolean("displayDeletionButton", isEnabled); - return true; - }); - } - - - Preference betaTesterPreference = findPreference("becomeBetaTester"); - betaTesterPreference.setOnPreferenceClickListener(preference -> { - Utils.handleWebUrl(getActivity(), Uri.parse(getResources().getString(R.string.beta_opt_in_link))); - return true; - }); - Preference sendLogsPreference = findPreference("sendLogFile"); - sendLogsPreference.setOnPreferenceClickListener(preference -> { - checkPermissionsAndSendLogs(); - return true; - }); - - Preference documentBasedPickerPreference = findPreference("openDocumentPhotoPickerPref"); - documentBasedPickerPreference.setOnPreferenceChangeListener( - (preference, newValue) -> { - boolean isGetContentPickerTurnedOn = !(boolean) newValue; - if (isGetContentPickerTurnedOn) { - showLocationLossWarning(); - } - return true; - } - ); - // Disable some settings when not logged in. - if (defaultKvStore.getBoolean("login_skipped", false)) { - findPreference("useExternalStorage").setEnabled(false); - findPreference("useAuthorName").setEnabled(false); - findPreference("displayNearbyCardView").setEnabled(false); - findPreference("descriptionDefaultLanguagePref").setEnabled(false); - findPreference("displayLocationPermissionForCardView").setEnabled(false); - findPreference(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE).setEnabled(false); - findPreference("managed_exif_tags").setEnabled(false); - findPreference("openDocumentPhotoPickerPref").setEnabled(false); - findPreference("inAppCameraLocationPref").setEnabled(false); - } - } - - /** - * Asks users to provide location access - * - * @param activity - */ - private void createDialogsAndHandleLocationPermissions(Activity activity) { - inAppCameraLocationPermissionLauncher.launch(new String[]{permission.ACCESS_FINE_LOCATION}); - } - - /** - * On some devices, the new Photo Picker with GET_CONTENT takeover - * redacts location tags from EXIF metadata - * - * Show warning to the user when ACTION_GET_CONTENT intent is enabled - */ - private void showLocationLossWarning() { - DialogUtil.showAlertDialog( - getActivity(), - null, - getString(R.string.location_loss_warning), - getString(R.string.ok), - getString(R.string.read_help_link), - () -> {}, - () -> Utils.handleWebUrl(requireContext(), Uri.parse(GET_CONTENT_PICKER_HELP_URL)), - null, - true - ); - } - - @Override - protected Adapter onCreateAdapter(final PreferenceScreen preferenceScreen) { - return new PreferenceGroupAdapter(preferenceScreen) { - @Override - public void onBindViewHolder(PreferenceViewHolder holder, int position) { - super.onBindViewHolder(holder, position); - Preference preference = getItem(position); - View iconFrame = holder.itemView.findViewById(R.id.icon_frame); - if (iconFrame != null) { - iconFrame.setVisibility(View.GONE); - } - } - }; - } - - /** - * Sets the theme pref - */ - private void prepareTheme() { - themeListPreference.setOnPreferenceChangeListener((preference, newValue) -> { - getActivity().recreate(); - return true; - }); - } - - /** - * Prepare and Show language selection dialog box - * Uses previously saved language if there is any, if not uses phone locale as initial language. - * Disable default/already selected language from dialog box - * Get ListPreference key and act accordingly for each ListPreference. - * saves value chosen by user to shared preferences - * to remember later and recall MainActivity to reflect language changes - * @param keyListPreference - */ - private void prepareAppLanguages(final String keyListPreference) { - - // Gets current language code from shared preferences - final String languageCode = getCurrentLanguageCode(keyListPreference); - final List recentLanguages = recentLanguagesDao.getRecentLanguages(); - HashMap selectedLanguages = new HashMap<>(); - - if (keyListPreference.equals("appUiDefaultLanguagePref")) { - - assert languageCode != null; - if (languageCode.equals("")) { - selectedLanguages.put(0, Locale.getDefault().getLanguage()); - } else { - selectedLanguages.put(0, languageCode); - } - } else if (keyListPreference.equals("descriptionDefaultLanguagePref")) { - - assert languageCode != null; - if (languageCode.equals("")) { - selectedLanguages.put(0, Locale.getDefault().getLanguage()); - - } else { - selectedLanguages.put(0, languageCode); - } - } - - LanguagesAdapter languagesAdapter = new LanguagesAdapter( - getActivity(), - selectedLanguages - ); - - Dialog dialog = new Dialog(getActivity()); - dialog.setContentView(R.layout.dialog_select_language); - dialog.setCanceledOnTouchOutside(true); - dialog.getWindow().setLayout((int)(getActivity().getResources().getDisplayMetrics().widthPixels*0.90), - (int)(getActivity().getResources().getDisplayMetrics().heightPixels*0.90)); - dialog.show(); - - EditText editText = dialog.findViewById(R.id.search_language); - ListView listView = dialog.findViewById(R.id.language_list); - languageHistoryListView = dialog.findViewById(R.id.language_history_list); - recentLanguagesTextView = dialog.findViewById(R.id.recent_searches); - separator = dialog.findViewById(R.id.separator); - - setUpRecentLanguagesSection(recentLanguages, selectedLanguages); - - listView.setAdapter(languagesAdapter); - - editText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, - int i2) { - hideRecentLanguagesSection(); - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, - int i2) { - languagesAdapter.getFilter().filter(charSequence); - } - - @Override - public void afterTextChanged(Editable editable) { - - } - }); - - languageHistoryListView.setOnItemClickListener((adapterView, view, position, id) -> { - onRecentLanguageClicked(keyListPreference, dialog, adapterView, position); - }); - - listView.setOnItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int i, - long l) { - String languageCode = ((LanguagesAdapter) adapterView.getAdapter()) - .getLanguageCode(i); - final String languageName = ((LanguagesAdapter) adapterView.getAdapter()) - .getLanguageName(i); - final boolean isExists = recentLanguagesDao.findRecentLanguage(languageCode); - if (isExists) { - recentLanguagesDao.deleteRecentLanguage(languageCode); - } - recentLanguagesDao.addRecentLanguage(new Language(languageName, languageCode)); - saveLanguageValue(languageCode, keyListPreference); - Locale defLocale = createLocale(languageCode); - if(keyListPreference.equals("appUiDefaultLanguagePref")) { - appUiLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - setLocale(requireActivity(), languageCode); - getActivity().recreate(); - final Intent intent = new Intent(getActivity(), MainActivity.class); - startActivity(intent); - }else { - descriptionLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - } - dialog.dismiss(); - } - }); - - dialog.setOnDismissListener( - dialogInterface -> languagesAdapter.getFilter().filter("")); - } - - /** - * Set up recent languages section - * - * @param recentLanguages recently used languages - * @param selectedLanguages selected languages - */ - private void setUpRecentLanguagesSection(List recentLanguages, - HashMap selectedLanguages) { - if (recentLanguages.isEmpty()) { - languageHistoryListView.setVisibility(View.GONE); - recentLanguagesTextView.setVisibility(View.GONE); - separator.setVisibility(View.GONE); - } else { - if (recentLanguages.size() > 5) { - for (int i = recentLanguages.size()-1; i >=5; i--) { - recentLanguagesDao - .deleteRecentLanguage(recentLanguages.get(i).getLanguageCode()); - } - } - languageHistoryListView.setVisibility(View.VISIBLE); - recentLanguagesTextView.setVisibility(View.VISIBLE); - separator.setVisibility(View.VISIBLE); - final RecentLanguagesAdapter recentLanguagesAdapter - = new RecentLanguagesAdapter( - getActivity(), - recentLanguagesDao.getRecentLanguages(), - selectedLanguages); - languageHistoryListView.setAdapter(recentLanguagesAdapter); - } - } - - /** - * Handles click event for recent language section - */ - private void onRecentLanguageClicked(String keyListPreference, Dialog dialog, AdapterView adapterView, - int position) { - final String recentLanguageCode = ((RecentLanguagesAdapter) adapterView.getAdapter()) - .getLanguageCode(position); - final String recentLanguageName = ((RecentLanguagesAdapter) adapterView.getAdapter()) - .getLanguageName(position); - final boolean isExists = recentLanguagesDao.findRecentLanguage(recentLanguageCode); - if (isExists) { - recentLanguagesDao.deleteRecentLanguage(recentLanguageCode); - } - recentLanguagesDao.addRecentLanguage( - new Language(recentLanguageName, recentLanguageCode)); - saveLanguageValue(recentLanguageCode, keyListPreference); - final Locale defLocale = createLocale(recentLanguageCode); - if (keyListPreference.equals("appUiDefaultLanguagePref")) { - appUiLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - setLocale(requireActivity(), recentLanguageCode); - getActivity().recreate(); - final Intent intent = new Intent(getActivity(), MainActivity.class); - startActivity(intent); - } else { - descriptionLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - } - dialog.dismiss(); - } - - /** - * Remove the section of recent languages - */ - private void hideRecentLanguagesSection() { - languageHistoryListView.setVisibility(View.GONE); - recentLanguagesTextView.setVisibility(View.GONE); - separator.setVisibility(View.GONE); - } - - /** - * Changing the default app language with selected one and save it to SharedPreferences - */ - public void setLocale(final Activity activity, String userSelectedValue) { - if (userSelectedValue.equals("")) { - userSelectedValue = Locale.getDefault().getLanguage(); - } - final Locale locale = createLocale(userSelectedValue); - Locale.setDefault(locale); - final Configuration configuration = new Configuration(); - configuration.locale = locale; - activity.getBaseContext().getResources().updateConfiguration(configuration, - activity.getBaseContext().getResources().getDisplayMetrics()); - - final SharedPreferences.Editor editor = activity.getSharedPreferences("Settings", MODE_PRIVATE).edit(); - editor.putString("language", userSelectedValue); - editor.apply(); - } - - /** - * Create Locale based on different types of language codes - * @param languageCode - * @return Locale and throws error for invalid language codes - */ - public static Locale createLocale(String languageCode) { - String[] parts = languageCode.split("-"); - switch (parts.length) { - case 1: - return new Locale(parts[0]); - case 2: - return new Locale(parts[0], parts[1]); - case 3: - return new Locale(parts[0], parts[1], parts[2]); - default: - throw new IllegalArgumentException("Invalid language code: " + languageCode); - } - } - - /** - * Save userselected language in List Preference - * @param userSelectedValue - * @param preferenceKey - */ - private void saveLanguageValue(final String userSelectedValue, final String preferenceKey) { - if (preferenceKey.equals("appUiDefaultLanguagePref")) { - defaultKvStore.putString(Prefs.APP_UI_LANGUAGE, userSelectedValue); - } else if (preferenceKey.equals("descriptionDefaultLanguagePref")) { - defaultKvStore.putString(Prefs.DESCRIPTION_LANGUAGE, userSelectedValue); - } - } - - /** - * Gets current language code from shared preferences - * @param preferenceKey - * @return - */ - private String getCurrentLanguageCode(final String preferenceKey) { - if (preferenceKey.equals("appUiDefaultLanguagePref")) { - return defaultKvStore.getString(Prefs.APP_UI_LANGUAGE, ""); - } - if (preferenceKey.equals("descriptionDefaultLanguagePref")) { - return defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""); - } - return null; - } - - /** - * First checks for external storage permissions and then sends logs via email - */ - private void checkPermissionsAndSendLogs() { - if (PermissionUtils.hasPermission(getActivity(), PermissionUtils.getPERMISSIONS_STORAGE())) { - commonsLogSender.send(getActivity(), null); - } else { - requestExternalStoragePermissions(); - } - } - - /** - * Requests external storage permissions and shows a toast stating that log collection has - * started - */ - private void requestExternalStoragePermissions() { - Dexter.withActivity(getActivity()) - .withPermissions(PermissionUtils.getPERMISSIONS_STORAGE()) - .withListener(new MultiplePermissionsListener() { - @Override - public void onPermissionsChecked(MultiplePermissionsReport report) { - ViewUtil.showLongToast(getActivity(), - getResources().getString(R.string.log_collection_started)); - } - - @Override - public void onPermissionRationaleShouldBeShown( - List permissions, PermissionToken token) { - - } - }) - .onSameThread() - .check(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt new file mode 100644 index 000000000..53f6b28fe --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt @@ -0,0 +1,556 @@ +package fr.free.nrw.commons.settings + +import android.Manifest.permission +import android.app.Activity +import android.app.Dialog +import android.content.Context.MODE_PRIVATE +import android.content.Intent +import android.content.res.Configuration +import android.net.Uri +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.widget.AdapterView +import android.widget.EditText +import android.widget.ListView +import android.widget.TextView +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.preference.ListPreference +import androidx.preference.MultiSelectListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceGroupAdapter +import androidx.preference.PreferenceScreen +import androidx.preference.PreferenceViewHolder +import androidx.recyclerview.widget.RecyclerView.Adapter +import com.karumi.dexter.Dexter +import com.karumi.dexter.MultiplePermissionsReport +import com.karumi.dexter.PermissionToken +import com.karumi.dexter.listener.PermissionRequest +import com.karumi.dexter.listener.multi.MultiplePermissionsListener +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.campaigns.CampaignView +import fr.free.nrw.commons.contributions.ContributionController +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LocationServiceManager +import fr.free.nrw.commons.logging.CommonsLogSender +import fr.free.nrw.commons.recentlanguages.Language +import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter +import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao +import fr.free.nrw.commons.upload.LanguagesAdapter +import fr.free.nrw.commons.utils.DialogUtil +import fr.free.nrw.commons.utils.PermissionUtils +import fr.free.nrw.commons.utils.ViewUtil +import java.util.Locale +import javax.inject.Inject +import javax.inject.Named + +class SettingsFragment : PreferenceFragmentCompat() { + + @Inject + @field: Named("default_preferences") + lateinit var defaultKvStore: JsonKvStore + + @Inject + lateinit var commonsLogSender: CommonsLogSender + + @Inject + lateinit var recentLanguagesDao: RecentLanguagesDao + + @Inject + lateinit var contributionController: ContributionController + + @Inject + lateinit var locationManager: LocationServiceManager + + private var themeListPreference: ListPreference? = null + private var descriptionLanguageListPreference: Preference? = null + private var appUiLanguageListPreference: Preference? = null + private var showDeletionButtonPreference: Preference? = null + private var keyLanguageListPreference: String? = null + private var recentLanguagesTextView: TextView? = null + private var separator: View? = null + private var languageHistoryListView: ListView? = null + private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher> + private val GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content" + + private val cameraPickLauncherForResult: ActivityResultLauncher = + registerForActivityResult(StartActivityForResult()) { result -> + contributionController.handleActivityResultWithCallback(requireActivity()) { callbacks -> + contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks) + } + } + + /** + * to be called when the fragment creates preferences + * @param savedInstanceState the previously saved state + * @param rootKey the root key for preferences + */ + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + ApplicationlessInjection + .getInstance(requireActivity().applicationContext) + .commonsApplicationComponent + .inject(this) + + // Set the preferences from an XML resource + setPreferencesFromResource(R.xml.preferences, rootKey) + + themeListPreference = findPreference(Prefs.KEY_THEME_VALUE) + prepareTheme() + + val multiSelectListPref: MultiSelectListPreference? = findPreference( + Prefs.MANAGED_EXIF_TAGS + ) + multiSelectListPref?.setOnPreferenceChangeListener { _, newValue -> + if (newValue is HashSet<*> && !newValue.contains(getString(R.string.exif_tag_location))) + { + defaultKvStore.putBoolean("has_user_manually_removed_location", true) + } + true + } + + val inAppCameraLocationPref: Preference? = findPreference("inAppCameraLocationPref") + inAppCameraLocationPref?.setOnPreferenceChangeListener { _, newValue -> + val isInAppCameraLocationTurnedOn = newValue as Boolean + if (isInAppCameraLocationTurnedOn) { + createDialogsAndHandleLocationPermissions(requireActivity()) + } + true + } + + inAppCameraLocationPermissionLauncher = registerForActivityResult( + RequestMultiplePermissions() + ) { result -> + var areAllGranted = true + for (b in result.values) { + areAllGranted = areAllGranted && b + } + if ( + !areAllGranted + && + shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION) + ) { + contributionController.handleShowRationaleFlowCameraLocation( + requireActivity(), + inAppCameraLocationPermissionLauncher, + cameraPickLauncherForResult + ) + } + } + + // Gets current language code from shared preferences + var languageCode: String? + + appUiLanguageListPreference = findPreference("appUiDefaultLanguagePref") + appUiLanguageListPreference?.let { appUiLanguageListPreference -> + keyLanguageListPreference = appUiLanguageListPreference.key + languageCode = getCurrentLanguageCode(keyLanguageListPreference!!) + + languageCode?.let { code -> + if (code.isEmpty()) { + // If current language code is empty, means none selected by user yet so use + // phone locale + appUiLanguageListPreference.summary = Locale.getDefault().displayLanguage + } else { + // If any language is selected by user previously, use it + val defLocale = createLocale(code) + appUiLanguageListPreference.summary = defLocale.getDisplayLanguage(defLocale) + } + } + + appUiLanguageListPreference.setOnPreferenceClickListener { + prepareAppLanguages(keyLanguageListPreference!!) + true + } + } + + descriptionLanguageListPreference = findPreference("descriptionDefaultLanguagePref") + descriptionLanguageListPreference?.let { descriptionLanguageListPreference -> + languageCode = getCurrentLanguageCode(descriptionLanguageListPreference.key) + + languageCode?.let { code -> + if (code.isEmpty()) { + // If current language code is empty, means none selected by user yet so use + // phone locale + descriptionLanguageListPreference.summary = Locale.getDefault().displayLanguage + } else { + // If any language is selected by user previously, use it + val defLocale = createLocale(code) + descriptionLanguageListPreference.summary = defLocale.getDisplayLanguage( + defLocale + ) + } + } + + descriptionLanguageListPreference.setOnPreferenceClickListener { + prepareAppLanguages(it.key) + true + } + } + + showDeletionButtonPreference = findPreference("displayDeletionButton") + showDeletionButtonPreference?.setOnPreferenceChangeListener { _, newValue -> + val isEnabled = newValue as Boolean + // Save preference when user toggles the button + defaultKvStore.putBoolean("displayDeletionButton", isEnabled) + true + } + + val betaTesterPreference: Preference? = findPreference("becomeBetaTester") + betaTesterPreference?.setOnPreferenceClickListener { + Utils.handleWebUrl(requireActivity(), Uri.parse(getString(R.string.beta_opt_in_link))) + true + } + + val sendLogsPreference: Preference? = findPreference("sendLogFile") + sendLogsPreference?.setOnPreferenceClickListener { + checkPermissionsAndSendLogs() + true + } + + val documentBasedPickerPreference: Preference? = findPreference( + "openDocumentPhotoPickerPref" + ) + documentBasedPickerPreference?.setOnPreferenceChangeListener { _, newValue -> + val isGetContentPickerTurnedOn = newValue as Boolean + if (!isGetContentPickerTurnedOn) { + showLocationLossWarning() + } + true + } + + // Disable some settings when not logged in. + if (defaultKvStore.getBoolean("login_skipped", false)) { + findPreference("useExternalStorage")?.isEnabled = false + findPreference("useAuthorName")?.isEnabled = false + findPreference("displayNearbyCardView")?.isEnabled = false + findPreference("descriptionDefaultLanguagePref")?.isEnabled = false + findPreference("displayLocationPermissionForCardView")?.isEnabled = false + findPreference(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE)?.isEnabled = false + findPreference("managed_exif_tags")?.isEnabled = false + findPreference("openDocumentPhotoPickerPref")?.isEnabled = false + findPreference("inAppCameraLocationPref")?.isEnabled = false + } + } + + /** + * Asks users to provide location access + * + * @param activity + */ + private fun createDialogsAndHandleLocationPermissions(activity: Activity) { + inAppCameraLocationPermissionLauncher.launch(arrayOf(permission.ACCESS_FINE_LOCATION)) + } + + /** + * On some devices, the new Photo Picker with GET_CONTENT takeover + * redacts location tags from EXIF metadata + * + * Show warning to the user when ACTION_GET_CONTENT intent is enabled + */ + private fun showLocationLossWarning() { + DialogUtil.showAlertDialog( + requireActivity(), + null, + getString(R.string.location_loss_warning), + getString(R.string.ok), + getString(R.string.read_help_link), + { }, + { Utils.handleWebUrl(requireContext(), Uri.parse(GET_CONTENT_PICKER_HELP_URL)) }, + null, + true + ) + } + + override fun onCreateAdapter(preferenceScreen: PreferenceScreen): Adapter + { + return object : PreferenceGroupAdapter(preferenceScreen) { + override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) { + super.onBindViewHolder(holder, position) + val preference = getItem(position) + val iconFrame: View? = holder.itemView.findViewById(R.id.icon_frame) + iconFrame?.visibility = View.GONE + } + } + } + + /** + * Sets the theme pref + */ + private fun prepareTheme() { + themeListPreference?.setOnPreferenceChangeListener { _, _ -> + requireActivity().recreate() + true + } + } + + /** + * Prepare and Show language selection dialog box + * Uses previously saved language if there is any, if not uses phone locale as initial language. + * Disable default/already selected language from dialog box + * Get ListPreference key and act accordingly for each ListPreference. + * saves value chosen by user to shared preferences + * to remember later and recall MainActivity to reflect language changes + * @param keyListPreference + */ + private fun prepareAppLanguages(keyListPreference: String) { + // Gets current language code from shared preferences + val languageCode = getCurrentLanguageCode(keyListPreference) + val recentLanguages = recentLanguagesDao.getRecentLanguages() + val selectedLanguages = hashMapOf() + + if (keyListPreference == "appUiDefaultLanguagePref") { + if (languageCode.isNullOrEmpty()) { + selectedLanguages[0] = Locale.getDefault().language + } else { + selectedLanguages[0] = languageCode + } + } else if (keyListPreference == "descriptionDefaultLanguagePref") { + if (languageCode.isNullOrEmpty()) { + selectedLanguages[0] = Locale.getDefault().language + } else { + selectedLanguages[0] = languageCode + } + } + + val languagesAdapter = LanguagesAdapter(requireActivity(), selectedLanguages) + + val dialog = Dialog(requireActivity()) + dialog.setContentView(R.layout.dialog_select_language) + dialog.setCanceledOnTouchOutside(true) + dialog.window?.setLayout( + (resources.displayMetrics.widthPixels * 0.90).toInt(), + (resources.displayMetrics.heightPixels * 0.90).toInt() + ) + dialog.show() + + val editText: EditText = dialog.findViewById(R.id.search_language) + val listView: ListView = dialog.findViewById(R.id.language_list) + languageHistoryListView = dialog.findViewById(R.id.language_history_list) + recentLanguagesTextView = dialog.findViewById(R.id.recent_searches) + separator = dialog.findViewById(R.id.separator) + + setUpRecentLanguagesSection(recentLanguages, selectedLanguages) + + listView.adapter = languagesAdapter + + editText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, start: Int, count: Int, after: Int) { + hideRecentLanguagesSection() + } + + override fun onTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) { + languagesAdapter.filter.filter(charSequence) + } + + override fun afterTextChanged(editable: Editable?) {} + }) + + languageHistoryListView?.setOnItemClickListener { adapterView, _, position, _ -> + onRecentLanguageClicked(keyListPreference, dialog, adapterView, position) + } + + listView.setOnItemClickListener { adapterView, _, position, _ -> + val lCode = (adapterView.adapter as LanguagesAdapter).getLanguageCode(position) + val languageName = (adapterView.adapter as LanguagesAdapter).getLanguageName(position) + val isExists = recentLanguagesDao.findRecentLanguage(lCode) + if (isExists) { + recentLanguagesDao.deleteRecentLanguage(lCode) + } + recentLanguagesDao.addRecentLanguage(Language(languageName, lCode)) + saveLanguageValue(lCode, keyListPreference) + val defLocale = createLocale(lCode) + if (keyListPreference == "appUiDefaultLanguagePref") { + appUiLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale) + setLocale(requireActivity(), lCode) + requireActivity().recreate() + val intent = Intent(requireActivity(), MainActivity::class.java) + startActivity(intent) + } else { + descriptionLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale) + } + dialog.dismiss() + } + + dialog.setOnDismissListener { languagesAdapter.filter.filter("") } + } + + /** + * Set up recent languages section + * + * @param recentLanguages recently used languages + * @param selectedLanguages selected languages + */ + private fun setUpRecentLanguagesSection( + recentLanguages: List, + selectedLanguages: HashMap + ) { + if (recentLanguages.isEmpty()) { + languageHistoryListView?.visibility = View.GONE + recentLanguagesTextView?.visibility = View.GONE + separator?.visibility = View.GONE + } else { + if (recentLanguages.size > 5) { + for (i in recentLanguages.size - 1 downTo 5) { + recentLanguagesDao.deleteRecentLanguage(recentLanguages[i].languageCode) + } + } + languageHistoryListView?.visibility = View.VISIBLE + recentLanguagesTextView?.visibility = View.VISIBLE + separator?.visibility = View.VISIBLE + val recentLanguagesAdapter = RecentLanguagesAdapter( + requireActivity(), + recentLanguagesDao.getRecentLanguages(), + selectedLanguages + ) + languageHistoryListView?.adapter = recentLanguagesAdapter + } + } + + /** + * Handles click event for recent language section + */ + private fun onRecentLanguageClicked( + keyListPreference: String, + dialog: Dialog, + adapterView: AdapterView<*>, + position: Int + ) { + val recentLanguageCode = (adapterView.adapter as RecentLanguagesAdapter).getLanguageCode(position) + val recentLanguageName = (adapterView.adapter as RecentLanguagesAdapter).getLanguageName(position) + val isExists = recentLanguagesDao.findRecentLanguage(recentLanguageCode) + if (isExists) { + recentLanguagesDao.deleteRecentLanguage(recentLanguageCode) + } + recentLanguagesDao.addRecentLanguage(Language(recentLanguageName, recentLanguageCode)) + saveLanguageValue(recentLanguageCode, keyListPreference) + val defLocale = createLocale(recentLanguageCode) + if (keyListPreference == "appUiDefaultLanguagePref") { + appUiLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale) + setLocale(requireActivity(), recentLanguageCode) + requireActivity().recreate() + val intent = Intent(requireActivity(), MainActivity::class.java) + startActivity(intent) + } else { + descriptionLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale) + } + dialog.dismiss() + } + + /** + * Remove the section of recent languages + */ + private fun hideRecentLanguagesSection() { + languageHistoryListView?.visibility = View.GONE + recentLanguagesTextView?.visibility = View.GONE + separator?.visibility = View.GONE + } + + /** + * Changing the default app language with selected one and save it to SharedPreferences + */ + fun setLocale(activity: Activity, userSelectedValue: String) { + var selectedLanguage = userSelectedValue + if (selectedLanguage == "") { + selectedLanguage = Locale.getDefault().language + } + val locale = createLocale(selectedLanguage) + Locale.setDefault(locale) + val configuration = Configuration() + configuration.locale = locale + activity.baseContext.resources.updateConfiguration(configuration, activity.baseContext.resources.displayMetrics) + + val editor = activity.getSharedPreferences("Settings", MODE_PRIVATE).edit() + editor.putString("language", selectedLanguage) + editor.apply() + } + + /** + * Create Locale based on different types of language codes + * @param languageCode + * @return Locale and throws error for invalid language codes + */ + fun createLocale(languageCode: String): Locale { + val parts = languageCode.split("-") + return when (parts.size) { + 1 -> Locale(parts[0]) + 2 -> Locale(parts[0], parts[1]) + 3 -> Locale(parts[0], parts[1], parts[2]) + else -> throw IllegalArgumentException("Invalid language code: $languageCode") + } + } + + /** + * Save userSelected language in List Preference + * @param userSelectedValue + * @param preferenceKey + */ + private fun saveLanguageValue(userSelectedValue: String, preferenceKey: String) { + when (preferenceKey) { + "appUiDefaultLanguagePref" -> defaultKvStore.putString(Prefs.APP_UI_LANGUAGE, userSelectedValue) + "descriptionDefaultLanguagePref" -> defaultKvStore.putString(Prefs.DESCRIPTION_LANGUAGE, userSelectedValue) + } + } + + /** + * Gets current language code from shared preferences + * @param preferenceKey + * @return + */ + private fun getCurrentLanguageCode(preferenceKey: String): String? { + return when (preferenceKey) { + "appUiDefaultLanguagePref" -> defaultKvStore.getString( + Prefs.APP_UI_LANGUAGE, "" + ) + "descriptionDefaultLanguagePref" -> defaultKvStore.getString( + Prefs.DESCRIPTION_LANGUAGE, "" + ) + else -> null + } + } + + /** + * First checks for external storage permissions and then sends logs via email + */ + private fun checkPermissionsAndSendLogs() { + if ( + PermissionUtils.hasPermission( + requireActivity(), + PermissionUtils.PERMISSIONS_STORAGE + ) + ) { + commonsLogSender.send(requireActivity(), null) + } else { + requestExternalStoragePermissions() + } + } + + /** + * Requests external storage permissions and shows a toast stating that log collection has + * started + */ + private fun requestExternalStoragePermissions() { + Dexter.withActivity(requireActivity()) + .withPermissions(*PermissionUtils.PERMISSIONS_STORAGE) + .withListener(object : MultiplePermissionsListener { + override fun onPermissionsChecked(report: MultiplePermissionsReport) { + ViewUtil.showLongToast(requireActivity(), getString(R.string.log_collection_started)) + } + + override fun onPermissionRationaleShouldBeShown( + permissions: List, token: PermissionToken + ) { + // No action needed + } + }) + .onSameThread() + .check() + } + +} From 088dd2479e56523591085d5bb0a076c2903d1813 Mon Sep 17 00:00:00 2001 From: u7479759 Date: Thu, 21 Nov 2024 23:42:54 +1100 Subject: [PATCH 34/74] Changed to data classes, and added immutability (#5905) Co-authored-by: Jinniu Du <127721018+Donutcheese@users.noreply.github.com> --- .../profile/achievements/Achievements.kt | 121 ++++------------ .../achievements/AchievementsFragment.java | 134 ++++++++++-------- .../profile/achievements/FeaturedImages.kt | 2 +- 3 files changed, 104 insertions(+), 153 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/Achievements.kt b/app/src/main/java/fr/free/nrw/commons/profile/achievements/Achievements.kt index 861040fcf..7b23db2cd 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/achievements/Achievements.kt +++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/Achievements.kt @@ -1,104 +1,45 @@ package fr.free.nrw.commons.profile.achievements /** - * Represents Achievements class and stores all the parameters + * Represents Achievements data class and stores all the parameters. + * Immutable version with default values for optional properties. */ -class Achievements { +data class Achievements( + val uniqueUsedImages: Int = 0, + val articlesUsingImages: Int = 0, + val thanksReceived: Int = 0, + val featuredImages: Int = 0, + val qualityImages: Int = 0, + val imagesUploaded: Int = 0, + val revertCount: Int = 0 +) { /** - * The count of unique images used by the wiki. - * @return The count of unique images used. - * @param uniqueUsedImages The count to set for unique images used. - */ - var uniqueUsedImages = 0 - private var articlesUsingImages = 0 - - /** - * The count of thanks received. - * @return The count of thanks received. - * @param thanksReceived The count to set for thanks received. - */ - var thanksReceived = 0 - - /** - * The count of featured images. - * @return The count of featured images. - * @param featuredImages The count to set for featured images. - */ - var featuredImages = 0 - - /** - * The count of quality images. - * @return The count of quality images. - * @param qualityImages The count to set for quality images. - */ - var qualityImages = 0 - - /** - * The count of images uploaded. - * @return The count of images uploaded. - * @param imagesUploaded The count to set for images uploaded. - */ - var imagesUploaded = 0 - private var revertCount = 0 - - constructor() {} - - /** - * constructor for achievements class to set its data members - * @param uniqueUsedImages - * @param articlesUsingImages - * @param thanksReceived - * @param featuredImages - * @param imagesUploaded - * @param revertCount - */ - constructor( - uniqueUsedImages: Int, - articlesUsingImages: Int, - thanksReceived: Int, - featuredImages: Int, - qualityImages: Int, - imagesUploaded: Int, - revertCount: Int, - ) { - this.uniqueUsedImages = uniqueUsedImages - this.articlesUsingImages = articlesUsingImages - this.thanksReceived = thanksReceived - this.featuredImages = featuredImages - this.qualityImages = qualityImages - this.imagesUploaded = imagesUploaded - this.revertCount = revertCount - } - - /** - * used to calculate the percentages of images that haven't been reverted - * @return + * Used to calculate the percentages of images that haven't been reverted. + * Returns 100 if imagesUploaded is 0 to avoid division by zero. */ val notRevertPercentage: Int - get() = - try { - (imagesUploaded - revertCount) * 100 / imagesUploaded - } catch (divideByZero: ArithmeticException) { - 100 - } + get() = if (imagesUploaded > 0) { + (imagesUploaded - revertCount) * 100 / imagesUploaded + } else { + 100 + } companion object { /** - * Get Achievements object from FeedbackResponse + * Get Achievements object from FeedbackResponse. * - * @param response - * @return + * @param response The feedback response to convert. + * @return An Achievements object with values from the response. */ @JvmStatic - fun from(response: FeedbackResponse): Achievements = - Achievements( - response.uniqueUsedImages, - response.articlesUsingImages, - response.thanksReceived, - response.featuredImages.featuredPicturesOnWikimediaCommons, - response.featuredImages.qualityImages, - 0, - response.deletedUploads, - ) + fun from(response: FeedbackResponse): Achievements = Achievements( + uniqueUsedImages = response.uniqueUsedImages, + articlesUsingImages = response.articlesUsingImages, + thanksReceived = response.thanksReceived, + featuredImages = response.featuredImages.featuredPicturesOnWikimediaCommons, + qualityImages = response.featuredImages.qualityImages, + imagesUploaded = 0, // Assuming imagesUploaded should be 0 + revertCount = response.deletedUploads + ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java index f44b7eb6d..ef6a323b2 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java @@ -105,7 +105,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { // Used for the setting the size of imageView at runtime ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) - binding.achievementBadgeImage.getLayoutParams(); + binding.achievementBadgeImage.getLayoutParams(); params.height = (int) (height * BADGE_IMAGE_HEIGHT_RATIO); params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO); binding.achievementBadgeImage.requestLayout(); @@ -186,37 +186,37 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { try{ compositeDisposable.add(okHttpJsonApiClient - .getAchievements(Objects.requireNonNull(userName)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null) { - setUploadCount(Achievements.from(response)); - } else { - Timber.d("success"); - binding.layoutImageReverts.setVisibility(View.INVISIBLE); - binding.achievementBadgeImage.setVisibility(View.INVISIBLE); - // If the number of edits made by the user are more than 150,000 - // in some cases such high number of wiki edit counts cause the - // achievements calculator to fail in some cases, for more details - // refer Issue: #3295 - if (numberOfEdits <= 150000) { - showSnackBarWithRetry(false); - } else { - showSnackBarWithRetry(true); - } - } - }, - t -> { - Timber.e(t, "Fetching achievements statistics failed"); - if (numberOfEdits <= 150000) { - showSnackBarWithRetry(false); - } else { - showSnackBarWithRetry(true); - } + .getAchievements(Objects.requireNonNull(userName)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + response -> { + if (response != null) { + setUploadCount(Achievements.from(response)); + } else { + Timber.d("success"); + binding.layoutImageReverts.setVisibility(View.INVISIBLE); + binding.achievementBadgeImage.setVisibility(View.INVISIBLE); + // If the number of edits made by the user are more than 150,000 + // in some cases such high number of wiki edit counts cause the + // achievements calculator to fail in some cases, for more details + // refer Issue: #3295 + if (numberOfEdits <= 150000) { + showSnackBarWithRetry(false); + } else { + showSnackBarWithRetry(true); } - )); + } + }, + t -> { + Timber.e(t, "Fetching achievements statistics failed"); + if (numberOfEdits <= 150000) { + showSnackBarWithRetry(false); + } else { + showSnackBarWithRetry(true); + } + } + )); } catch (Exception e){ Timber.d(e+"success"); @@ -233,15 +233,15 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { return; } compositeDisposable.add(okHttpJsonApiClient - .getWikidataEdits(userName) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(edits -> { - numberOfEdits = edits; - binding.wikidataEdits.setText(String.valueOf(edits)); - }, e -> { - Timber.e("Error:" + e); - })); + .getWikidataEdits(userName) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(edits -> { + numberOfEdits = edits; + binding.wikidataEdits.setText(String.valueOf(edits)); + }, e -> { + Timber.e("Error:" + e); + })); } /** @@ -255,11 +255,11 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { if (tooManyAchievements) { binding.progressBar.setVisibility(View.GONE); ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content), - R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry, view -> setAchievements()); + R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry, view -> setAchievements()); } else { binding.progressBar.setVisibility(View.GONE); ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content), - R.string.achievements_fetch_failed, R.string.retry, view -> setAchievements()); + R.string.achievements_fetch_failed, R.string.retry, view -> setAchievements()); } } @@ -277,16 +277,16 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { private void setUploadCount(Achievements achievements) { if (checkAccount()) { compositeDisposable.add(okHttpJsonApiClient - .getUploadCount(Objects.requireNonNull(userName)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - uploadCount -> setAchievementsUploadCount(achievements, uploadCount), - t -> { - Timber.e(t, "Fetching upload count failed"); - onError(); - } - )); + .getUploadCount(Objects.requireNonNull(userName)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + uploadCount -> setAchievementsUploadCount(achievements, uploadCount), + t -> { + Timber.e(t, "Fetching upload count failed"); + onError(); + } + )); } } @@ -295,8 +295,18 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { * @param uploadCount */ private void setAchievementsUploadCount(Achievements achievements, int uploadCount) { - achievements.setImagesUploaded(uploadCount); - hideProgressBar(achievements); + // Create a new instance of Achievements with updated imagesUploaded + Achievements updatedAchievements = new Achievements( + achievements.getUniqueUsedImages(), + achievements.getArticlesUsingImages(), + achievements.getThanksReceived(), + achievements.getFeaturedImages(), + achievements.getQualityImages(), + uploadCount, // Update imagesUploaded with new value + achievements.getRevertCount() + ); + + hideProgressBar(updatedAchievements); } /** @@ -309,7 +319,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { }else { binding.imagesUploadedProgressbar.setVisibility(View.VISIBLE); binding.imagesUploadedProgressbar.setProgress - (100*uploadCount/levelInfo.getMaxUploadCount()); + (100*uploadCount/levelInfo.getMaxUploadCount()); binding.tvUploadedImages.setText (uploadCount + "/" + levelInfo.getMaxUploadCount()); } @@ -318,8 +328,8 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { private void setZeroAchievements() { String message = !Objects.equals(sessionManager.getUserName(), userName) ? - getString(R.string.no_achievements_yet, userName) : - getString(R.string.you_have_no_achievements_yet); + getString(R.string.no_achievements_yet, userName) : + getString(R.string.you_have_no_achievements_yet); DialogUtil.showAlertDialog(getActivity(), null, message, @@ -357,7 +367,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { // binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE); binding.thanksReceived.setText(String.valueOf(achievements.getThanksReceived())); binding.imagesUsedByWikiProgressBar.setProgress - (100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages()); + (100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages()); binding.tvWikiPb.setText(achievements.getUniqueUsedImages() + "/" + levelInfo.getMaxUniqueImages()); binding.imageFeatured.setText(String.valueOf(achievements.getFeaturedImages())); @@ -366,7 +376,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { levelUpInfoString += " " + levelInfo.getLevelNumber(); binding.achievementLevel.setText(levelUpInfoString); binding.achievementBadgeImage.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge, - new ContextThemeWrapper(getActivity(), levelInfo.getLevelStyle()).getTheme())); + new ContextThemeWrapper(getActivity(), levelInfo.getLevelStyle()).getTheme())); binding.achievementBadgeText.setText(Integer.toString(levelInfo.getLevelNumber())); BasicKvStore store = new BasicKvStore(this.getContext(), userName); store.putString("userAchievementsLevel", Integer.toString(levelInfo.getLevelNumber())); @@ -378,8 +388,8 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { private void hideProgressBar(Achievements achievements) { if (binding.progressBar != null) { levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(), - achievements.getUniqueUsedImages(), - achievements.getNotRevertPercentage()); + achievements.getUniqueUsedImages(), + achievements.getNotRevertPercentage()); inflateAchievements(achievements); setUploadProgress(achievements.getImagesUploaded()); setImageRevertPercentage(achievements.getNotRevertPercentage()); @@ -479,4 +489,4 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { } return true; } -} +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/FeaturedImages.kt b/app/src/main/java/fr/free/nrw/commons/profile/achievements/FeaturedImages.kt index 4784103fd..2a336d349 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/achievements/FeaturedImages.kt +++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/FeaturedImages.kt @@ -6,7 +6,7 @@ import com.google.gson.annotations.SerializedName * Represents Featured Images on WikiMedia Commons platform * Used by Achievements and FeedbackResponse (objects) of the user */ -class FeaturedImages( +data class FeaturedImages( @field:SerializedName("Quality_images") val qualityImages: Int, @field:SerializedName("Featured_pictures_on_Wikimedia_Commons") val featuredPicturesOnWikimediaCommons: Int, ) From fe347c21fdede145e6a945750aa43a6de7ec24eb Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Fri, 22 Nov 2024 19:28:16 +0530 Subject: [PATCH 35/74] Migrated recentlanguages and repository module from Java to Kotlin (#5948) * Rename .java to .kt * Migrated recentlanguages module to Kotlin * Rename .java to .kt * Migrated repository module to Kotlin --- .../RecentLanguagesContentProvider.java | 122 ----- .../RecentLanguagesContentProvider.kt | 142 ++++++ .../recentlanguages/RecentLanguagesDao.java | 204 --------- .../recentlanguages/RecentLanguagesDao.kt | 216 +++++++++ .../commons/repository/UploadRepository.java | 423 ------------------ .../commons/repository/UploadRepository.kt | 410 +++++++++++++++++ .../upload/categories/CategoriesPresenter.kt | 34 +- .../upload/depicts/DepictsPresenter.kt | 24 +- .../structure/depictions/DepictModel.kt | 2 +- .../commons/upload/CategoriesPresenterTest.kt | 14 +- .../commons/upload/DepictsPresenterTest.kt | 8 +- .../nrw/commons/upload/UploadPresenterTest.kt | 4 +- .../upload/UploadRepositoryUnitTest.kt | 20 +- 13 files changed, 825 insertions(+), 798 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.java create mode 100644 app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java create mode 100644 app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java create mode 100644 app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.java b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.java deleted file mode 100644 index de94c4b09..000000000 --- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.java +++ /dev/null @@ -1,122 +0,0 @@ -package fr.free.nrw.commons.recentlanguages; - -import static fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.COLUMN_NAME; -import static fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.TABLE_NAME; - -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteQueryBuilder; -import android.net.Uri; -import android.text.TextUtils; -import androidx.annotation.NonNull; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.data.DBOpenHelper; -import fr.free.nrw.commons.di.CommonsDaggerContentProvider; -import javax.inject.Inject; -import timber.log.Timber; - -/** - * Content provider of recently used languages - */ -public class RecentLanguagesContentProvider extends CommonsDaggerContentProvider { - - private static final String BASE_PATH = "recent_languages"; - public static final Uri BASE_URI = - Uri.parse("content://" + BuildConfig.RECENT_LANGUAGE_AUTHORITY + "/" + BASE_PATH); - - - /** - * Append language code to the base uri - * @param languageCode Code of a language - */ - public static Uri uriForCode(final String languageCode) { - return Uri.parse(BASE_URI + "/" + languageCode); - } - - @Inject - DBOpenHelper dbOpenHelper; - - @Override - public String getType(@NonNull final Uri uri) { - return null; - } - - /** - * Queries the SQLite database for the recently used languages - * @param uri : contains the uri for recently used languages - * @param projection : contains the all fields of the table - * @param selection : handles Where - * @param selectionArgs : the condition of Where clause - * @param sortOrder : ascending or descending - */ - @Override - public Cursor query(@NonNull final Uri uri, final String[] projection, final String selection, - final String[] selectionArgs, final String sortOrder) { - final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); - queryBuilder.setTables(TABLE_NAME); - final SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); - final Cursor cursor = queryBuilder.query(db, projection, selection, - selectionArgs, null, null, sortOrder); - cursor.setNotificationUri(getContext().getContentResolver(), uri); - return cursor; - } - - /** - * Handles the update query of local SQLite Database - * @param uri : contains the uri for recently used languages - * @param contentValues : new values to be entered to db - * @param selection : handles Where - * @param selectionArgs : the condition of Where clause - */ - @Override - public int update(@NonNull final Uri uri, final ContentValues contentValues, - final String selection, final String[] selectionArgs) { - final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - final int rowsUpdated; - if (TextUtils.isEmpty(selection)) { - final int id = Integer.parseInt(uri.getLastPathSegment()); - rowsUpdated = sqlDB.update(TABLE_NAME, - contentValues, - COLUMN_NAME + " = ?", - new String[]{String.valueOf(id)}); - } else { - throw new IllegalArgumentException( - "Parameter `selection` should be empty when updating an ID"); - } - - getContext().getContentResolver().notifyChange(uri, null); - return rowsUpdated; - } - - /** - * Handles the insertion of new recently used languages record to local SQLite Database - * @param uri : contains the uri for recently used languages - * @param contentValues : new values to be entered to db - */ - @Override - public Uri insert(@NonNull final Uri uri, final ContentValues contentValues) { - final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - final long id = sqlDB.insert(TABLE_NAME, null, contentValues); - getContext().getContentResolver().notifyChange(uri, null); - return Uri.parse(BASE_URI + "/" + id); - } - - /** - * Handles the deletion of new recently used languages record to local SQLite Database - * @param uri : contains the uri for recently used languages - */ - @Override - public int delete(@NonNull final Uri uri, final String s, final String[] strings) { - final int rows; - final SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); - Timber.d("Deleting recently used language %s", uri.getLastPathSegment()); - rows = db.delete( - TABLE_NAME, - "language_code = ?", - new String[]{uri.getLastPathSegment()} - ); - getContext().getContentResolver().notifyChange(uri, null); - return rows; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt new file mode 100644 index 000000000..facc4384f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt @@ -0,0 +1,142 @@ +package fr.free.nrw.commons.recentlanguages + + +import android.content.ContentValues +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteQueryBuilder +import android.net.Uri +import android.text.TextUtils +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.data.DBOpenHelper +import fr.free.nrw.commons.di.CommonsDaggerContentProvider +import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.COLUMN_NAME +import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.TABLE_NAME +import javax.inject.Inject +import timber.log.Timber + + +/** + * Content provider of recently used languages + */ +class RecentLanguagesContentProvider : CommonsDaggerContentProvider() { + + companion object { + private const val BASE_PATH = "recent_languages" + val BASE_URI: Uri = + Uri.parse( + "content://${BuildConfig.RECENT_LANGUAGE_AUTHORITY}/$BASE_PATH" + ) + + /** + * Append language code to the base URI + * @param languageCode Code of a language + */ + @JvmStatic + fun uriForCode(languageCode: String): Uri { + return Uri.parse("$BASE_URI/$languageCode") + } + } + + @Inject + lateinit var dbOpenHelper: DBOpenHelper + + override fun getType(uri: Uri): String? { + return null + } + + /** + * Queries the SQLite database for the recently used languages + * @param uri : contains the URI for recently used languages + * @param projection : contains all fields of the table + * @param selection : handles WHERE + * @param selectionArgs : the condition of WHERE clause + * @param sortOrder : ascending or descending + */ + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + val queryBuilder = SQLiteQueryBuilder() + queryBuilder.tables = TABLE_NAME + val db = dbOpenHelper.readableDatabase + val cursor = queryBuilder.query( + db, + projection, + selection, + selectionArgs, + null, + null, + sortOrder + ) + cursor.setNotificationUri(context?.contentResolver, uri) + return cursor + } + + /** + * Handles the update query of local SQLite Database + * @param uri : contains the URI for recently used languages + * @param contentValues : new values to be entered to the database + * @param selection : handles WHERE + * @param selectionArgs : the condition of WHERE clause + */ + override fun update( + uri: Uri, + contentValues: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + val sqlDB = dbOpenHelper.writableDatabase + val rowsUpdated: Int + if (selection.isNullOrEmpty()) { + val id = uri.lastPathSegment?.toInt() + ?: throw IllegalArgumentException("Invalid URI: $uri") + rowsUpdated = sqlDB.update( + TABLE_NAME, + contentValues, + "$COLUMN_NAME = ?", + arrayOf(id.toString()) + ) + } else { + throw IllegalArgumentException("Parameter `selection` should be empty when updating an ID") + } + + context?.contentResolver?.notifyChange(uri, null) + return rowsUpdated + } + + /** + * Handles the insertion of new recently used languages record to local SQLite Database + * @param uri : contains the URI for recently used languages + * @param contentValues : new values to be entered to the database + */ + override fun insert(uri: Uri, contentValues: ContentValues?): Uri? { + val sqlDB = dbOpenHelper.writableDatabase + val id = sqlDB.insert( + TABLE_NAME, + null, + contentValues + ) + context?.contentResolver?.notifyChange(uri, null) + return Uri.parse("$BASE_URI/$id") + } + + /** + * Handles the deletion of a recently used languages record from local SQLite Database + * @param uri : contains the URI for recently used languages + */ + override fun delete(uri: Uri, s: String?, strings: Array?): Int { + val db = dbOpenHelper.readableDatabase + Timber.d("Deleting recently used language %s", uri.lastPathSegment) + val rows = db.delete( + TABLE_NAME, + "language_code = ?", + arrayOf(uri.lastPathSegment) + ) + context?.contentResolver?.notifyChange(uri, null) + return rows + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java deleted file mode 100644 index cbb8c8a1c..000000000 --- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java +++ /dev/null @@ -1,204 +0,0 @@ -package fr.free.nrw.commons.recentlanguages; - -import android.annotation.SuppressLint; -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.os.RemoteException; -import androidx.annotation.NonNull; -import java.util.ArrayList; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Provider; -import javax.inject.Singleton; - -/** - * Handles database operations for recently used languages - */ -@Singleton -public class RecentLanguagesDao { - - private final Provider clientProvider; - - @Inject - public RecentLanguagesDao - (@Named("recent_languages") final Provider clientProvider) { - this.clientProvider = clientProvider; - } - - /** - * Find all persisted recently used languages on database - * @return list of recently used languages - */ - public List getRecentLanguages() { - final List languages = new ArrayList<>(); - final ContentProviderClient db = clientProvider.get(); - try (final Cursor cursor = db.query( - RecentLanguagesContentProvider.BASE_URI, - RecentLanguagesDao.Table.ALL_FIELDS, - null, - new String[]{}, - null)) { - if(cursor != null && cursor.moveToLast()) { - do { - languages.add(fromCursor(cursor)); - } while (cursor.moveToPrevious()); - } - } catch (final RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - return languages; - } - - /** - * Add a Language to database - * @param language : Language to add - */ - public void addRecentLanguage(final Language language) { - final ContentProviderClient db = clientProvider.get(); - try { - db.insert(RecentLanguagesContentProvider.BASE_URI, toContentValues(language)); - } catch (final RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - /** - * Delete a language from database - * @param languageCode : code of the Language to delete - */ - public void deleteRecentLanguage(final String languageCode) { - final ContentProviderClient db = clientProvider.get(); - try { - db.delete(RecentLanguagesContentProvider.uriForCode(languageCode), null, null); - } catch (final RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - /** - * Find a language from database based on its name - * @param languageCode : code of the Language to find - * @return boolean : is language in database ? - */ - public boolean findRecentLanguage(final String languageCode) { - if (languageCode == null) { //Avoiding NPE's - return false; - } - final ContentProviderClient db = clientProvider.get(); - try (final Cursor cursor = db.query( - RecentLanguagesContentProvider.BASE_URI, - RecentLanguagesDao.Table.ALL_FIELDS, - Table.COLUMN_CODE + "=?", - new String[]{languageCode}, - null - )) { - if (cursor != null && cursor.moveToFirst()) { - return true; - } - } catch (final RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - return false; - } - - /** - * It creates an Recent Language object from data stored in the SQLite DB by using cursor - * @param cursor cursor - * @return Language object - */ - @NonNull - @SuppressLint("Range") - Language fromCursor(final Cursor cursor) { - // Hardcoding column positions! - final String languageName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)); - final String languageCode = cursor.getString(cursor.getColumnIndex(Table.COLUMN_CODE)); - return new Language(languageName, languageCode); - } - - /** - * Takes data from Language and create a content value object - * @param recentLanguage recently used language - * @return ContentValues - */ - private ContentValues toContentValues(final Language recentLanguage) { - final ContentValues cv = new ContentValues(); - cv.put(Table.COLUMN_NAME, recentLanguage.getLanguageName()); - cv.put(Table.COLUMN_CODE, recentLanguage.getLanguageCode()); - return cv; - } - - /** - * This class contains the database table architecture for recently used languages, - * It also contains queries and logic necessary to the create, update, delete this table. - */ - public static final class Table { - public static final String TABLE_NAME = "recent_languages"; - static final String COLUMN_NAME = "language_name"; - static final String COLUMN_CODE = "language_code"; - - // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. - public static final String[] ALL_FIELDS = { - COLUMN_NAME, - COLUMN_CODE - }; - - static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; - - static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + COLUMN_NAME + " STRING," - + COLUMN_CODE + " STRING PRIMARY KEY" - + ");"; - - /** - * This method creates a LanguagesTable in SQLiteDatabase - * @param db SQLiteDatabase - */ - public static void onCreate(final SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - /** - * This method deletes LanguagesTable from SQLiteDatabase - * @param db SQLiteDatabase - */ - public static void onDelete(final SQLiteDatabase db) { - db.execSQL(DROP_TABLE_STATEMENT); - onCreate(db); - } - - /** - * This method is called on migrating from a older version to a newer version - * @param db SQLiteDatabase - * @param from Version from which we are migrating - * @param to Version to which we are migrating - */ - public static void onUpdate(final SQLiteDatabase db, int from, final int to) { - if (from == to) { - return; - } - if (from < 19) { - // doesn't exist yet - from++; - onUpdate(db, from, to); - return; - } - if (from == 19) { - // table added in version 20 - onCreate(db); - from++; - onUpdate(db, from, to); - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt new file mode 100644 index 000000000..e97c4f816 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt @@ -0,0 +1,216 @@ +package fr.free.nrw.commons.recentlanguages + +import android.annotation.SuppressLint +import android.content.ContentProviderClient +import android.content.ContentValues +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.os.RemoteException +import java.util.ArrayList +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Provider +import javax.inject.Singleton + + +/** + * Handles database operations for recently used languages + */ +@Singleton +class RecentLanguagesDao @Inject constructor( + @Named("recent_languages") + private val clientProvider: Provider +) { + + /** + * Find all persisted recently used languages on database + * @return list of recently used languages + */ + fun getRecentLanguages(): List { + val languages = mutableListOf() + val db = clientProvider.get() + try { + db.query( + RecentLanguagesContentProvider.BASE_URI, + Table.ALL_FIELDS, + null, + arrayOf(), + null + )?.use { cursor -> + if (cursor.moveToLast()) { + do { + languages.add(fromCursor(cursor)) + } while (cursor.moveToPrevious()) + } + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + return languages + } + + /** + * Add a Language to database + * @param language : Language to add + */ + fun addRecentLanguage(language: Language) { + val db = clientProvider.get() + try { + db.insert( + RecentLanguagesContentProvider.BASE_URI, + toContentValues(language) + ) + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + /** + * Delete a language from database + * @param languageCode : code of the Language to delete + */ + fun deleteRecentLanguage(languageCode: String) { + val db = clientProvider.get() + try { + db.delete( + RecentLanguagesContentProvider.uriForCode(languageCode), + null, + null + ) + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + /** + * Find a language from database based on its name + * @param languageCode : code of the Language to find + * @return boolean : is language in database ? + */ + fun findRecentLanguage(languageCode: String?): Boolean { + if (languageCode == null) { // Avoiding NPEs + return false + } + val db = clientProvider.get() + try { + db.query( + RecentLanguagesContentProvider.BASE_URI, + Table.ALL_FIELDS, + "${Table.COLUMN_CODE}=?", + arrayOf(languageCode), + null + )?.use { cursor -> + if (cursor.moveToFirst()) { + return true + } + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + return false + } + + /** + * It creates an Recent Language object from data stored in the SQLite DB by using cursor + * @param cursor cursor + * @return Language object + */ + @SuppressLint("Range") + fun fromCursor(cursor: Cursor): Language { + // Hardcoding column positions! + val languageName = cursor.getString( + cursor.getColumnIndex(Table.COLUMN_NAME) + ) + val languageCode = cursor.getString( + cursor.getColumnIndex(Table.COLUMN_CODE) + ) + return Language(languageName, languageCode) + } + + /** + * Takes data from Language and create a content value object + * @param recentLanguage recently used language + * @return ContentValues + */ + private fun toContentValues(recentLanguage: Language): ContentValues { + return ContentValues().apply { + put(Table.COLUMN_NAME, recentLanguage.languageName) + put(Table.COLUMN_CODE, recentLanguage.languageCode) + } + } + + /** + * This class contains the database table architecture for recently used languages, + * It also contains queries and logic necessary to the create, update, delete this table. + */ + object Table { + const val TABLE_NAME = "recent_languages" + const val COLUMN_NAME = "language_name" + const val COLUMN_CODE = "language_code" + + // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. + @JvmStatic + val ALL_FIELDS = arrayOf( + COLUMN_NAME, + COLUMN_CODE + ) + + private const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME" + + private const val CREATE_TABLE_STATEMENT = "CREATE TABLE $TABLE_NAME (" + + "$COLUMN_NAME STRING," + + "$COLUMN_CODE STRING PRIMARY KEY" + + ");" + + /** + * This method creates a LanguagesTable in SQLiteDatabase + * @param db SQLiteDatabase + */ + @SuppressLint("SQLiteString") + @JvmStatic + fun onCreate(db: SQLiteDatabase) { + db.execSQL(CREATE_TABLE_STATEMENT) + } + + /** + * This method deletes LanguagesTable from SQLiteDatabase + * @param db SQLiteDatabase + */ + @JvmStatic + fun onDelete(db: SQLiteDatabase) { + db.execSQL(DROP_TABLE_STATEMENT) + onCreate(db) + } + + /** + * This method is called on migrating from a older version to a newer version + * @param db SQLiteDatabase + * @param from Version from which we are migrating + * @param to Version to which we are migrating + */ + @JvmStatic + fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) { + if (from == to) { + return + } + if (from < 19) { + // doesn't exist yet + onUpdate(db, from + 1, to) + return + } + if (from == 19) { + // table added in version 20 + onCreate(db) + onUpdate(db, from + 1, to) + } + } + } +} 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 deleted file mode 100644 index de0154947..000000000 --- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java +++ /dev/null @@ -1,423 +0,0 @@ -package fr.free.nrw.commons.repository; - -import androidx.annotation.Nullable; -import fr.free.nrw.commons.Media; -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.ContributionDao; -import fr.free.nrw.commons.filepicker.UploadableFile; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.nearby.NearbyPlaces; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.upload.ImageCoordinates; -import fr.free.nrw.commons.upload.SimilarImageInterface; -import fr.free.nrw.commons.upload.UploadController; -import fr.free.nrw.commons.upload.UploadItem; -import fr.free.nrw.commons.upload.UploadModel; -import fr.free.nrw.commons.upload.structure.depictions.DepictModel; -import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; -import io.reactivex.Flowable; -import io.reactivex.Observable; -import io.reactivex.Single; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import javax.inject.Inject; -import javax.inject.Singleton; -import timber.log.Timber; - -/** - * The repository class for UploadActivity - */ -@Singleton -public class UploadRepository { - - private final UploadModel uploadModel; - private final UploadController uploadController; - private final CategoriesModel categoriesModel; - private final NearbyPlaces nearbyPlaces; - private final DepictModel depictModel; - - private static final double NEARBY_RADIUS_IN_KILO_METERS = 0.1; //100 meters - private final ContributionDao contributionDao; - - @Inject - public UploadRepository(UploadModel uploadModel, - UploadController uploadController, - CategoriesModel categoriesModel, - NearbyPlaces nearbyPlaces, - DepictModel depictModel, - ContributionDao contributionDao) { - this.uploadModel = uploadModel; - this.uploadController = uploadController; - this.categoriesModel = categoriesModel; - this.nearbyPlaces = nearbyPlaces; - this.depictModel = depictModel; - this.contributionDao=contributionDao; - } - - /** - * asks the RemoteDataSource to build contributions - * - * @return - */ - public Observable buildContributions() { - return uploadModel.buildContributions(); - } - - /** - * asks the RemoteDataSource to start upload for the contribution - * - * @param contribution - */ - - public void prepareMedia(Contribution contribution) { - uploadController.prepareMedia(contribution); - } - - - public void saveContribution(Contribution contribution) { - contributionDao.save(contribution).blockingAwait(); - } - - /** - * Fetches and returns all the Upload Items - * - * @return - */ - public List getUploads() { - return uploadModel.getUploads(); - } - - /** - *Prepare for a fresh upload - */ - public void cleanup() { - uploadModel.cleanUp(); - //This needs further refactoring, this should not be here, right now the structure wont suppoort rhis - categoriesModel.cleanUp(); - depictModel.cleanUp(); - } - - /** - * Fetches and returns the selected categories for the current upload - * - * @return - */ - public List getSelectedCategories() { - return categoriesModel.getSelectedCategories(); - } - - /** - * all categories from MWApi - * - * @param query - * @param imageTitleList - * @param selectedDepictions - * @return - */ - public Observable> searchAll(String query, List imageTitleList, - List selectedDepictions) { - return categoriesModel.searchAll(query, imageTitleList, selectedDepictions); - } - - /** - * sets the list of selected categories for the current upload - * - * @param categoryStringList - */ - public void setSelectedCategories(List categoryStringList) { - uploadModel.setSelectedCategories(categoryStringList); - } - - /** - * handles the category selection/deselection - * - * @param categoryItem - */ - public void onCategoryClicked(CategoryItem categoryItem, final Media media) { - categoriesModel.onCategoryItemClicked(categoryItem, media); - } - - /** - * prunes the category list for irrelevant categories see #750 - * - * @param name - * @return - */ - public boolean isSpammyCategory(String name) { - return categoriesModel.isSpammyCategory(name); - } - - /** - * retursn the string list of available license from the LocalDataSource - * - * @return - */ - public List getLicenses() { - return uploadModel.getLicenses(); - } - - /** - * returns the selected license for the current upload - * - * @return - */ - public String getSelectedLicense() { - return uploadModel.getSelectedLicense(); - } - - /** - * returns the number of Upload Items - * - * @return - */ - public int getCount() { - return uploadModel.getCount(); - } - - /** - * ask the RemoteDataSource to pre process the image - * - * @param uploadableFile - * @param place - * @param similarImageInterface - * @return - */ - public Observable preProcessImage(UploadableFile uploadableFile, Place place, - SimilarImageInterface similarImageInterface, LatLng inAppPictureLocation) { - return uploadModel.preProcessImage(uploadableFile, place, - similarImageInterface, inAppPictureLocation); - } - - /** - * query the RemoteDataSource for image quality - * - * @param uploadItem UploadItem whose caption is to be checked - * @return Quality of UploadItem - */ - public Single getImageQuality(UploadItem uploadItem, LatLng location) { - return uploadModel.getImageQuality(uploadItem, location); - } - - /** - * query the RemoteDataSource for image duplicity check - * - * @param filePath file to be checked - * @return IMAGE_DUPLICATE or IMAGE_OK - */ - public Single checkDuplicateImage(String filePath) { - return uploadModel.checkDuplicateImage(filePath); - } - - /** - * query the RemoteDataSource for caption quality - * - * @param uploadItem UploadItem whose caption is to be checked - * @return Quality of caption of the UploadItem - */ - public Single getCaptionQuality(UploadItem uploadItem) { - return uploadModel.getCaptionQuality(uploadItem); - } - - /** - * asks the LocalDataSource to delete the file with the given file path - * - * @param filePath - */ - public void deletePicture(String filePath) { - uploadModel.deletePicture(filePath); - } - - /** - * fetches and returns the upload item - * - * @param index - * @return - */ - public UploadItem getUploadItem(int index) { - if (index >= 0) { - return uploadModel.getItems().get(index); - } - return null; //There is no item to copy details - } - - /** - * set selected license for the current upload - * - * @param licenseName - */ - public void setSelectedLicense(String licenseName) { - uploadModel.setSelectedLicense(licenseName); - } - - public void onDepictItemClicked(DepictedItem depictedItem, final Media media) { - uploadModel.onDepictItemClicked(depictedItem, media); - } - - /** - * Fetches and returns the selected depictions for the current upload - * - * @return - */ - - public List getSelectedDepictions() { - return uploadModel.getSelectedDepictions(); - } - - /** - * Provides selected existing depicts - * - * @return selected existing depicts - */ - public List getSelectedExistingDepictions() { - return uploadModel.getSelectedExistingDepictions(); - } - - /** - * Initialize existing depicts - * - * @param selectedExistingDepictions existing depicts - */ - public void setSelectedExistingDepictions(final List selectedExistingDepictions) { - uploadModel.setSelectedExistingDepictions(selectedExistingDepictions); - } - /** - * Search all depictions from - * - * @param query - * @return - */ - - public Flowable> searchAllEntities(String query) { - return depictModel.searchAllEntities(query, this); - } - - /** - * Gets the depiction for each unique {@link Place} associated with an {@link UploadItem} - * from {@link #getUploads()} - * - * @return a single that provides the depictions - */ - public Single> getPlaceDepictions() { - final Set qids = new HashSet<>(); - for (final UploadItem item : getUploads()) { - final Place place = item.getPlace(); - if (place != null) { - qids.add(place.getWikiDataEntityId()); - } - } - return depictModel.getPlaceDepictions(new ArrayList<>(qids)); - } - - /** - * Gets the category for each unique {@link Place} associated with an {@link UploadItem} - * from {@link #getUploads()} - * - * @return a single that provides the categories - */ - public Single> getPlaceCategories() { - final Set qids = new HashSet<>(); - for (final UploadItem item : getUploads()) { - final Place place = item.getPlace(); - if (place != null) { - qids.add(place.getCategory()); - } - } - return Single.fromObservable(categoriesModel.getCategoriesByName(new ArrayList<>(qids))); - } - - /** - * Takes depict IDs as a parameter, converts into a slash separated String and Gets DepictItem - * from the server - * - * @param depictionsQIDs IDs of Depiction - * @return Flowable> - */ - public Flowable> getDepictions(final List depictionsQIDs){ - final String ids = joinQIDs(depictionsQIDs); - return depictModel.getDepictions(ids).toFlowable(); - } - - /** - * Builds a string by joining all IDs divided by "|" - * - * @param depictionsQIDs IDs of depiction ex. ["Q11023","Q1356"] - * @return string ex. "Q11023|Q1356" - */ - private String joinQIDs(final List depictionsQIDs) { - if (depictionsQIDs != null && !depictionsQIDs.isEmpty()) { - final StringBuilder buffer = new StringBuilder(depictionsQIDs.get(0)); - - if (depictionsQIDs.size() > 1) { - for (int i = 1; i < depictionsQIDs.size(); i++) { - buffer.append("|"); - buffer.append(depictionsQIDs.get(i)); - } - } - return buffer.toString(); - } - return null; - } - - /** - * Returns nearest place matching the passed latitude and longitude - * - * @param decLatitude - * @param decLongitude - * @return - */ - @Nullable - public Place checkNearbyPlaces(final double decLatitude, final double decLongitude) { - try { - final List fromWikidataQuery = nearbyPlaces.getFromWikidataQuery(new LatLng( - decLatitude, decLongitude, 0.0f), - Locale.getDefault().getLanguage(), - NEARBY_RADIUS_IN_KILO_METERS, null); - return (fromWikidataQuery != null && fromWikidataQuery.size() > 0) ? fromWikidataQuery - .get(0) : null; - } catch (final Exception e) { - Timber.e("Error fetching nearby places: %s", e.getMessage()); - return null; - } - } - - public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) { - uploadModel.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex); - } - - public boolean isWMLSupportedForThisPlace() { - return uploadModel.getItems().get(0).isWLMUpload(); - } - - /** - * Provides selected existing categories - * - * @return selected existing categories - */ - public List getSelectedExistingCategories() { - return categoriesModel.getSelectedExistingCategories(); - } - - /** - * Initialize existing categories - * - * @param selectedExistingCategories existing categories - */ - public void setSelectedExistingCategories(final List selectedExistingCategories) { - categoriesModel.setSelectedExistingCategories(selectedExistingCategories); - } - - /** - * Takes category names and Gets CategoryItem from the server - * - * @param categories names of Category - * @return Observable> - */ - public Observable> getCategories(final List categories){ - return categoriesModel.getCategoriesByName(categories); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt new file mode 100644 index 000000000..0500f4946 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt @@ -0,0 +1,410 @@ +package fr.free.nrw.commons.repository + +import fr.free.nrw.commons.Media +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.ContributionDao +import fr.free.nrw.commons.filepicker.UploadableFile +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.NearbyPlaces +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.upload.ImageCoordinates +import fr.free.nrw.commons.upload.SimilarImageInterface +import fr.free.nrw.commons.upload.UploadController +import fr.free.nrw.commons.upload.UploadItem +import fr.free.nrw.commons.upload.UploadModel +import fr.free.nrw.commons.upload.structure.depictions.DepictModel +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem +import io.reactivex.Flowable +import io.reactivex.Observable +import io.reactivex.Single +import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton +import timber.log.Timber + +/** + * The repository class for UploadActivity + */ +@Singleton +class UploadRepository @Inject constructor( + private val uploadModel: UploadModel, + private val uploadController: UploadController, + private val categoriesModel: CategoriesModel, + private val nearbyPlaces: NearbyPlaces, + private val depictModel: DepictModel, + private val contributionDao: ContributionDao +) { + + companion object { + private const val NEARBY_RADIUS_IN_KILO_METERS = 0.1 // 100 meters + } + + /** + * Asks the RemoteDataSource to build contributions + * + * @return + */ + fun buildContributions(): Observable { + return uploadModel.buildContributions() + } + + /** + * Asks the RemoteDataSource to start upload for the contribution + * + * @param contribution + */ + fun prepareMedia(contribution: Contribution) { + uploadController.prepareMedia(contribution) + } + + fun saveContribution(contribution: Contribution) { + contributionDao.save(contribution).blockingAwait() + } + + /** + * Fetches and returns all the Upload Items + * + * @return + */ + fun getUploads(): List { + return uploadModel.getUploads() + } + + /** + * Prepare for a fresh upload + */ + fun cleanup() { + uploadModel.cleanUp() + // This needs further refactoring, this should not be here, right now the structure + // won't support this + categoriesModel.cleanUp() + depictModel.cleanUp() + } + + /** + * Fetches and returns the selected categories for the current upload + * + * @return + */ + fun getSelectedCategories(): List { + return categoriesModel.getSelectedCategories() + } + + /** + * All categories from MWApi + * + * @param query + * @param imageTitleList + * @param selectedDepictions + * @return + */ + fun searchAll( + query: String, + imageTitleList: List, + selectedDepictions: List + ): Observable> { + return categoriesModel.searchAll(query, imageTitleList, selectedDepictions) + } + + /** + * Sets the list of selected categories for the current upload + * + * @param categoryStringList + */ + fun setSelectedCategories(categoryStringList: List) { + uploadModel.setSelectedCategories(categoryStringList) + } + + /** + * Handles the category selection/deselection + * + * @param categoryItem + */ + fun onCategoryClicked(categoryItem: CategoryItem, media: Media?) { + categoriesModel.onCategoryItemClicked(categoryItem, media) + } + + /** + * Prunes the category list for irrelevant categories see #750 + * + * @param name + * @return + */ + fun isSpammyCategory(name: String): Boolean { + return categoriesModel.isSpammyCategory(name) + } + + /** + * Returns the string list of available licenses from the LocalDataSource + * + * @return + */ + fun getLicenses(): List { + return uploadModel.licenses + } + + /** + * Returns the selected license for the current upload + * + * @return + */ + fun getSelectedLicense(): String { + return uploadModel.selectedLicense + } + + /** + * Returns the number of Upload Items + * + * @return + */ + fun getCount(): Int { + return uploadModel.count + } + + /** + * Ask the RemoteDataSource to preprocess the image + * + * @param uploadableFile + * @param place + * @param similarImageInterface + * @param inAppPictureLocation + * @return + */ + fun preProcessImage( + uploadableFile: UploadableFile, + place: Place?, + similarImageInterface: SimilarImageInterface, + inAppPictureLocation: LatLng? + ): Observable { + return uploadModel.preProcessImage( + uploadableFile, + place, + similarImageInterface, + inAppPictureLocation + ) + } + + /** + * Query the RemoteDataSource for image quality + * + * @param uploadItem UploadItem whose caption is to be checked + * @param location Location of the image + * @return Quality of UploadItem + */ + fun getImageQuality(uploadItem: UploadItem, location: LatLng?): Single { + return uploadModel.getImageQuality(uploadItem, location) + } + + /** + * Query the RemoteDataSource for image duplicity check + * + * @param filePath file to be checked + * @return IMAGE_DUPLICATE or IMAGE_OK + */ + fun checkDuplicateImage(filePath: String): Single { + return uploadModel.checkDuplicateImage(filePath) + } + + /** + * query the RemoteDataSource for caption quality + * + * @param uploadItem UploadItem whose caption is to be checked + * @return Quality of caption of the UploadItem + */ + fun getCaptionQuality(uploadItem: UploadItem): Single { + return uploadModel.getCaptionQuality(uploadItem) + } + + /** + * asks the LocalDataSource to delete the file with the given file path + * + * @param filePath + */ + fun deletePicture(filePath: String) { + uploadModel.deletePicture(filePath) + } + + /** + * fetches and returns the upload item + * + * @param index + * @return + */ + fun getUploadItem(index: Int): UploadItem? { + return if (index >= 0) { + uploadModel.items.getOrNull(index) + } else null //There is no item to copy details + } + + /** + * set selected license for the current upload + * + * @param licenseName + */ + fun setSelectedLicense(licenseName: String) { + uploadModel.selectedLicense = licenseName + } + + fun onDepictItemClicked(depictedItem: DepictedItem, media: Media?) { + uploadModel.onDepictItemClicked(depictedItem, media) + } + + /** + * Fetches and returns the selected depictions for the current upload + * + * @return + */ + fun getSelectedDepictions(): List { + return uploadModel.selectedDepictions + } + + /** + * Provides selected existing depicts + * + * @return selected existing depicts + */ + fun getSelectedExistingDepictions(): List { + return uploadModel.selectedExistingDepictions + } + + /** + * Initialize existing depicts + * + * @param selectedExistingDepictions existing depicts + */ + fun setSelectedExistingDepictions(selectedExistingDepictions: List) { + uploadModel.selectedExistingDepictions = selectedExistingDepictions + } + + /** + * Search all depictions from + * + * @param query + * @return + */ + fun searchAllEntities(query: String): Flowable> { + return depictModel.searchAllEntities(query, this) + } + + /** + * Gets the depiction for each unique {@link Place} associated with an {@link UploadItem} + * from {@link #getUploads()} + * + * @return a single that provides the depictions + */ + fun getPlaceDepictions(): Single> { + val qids = mutableSetOf() + getUploads().forEach { item -> + item.place?.let { + it.wikiDataEntityId?.let { it1 -> + qids.add(it1) + } + } + } + return depictModel.getPlaceDepictions(qids.toList()) + } + + /** + * Gets the category for each unique {@link Place} associated with an {@link UploadItem} + * from {@link #getUploads()} + * + * @return a single that provides the categories + */ + fun getPlaceCategories(): Single> { + val qids = mutableSetOf() + getUploads().forEach { item -> + item.place?.category?.let { qids.add(it) } + } + return Single.fromObservable(categoriesModel.getCategoriesByName(qids.toList())) + } + + /** + * Takes depict IDs as a parameter, converts into a slash separated String and Gets DepictItem + * from the server + * + * @param depictionsQIDs IDs of Depiction + * @return Flowable> + */ + fun getDepictions(depictionsQIDs: List): Flowable> { + val ids = joinQIDs(depictionsQIDs) ?: "" + return depictModel.getDepictions(ids).toFlowable() + } + + /** + * Builds a string by joining all IDs divided by "|" + * + * @param depictionsQIDs IDs of depiction ex. ["Q11023","Q1356"] + * @return string ex. "Q11023|Q1356" + */ + private fun joinQIDs(depictionsQIDs: List?): String? { + return depictionsQIDs?.takeIf { + it.isNotEmpty() + }?.joinToString("|") + } + + /** + * Returns nearest place matching the passed latitude and longitude + * + * @param decLatitude + * @param decLongitude + * @return + */ + fun checkNearbyPlaces(decLatitude: Double, decLongitude: Double): Place? { + return try { + val fromWikidataQuery = nearbyPlaces.getFromWikidataQuery( + LatLng(decLatitude, decLongitude, 0.0f), + Locale.getDefault().language, + NEARBY_RADIUS_IN_KILO_METERS, + null + ) + fromWikidataQuery?.firstOrNull() + } catch (e: Exception) { + Timber.e("Error fetching nearby places: %s", e.message) + null + } + } + + fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates, uploadItemIndex: Int) { + uploadModel.useSimilarPictureCoordinates( + imageCoordinates, + uploadItemIndex + ) + } + + fun isWMLSupportedForThisPlace(): Boolean { + return uploadModel.items.firstOrNull()?.isWLMUpload == true + } + + /** + * Provides selected existing categories + * + * @return selected existing categories + */ + fun getSelectedExistingCategories(): List { + return categoriesModel.getSelectedExistingCategories() + } + + /** + * Initialize existing categories + * + * @param selectedExistingCategories existing categories + */ + fun setSelectedExistingCategories(selectedExistingCategories: List) { + categoriesModel.setSelectedExistingCategories( + selectedExistingCategories.toMutableList() + ) + } + + /** + * Takes category names and Gets CategoryItem from the server + * + * @param categories names of Category + * @return Observable> + */ + fun getCategories(categories: List): Observable> { + return categoriesModel.getCategoriesByName(categories) + ?.map { it.toList() } ?: Observable.empty() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt index 1822df830..712f6fc3e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt @@ -61,7 +61,7 @@ class CategoriesPresenter .doOnNext { view.showProgress(true) }.switchMap(::searchResults) - .map { repository.selectedCategories + it } + .map { repository.getSelectedCategories() + it } .map { it.distinctBy { categoryItem -> categoryItem.name } } .observeOn(mainThreadScheduler) .subscribe( @@ -89,7 +89,7 @@ class CategoriesPresenter private fun searchResults(term: String): Observable>? { if (media == null) { return repository - .searchAll(term, getImageTitleList(), repository.selectedDepictions) + .searchAll(term, getImageTitleList(), repository.getSelectedDepictions()) .subscribeOn(ioScheduler) .map { it.filter { categoryItem -> @@ -101,13 +101,13 @@ class CategoriesPresenter return Observable .zip( repository - .getCategories(repository.selectedExistingCategories) + .getCategories(repository.getSelectedExistingCategories()) .map { list -> list.map { CategoryItem(it.name, it.description, it.thumbnail, true) } }, - repository.searchAll(term, getImageTitleList(), repository.selectedDepictions), + repository.searchAll(term, getImageTitleList(), repository.getSelectedDepictions()), ) { it1, it2 -> it1 + it2 }.subscribeOn(ioScheduler) @@ -138,7 +138,7 @@ class CategoriesPresenter * @return */ private fun getImageTitleList(): List = - repository.uploads + repository.getUploads() .map { it.uploadMediaDetails[0].captionText } .filterNot { TextUtils.isEmpty(it) } @@ -146,7 +146,7 @@ class CategoriesPresenter * Verifies the number of categories selected, prompts the user if none selected */ override fun verifyCategories() { - val selectedCategories = repository.selectedCategories + val selectedCategories = repository.getSelectedCategories() if (selectedCategories.isNotEmpty()) { repository.setSelectedCategories(selectedCategories.map { it.name }) view.goToNextScreen() @@ -173,14 +173,14 @@ class CategoriesPresenter ) { this.view = view this.media = media - repository.selectedExistingCategories = view.existingCategories + repository.setSelectedExistingCategories(view.existingCategories) compositeDisposable.add( searchTerms .observeOn(mainThreadScheduler) .doOnNext { view.showProgress(true) }.switchMap(::searchResults) - .map { repository.selectedCategories + it } + .map { repository.getSelectedCategories() + it } .map { it.distinctBy { categoryItem -> categoryItem.name } } .observeOn(mainThreadScheduler) .subscribe( @@ -218,13 +218,21 @@ class CategoriesPresenter wikiText: String, ) { // check if view.existingCategories is null - if (repository.selectedCategories.isNotEmpty() || - (view.existingCategories != null && repository.selectedExistingCategories.size != view.existingCategories.size) + if ( + repository.getSelectedCategories().isNotEmpty() + || + ( + view.existingCategories != null + && + repository.getSelectedExistingCategories().size + != + view.existingCategories.size + ) ) { val selectedCategories: MutableList = ( - repository.selectedCategories.map { it.name }.toMutableList() + - repository.selectedExistingCategories + repository.getSelectedCategories().map { it.name }.toMutableList() + + repository.getSelectedExistingCategories() ).toMutableList() if (selectedCategories.isNotEmpty()) { @@ -305,7 +313,7 @@ class CategoriesPresenter override fun selectCategories() { compositeDisposable.add( - repository.placeCategories + repository.getPlaceCategories() .subscribeOn(ioScheduler) .observeOn(mainThreadScheduler) .subscribe(::selectNewCategories), diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt index 3beedd9d5..fa3eb354e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt @@ -93,14 +93,14 @@ class DepictsPresenter return repository .searchAllEntities(querystring) .subscribeOn(ioScheduler) - .map { repository.selectedDepictions + it + recentDepictedItemList + controller.loadFavoritesItems() } + .map { repository.getSelectedDepictions() + it + recentDepictedItemList + controller.loadFavoritesItems() } .map { it.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } } .map { it.distinctBy(DepictedItem::id) } } else { return Flowable .zip( repository - .getDepictions(repository.selectedExistingDepictions) + .getDepictions(repository.getSelectedExistingDepictions()) .map { list -> list.map { DepictedItem( @@ -118,7 +118,7 @@ class DepictsPresenter ) { it1, it2 -> it1 + it2 }.subscribeOn(ioScheduler) - .map { repository.selectedDepictions + it + recentDepictedItemList + controller.loadFavoritesItems() } + .map { repository.getSelectedDepictions() + it + recentDepictedItemList + controller.loadFavoritesItems() } .map { it.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } } .map { it.distinctBy(DepictedItem::id) } } @@ -135,7 +135,7 @@ class DepictsPresenter */ override fun selectPlaceDepictions() { compositeDisposable.add( - repository.placeDepictions + repository.getPlaceDepictions() .subscribeOn(ioScheduler) .observeOn(mainThreadScheduler) .subscribe(::selectNewDepictions), @@ -188,10 +188,10 @@ class DepictsPresenter * from the depiction list */ override fun verifyDepictions() { - if (repository.selectedDepictions.isNotEmpty()) { + if (repository.getSelectedDepictions().isNotEmpty()) { if (::depictsDao.isInitialized) { // save all the selected Depicted item in room Database - depictsDao.savingDepictsInRoomDataBase(repository.selectedDepictions) + depictsDao.savingDepictsInRoomDataBase(repository.getSelectedDepictions()) } view.goToNextScreen() } else { @@ -205,20 +205,20 @@ class DepictsPresenter */ @SuppressLint("CheckResult") override fun updateDepictions(media: Media) { - if (repository.selectedDepictions.isNotEmpty() || - repository.selectedExistingDepictions.size != view.existingDepictions.size + if (repository.getSelectedDepictions().isNotEmpty() || + repository.getSelectedExistingDepictions().size != view.existingDepictions.size ) { view.showProgressDialog() val selectedDepictions: MutableList = ( - repository.selectedDepictions.map { it.id }.toMutableList() + - repository.selectedExistingDepictions + repository.getSelectedDepictions().map { it.id }.toMutableList() + + repository.getSelectedExistingDepictions() ).toMutableList() if (selectedDepictions.isNotEmpty()) { if (::depictsDao.isInitialized) { // save all the selected Depicted item in room Database - depictsDao.savingDepictsInRoomDataBase(repository.selectedDepictions) + depictsDao.savingDepictsInRoomDataBase(repository.getSelectedDepictions()) } compositeDisposable.add( @@ -254,7 +254,7 @@ class DepictsPresenter ) { this.view = view this.media = media - repository.selectedExistingDepictions = view.existingDepictions + repository.setSelectedExistingDepictions(view.existingDepictions) compositeDisposable.add( searchTerm .observeOn(mainThreadScheduler) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt index 7242b8eed..9337cb8b5 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt @@ -39,7 +39,7 @@ class DepictModel for (place in places) { place.wikiDataEntityId?.let { qids.add(it) } } - repository.uploads.forEach { item -> + repository.getUploads().forEach { item -> if (item.gpsCoords != null && item.gpsCoords.imageCoordsExists) { Coordinates2Country .countryQID( diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt index 4b321071f..bb8fd1fc5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt @@ -56,9 +56,9 @@ class CategoriesPresenterTest { @Throws(Exception::class) fun `Test onAttachViewWithMedia when media is not null`() { categoriesPresenter.onAttachViewWithMedia(view, media()) - whenever(repository.getCategories(repository.selectedExistingCategories)) + whenever(repository.getCategories(repository.getSelectedExistingCategories())) .thenReturn(Observable.just(mutableListOf(categoryItem()))) - whenever(repository.searchAll("mock", emptyList(), repository.selectedDepictions)) + whenever(repository.searchAll("mock", emptyList(), repository.getSelectedDepictions())) .thenReturn(Observable.just(mutableListOf(categoryItem()))) val method: Method = CategoriesPresenter::class.java.getDeclaredMethod( @@ -88,7 +88,7 @@ class CategoriesPresenterTest { val emptyCaptionUploadItem = mock() whenever(emptyCaptionUploadItem.uploadMediaDetails) .thenReturn(listOf(UploadMediaDetail(captionText = ""))) - whenever(repository.uploads).thenReturn( + whenever(repository.getUploads()).thenReturn( listOf( nonEmptyCaptionUploadItem, emptyCaptionUploadItem, @@ -105,7 +105,7 @@ class CategoriesPresenterTest { ) whenever(repository.isSpammyCategory("selected")).thenReturn(false) whenever(repository.isSpammyCategory("doesContainYear")).thenReturn(true) - whenever(repository.selectedCategories).thenReturn( + whenever(repository.getSelectedCategories()).thenReturn( listOf( categoryItem("selected", "", "", true), ), @@ -130,7 +130,7 @@ class CategoriesPresenterTest { whenever(repository.searchAll(any(), any(), any())) .thenReturn(Observable.just(emptyCategories)) - whenever(repository.selectedCategories).thenReturn(listOf()) + whenever(repository.getSelectedCategories()).thenReturn(listOf()) categoriesPresenter.searchForCategories(query) testScheduler.triggerActions() val method: Method = @@ -154,7 +154,7 @@ class CategoriesPresenterTest { fun `verifyCategories with non empty selection goes to next screen`() { categoriesPresenter.onAttachView(view) val item = categoryItem() - whenever(repository.selectedCategories).thenReturn(listOf(item)) + whenever(repository.getSelectedCategories()).thenReturn(listOf(item)) categoriesPresenter.verifyCategories() verify(repository).setSelectedCategories(listOf(item.name)) verify(view).goToNextScreen() @@ -163,7 +163,7 @@ class CategoriesPresenterTest { @Test fun `verifyCategories with empty selection show no category selected`() { categoriesPresenter.onAttachView(view) - whenever(repository.selectedCategories).thenReturn(listOf()) + whenever(repository.getSelectedCategories()).thenReturn(listOf()) categoriesPresenter.verifyCategories() verify(view).showNoCategorySelected() } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/DepictsPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/DepictsPresenterTest.kt index 748b95ea6..1abff908e 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/DepictsPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/DepictsPresenterTest.kt @@ -74,7 +74,7 @@ class DepictsPresenterTest { ) whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(searchResults)) val selectedItem = depictedItem(id = "selected") - whenever(repository.selectedDepictions).thenReturn(listOf(selectedItem)) + whenever(repository.getSelectedDepictions()).thenReturn(listOf(selectedItem)) depictsPresenter.searchForDepictions("") testScheduler.triggerActions() verify(view).showProgress(false) @@ -123,14 +123,14 @@ class DepictsPresenterTest { @Test fun `verifyDepictions with non empty selectedDepictions goes to next screen`() { - whenever(repository.selectedDepictions).thenReturn(listOf(depictedItem())) + whenever(repository.getSelectedDepictions()).thenReturn(listOf(depictedItem())) depictsPresenter.verifyDepictions() verify(view).goToNextScreen() } @Test fun `verifyDepictions with empty selectedDepictions goes to noDepictionSelected`() { - whenever(repository.selectedDepictions).thenReturn(emptyList()) + whenever(repository.getSelectedDepictions()).thenReturn(emptyList()) depictsPresenter.verifyDepictions() verify(view).noDepictionSelected() } @@ -162,7 +162,7 @@ class DepictsPresenterTest { @Test fun `Test searchResults when media is not null`() { Whitebox.setInternalState(depictsPresenter, "media", media) - whenever(repository.getDepictions(repository.selectedExistingDepictions)) + whenever(repository.getDepictions(repository.getSelectedExistingDepictions())) .thenReturn(Flowable.just(listOf(depictedItem()))) whenever(repository.searchAllEntities("querystring")) .thenReturn(Flowable.just(listOf(depictedItem()))) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt index bbaaaec1c..29a35c1e5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt @@ -84,7 +84,7 @@ class UploadPresenterTest { fun handleSubmitImagesNoLocationWithConsecutiveNoLocationUploads() { `when`(imageCoords.imageCoordsExists).thenReturn(false) `when`(uploadItem.getGpsCoords()).thenReturn(imageCoords) - `when`(repository.uploads).thenReturn(uploadableItems) + `when`(repository.getUploads()).thenReturn(uploadableItems) uploadableItems.add(uploadItem) // test 1 - insufficient count @@ -112,7 +112,7 @@ class UploadPresenterTest { ).thenReturn(UploadPresenter.CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD) `when`(imageCoords.imageCoordsExists).thenReturn(true) `when`(uploadItem.getGpsCoords()).thenReturn(imageCoords) - `when`(repository.uploads).thenReturn(uploadableItems) + `when`(repository.getUploads()).thenReturn(uploadableItems) uploadableItems.add(uploadItem) uploadPresenter.handleSubmit() // no alert dialog expected diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt index 54d35494a..233b0de32 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt @@ -117,7 +117,7 @@ class UploadRepositoryUnitTest { @Test fun testGetUploads() { - assertEquals(repository.uploads, uploadModel.uploads) + assertEquals(repository.getUploads(), uploadModel.uploads) } @Test @@ -130,7 +130,7 @@ class UploadRepositoryUnitTest { @Test fun testGetSelectedCategories() { - assertEquals(repository.selectedCategories, categoriesModel.getSelectedCategories()) + assertEquals(repository.getSelectedCategories(), categoriesModel.getSelectedCategories()) } @Test @@ -163,17 +163,17 @@ class UploadRepositoryUnitTest { @Test fun testGetLicenses() { - assertEquals(repository.licenses, uploadModel.licenses) + assertEquals(repository.getLicenses(), uploadModel.licenses) } @Test fun testGetSelectedLicense() { - assertEquals(repository.selectedLicense, uploadModel.selectedLicense) + assertEquals(repository.getSelectedLicense(), uploadModel.selectedLicense) } @Test fun testGetCount() { - assertEquals(repository.count, uploadModel.count) + assertEquals(repository.getCount(), uploadModel.count) } @Test @@ -242,12 +242,12 @@ class UploadRepositoryUnitTest { @Test fun testGetSelectedDepictions() { - assertEquals(repository.selectedDepictions, uploadModel.selectedDepictions) + assertEquals(repository.getSelectedDepictions(), uploadModel.selectedDepictions) } @Test fun testGetSelectedExistingDepictions() { - assertEquals(repository.selectedExistingDepictions, uploadModel.selectedExistingDepictions) + assertEquals(repository.getSelectedExistingDepictions(), uploadModel.selectedExistingDepictions) } @Test @@ -264,7 +264,7 @@ class UploadRepositoryUnitTest { `when`(uploadItem.place).thenReturn(place) `when`(place.wikiDataEntityId).thenReturn("1") assertEquals( - repository.placeDepictions, + repository.getPlaceDepictions(), depictModel.getPlaceDepictions(listOf("1")), ) } @@ -326,7 +326,7 @@ class UploadRepositoryUnitTest { `when`(uploadModel.items).thenReturn(listOf(uploadItem)) `when`(uploadItem.isWLMUpload).thenReturn(true) assertEquals( - repository.isWMLSupportedForThisPlace, + repository.isWMLSupportedForThisPlace(), true, ) } @@ -369,7 +369,7 @@ class UploadRepositoryUnitTest { @Test fun testGetSelectedExistingCategories() { assertEquals( - repository.selectedExistingCategories, + repository.getSelectedExistingCategories(), categoriesModel.getSelectedExistingCategories(), ) } From e070c5dbe89b31021fd4bccf9f836710c9616b37 Mon Sep 17 00:00:00 2001 From: Rohit Verma <101377978+rohit9625@users.noreply.github.com> Date: Sat, 23 Nov 2024 05:05:34 +0530 Subject: [PATCH 36/74] Fix unit tests (#5947) * move createLocale() method to companion object and add test dependency * use mockk() from Mockk library for mocking sealed classes * change method parameter to null-able String type * add null check for accessing property from unit tests * change method signature to match old method's signature It fixes the NullPointerException when running ImageProcessingUnitTest * Fix unresolved references and make properties public for unit tests * fix tests in UploadRepositoryUnitTest by making return type null-able --- app/build.gradle | 1 + .../nrw/commons/category/CategoryClient.kt | 4 ++- .../recentlanguages/RecentLanguagesDao.kt | 5 ++-- .../commons/repository/UploadRepository.kt | 16 ++++++------ .../nrw/commons/settings/SettingsFragment.kt | 26 ++++++++++--------- .../nrw/commons/utils/ImageUtilsWrapper.kt | 2 +- .../commons/utils/MediaDataExtractorUtil.kt | 8 ++---- .../commons/auth/LoginActivityUnitTests.kt | 3 ++- .../commons/auth/SessionManagerUnitTests.kt | 9 ++++--- .../RecentLanguagesDaoUnitTest.kt | 10 +++---- .../settings/SettingsFragmentUnitTests.kt | 8 +++--- .../upload/UploadMediaPresenterTest.kt | 6 ++--- .../upload/UploadRepositoryUnitTest.kt | 6 +++-- .../structure/depictions/DepictedItemTest.kt | 4 +-- 14 files changed, 56 insertions(+), 52 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9e6c56c83..468255d38 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -98,6 +98,7 @@ dependencies { testImplementation 'org.mockito:mockito-core:5.6.0' testImplementation "org.powermock:powermock-module-junit4:2.0.9" testImplementation "org.powermock:powermock-api-mockito2:2.0.9" + testImplementation("io.mockk:mockk:1.13.5") // Unit testing testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt index 992c4ed1c..5571e0ea7 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt @@ -124,7 +124,9 @@ class CategoryClient }.map { it .filter { page -> - !page.categoryInfo().isHidden + // Null check is not redundant because some values could be null + // for mocks when running unit tests + page.categoryInfo()?.isHidden != true }.map { CategoryItem( it.title().replace(CATEGORY_PREFIX, ""), diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt index e97c4f816..a4a06185b 100644 --- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt @@ -6,7 +6,6 @@ import android.content.ContentValues import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.os.RemoteException -import java.util.ArrayList import javax.inject.Inject import javax.inject.Named import javax.inject.Provider @@ -163,9 +162,9 @@ class RecentLanguagesDao @Inject constructor( COLUMN_CODE ) - private const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME" + const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME" - private const val CREATE_TABLE_STATEMENT = "CREATE TABLE $TABLE_NAME (" + + const val CREATE_TABLE_STATEMENT = "CREATE TABLE $TABLE_NAME (" + "$COLUMN_NAME STRING," + "$COLUMN_CODE STRING PRIMARY KEY" + ");" diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt index 0500f4946..377953254 100644 --- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt @@ -19,10 +19,10 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import io.reactivex.Flowable import io.reactivex.Observable import io.reactivex.Single +import timber.log.Timber import java.util.Locale import javax.inject.Inject import javax.inject.Singleton -import timber.log.Timber /** * The repository class for UploadActivity @@ -46,7 +46,7 @@ class UploadRepository @Inject constructor( * * @return */ - fun buildContributions(): Observable { + fun buildContributions(): Observable? { return uploadModel.buildContributions() } @@ -150,7 +150,7 @@ class UploadRepository @Inject constructor( * * @return */ - fun getSelectedLicense(): String { + fun getSelectedLicense(): String? { return uploadModel.selectedLicense } @@ -173,11 +173,11 @@ class UploadRepository @Inject constructor( * @return */ fun preProcessImage( - uploadableFile: UploadableFile, + uploadableFile: UploadableFile?, place: Place?, - similarImageInterface: SimilarImageInterface, + similarImageInterface: SimilarImageInterface?, inAppPictureLocation: LatLng? - ): Observable { + ): Observable? { return uploadModel.preProcessImage( uploadableFile, place, @@ -193,7 +193,7 @@ class UploadRepository @Inject constructor( * @param location Location of the image * @return Quality of UploadItem */ - fun getImageQuality(uploadItem: UploadItem, location: LatLng?): Single { + fun getImageQuality(uploadItem: UploadItem, location: LatLng?): Single? { return uploadModel.getImageQuality(uploadItem, location) } @@ -213,7 +213,7 @@ class UploadRepository @Inject constructor( * @param uploadItem UploadItem whose caption is to be checked * @return Quality of caption of the UploadItem */ - fun getCaptionQuality(uploadItem: UploadItem): Single { + fun getCaptionQuality(uploadItem: UploadItem): Single? { return uploadModel.getCaptionQuality(uploadItem) } diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt index 53f6b28fe..b55ac6009 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt @@ -471,18 +471,20 @@ class SettingsFragment : PreferenceFragmentCompat() { editor.apply() } - /** - * Create Locale based on different types of language codes - * @param languageCode - * @return Locale and throws error for invalid language codes - */ - fun createLocale(languageCode: String): Locale { - val parts = languageCode.split("-") - return when (parts.size) { - 1 -> Locale(parts[0]) - 2 -> Locale(parts[0], parts[1]) - 3 -> Locale(parts[0], parts[1], parts[2]) - else -> throw IllegalArgumentException("Invalid language code: $languageCode") + companion object { + /** + * Create Locale based on different types of language codes + * @param languageCode + * @return Locale and throws error for invalid language codes + */ + fun createLocale(languageCode: String): Locale { + val parts = languageCode.split("-") + return when (parts.size) { + 1 -> Locale(parts[0]) + 2 -> Locale(parts[0], parts[1]) + 3 -> Locale(parts[0], parts[1], parts[2]) + else -> throw IllegalArgumentException("Invalid language code: $languageCode") + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt index 2e0efc690..8393dc652 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt @@ -16,7 +16,7 @@ class ImageUtilsWrapper @Inject constructor() { fun checkImageGeolocationIsDifferent( geolocationOfFileString: String, - latLng: LatLng + latLng: LatLng? ): Single { return Single.fromCallable { ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng) diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt index 9e46525da..93cdabbfc 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt @@ -1,9 +1,5 @@ package fr.free.nrw.commons.utils -import org.apache.commons.lang3.StringUtils - -import java.util.ArrayList - object MediaDataExtractorUtil { /** @@ -13,8 +9,8 @@ object MediaDataExtractorUtil { * @return */ @JvmStatic - fun extractCategoriesFromList(source: String): List { - if (source.isBlank()) { + fun extractCategoriesFromList(source: String?): List { + if (source.isNullOrBlank()) { return emptyList() } val cats = source.split("|") diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt index b50c820a0..162f50584 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt @@ -16,6 +16,7 @@ import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.auth.login.LoginResult import fr.free.nrw.commons.createTestClient import fr.free.nrw.commons.kvstore.JsonKvStore +import io.mockk.mockk import org.junit.Assert import org.junit.Before import org.junit.Test @@ -66,11 +67,11 @@ class LoginActivityUnitTests { @Mock private lateinit var account: Account - @Mock private lateinit var loginResult: LoginResult @Before fun setUp() { + loginResult = mockk() MockitoAnnotations.openMocks(this) OkHttpConnectionFactory.CLIENT = createTestClient() activity = Robolectric.buildActivity(LoginActivity::class.java).create().get() diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/SessionManagerUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/SessionManagerUnitTests.kt index 7b7c260e8..4e5c78f3e 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/SessionManagerUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/SessionManagerUnitTests.kt @@ -7,13 +7,14 @@ import androidx.test.core.app.ApplicationProvider import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.auth.login.LoginResult import fr.free.nrw.commons.kvstore.JsonKvStore +import io.mockk.every +import io.mockk.mockk import org.junit.Assert import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations -import org.powermock.api.mockito.PowerMockito.`when` import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config @@ -33,7 +34,6 @@ class SessionManagerUnitTests { @Mock private lateinit var defaultKvStore: JsonKvStore - @Mock private lateinit var loginResult: LoginResult @Mock @@ -41,6 +41,7 @@ class SessionManagerUnitTests { @Before fun setUp() { + loginResult = mockk() MockitoAnnotations.openMocks(this) accountManager = AccountManager.get(ApplicationProvider.getApplicationContext()) shadowOf(accountManager).addAccount(account) @@ -68,8 +69,8 @@ class SessionManagerUnitTests { @Test @Throws(Exception::class) fun testUpdateAccount() { - `when`(loginResult.userName).thenReturn("username") - `when`(loginResult.password).thenReturn("password") + every { loginResult.userName } returns "username" + every { loginResult.password } returns "password" val method: Method = SessionManager::class.java.getDeclaredMethod( "updateAccount", diff --git a/app/src/test/kotlin/fr/free/nrw/commons/recentlanguages/RecentLanguagesDaoUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/recentlanguages/RecentLanguagesDaoUnitTest.kt index 087640a44..e0f4587f4 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/recentlanguages/RecentLanguagesDaoUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/recentlanguages/RecentLanguagesDaoUnitTest.kt @@ -85,7 +85,7 @@ class RecentLanguagesDaoUnitTest { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())) .thenReturn(createCursor(14)) - val result = testObject.recentLanguages + val result = testObject.getRecentLanguages() Assert.assertEquals(14, (result.size)) } @@ -95,20 +95,20 @@ class RecentLanguagesDaoUnitTest { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow( RemoteException(""), ) - testObject.recentLanguages + testObject.getRecentLanguages() } @Test fun getGetRecentLanguagesReturnsEmptyList_emptyCursor() { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())) .thenReturn(createCursor(0)) - Assert.assertTrue(testObject.recentLanguages.isEmpty()) + Assert.assertTrue(testObject.getRecentLanguages().isEmpty()) } @Test fun getGetRecentLanguagesReturnsEmptyList_nullCursor() { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null) - Assert.assertTrue(testObject.recentLanguages.isEmpty()) + Assert.assertTrue(testObject.getRecentLanguages().isEmpty()) } @Test @@ -117,7 +117,7 @@ class RecentLanguagesDaoUnitTest { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor) whenever(mockCursor.moveToFirst()).thenReturn(false) - testObject.recentLanguages + testObject.getRecentLanguages() verify(mockCursor).close() } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt index 6c9ee9d03..5a6d27e1b 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt @@ -19,7 +19,7 @@ import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.recentlanguages.Language import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao -import fr.free.nrw.commons.settings.SettingsFragment.createLocale +import fr.free.nrw.commons.settings.SettingsFragment.Companion.createLocale import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Before @@ -156,14 +156,14 @@ class SettingsFragmentUnitTests { ) method.isAccessible = true method.invoke(fragment, "appUiDefaultLanguagePref") - verify(recentLanguagesDao, times(1)).recentLanguages + verify(recentLanguagesDao, times(1)).getRecentLanguages() } @Test @Throws(Exception::class) fun `Test prepareAppLanguages when recently used languages is not empty`() { Shadows.shadowOf(Looper.getMainLooper()).idle() - whenever(recentLanguagesDao.recentLanguages) + whenever(recentLanguagesDao.getRecentLanguages()) .thenReturn( mutableListOf( Language("English", "en"), @@ -181,7 +181,7 @@ class SettingsFragmentUnitTests { ) method.isAccessible = true method.invoke(fragment, "appUiDefaultLanguagePref") - verify(recentLanguagesDao, times(2)).recentLanguages + verify(recentLanguagesDao, times(2)).getRecentLanguages() } @Test diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt index bf27280aa..47c4d0ae5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt @@ -131,7 +131,7 @@ class UploadMediaPresenterTest { */ @Test fun getImageQualityTest() { - whenever(repository.uploads).thenReturn(listOf(uploadItem)) + whenever(repository.getUploads()).thenReturn(listOf(uploadItem)) whenever(repository.getImageQuality(uploadItem, location)) .thenReturn(testSingleImageResult) whenever(uploadItem.imageQuality).thenReturn(0) @@ -149,7 +149,7 @@ class UploadMediaPresenterTest { */ @Test fun `get ImageQuality Test while coordinates equals to null`() { - whenever(repository.uploads).thenReturn(listOf(uploadItem)) + whenever(repository.getUploads()).thenReturn(listOf(uploadItem)) whenever(repository.getImageQuality(uploadItem, location)) .thenReturn(testSingleImageResult) whenever(uploadItem.imageQuality).thenReturn(0) @@ -225,7 +225,7 @@ class UploadMediaPresenterTest { */ @Test fun fetchImageAndTitleTest() { - whenever(repository.uploads).thenReturn(listOf(uploadItem)) + whenever(repository.getUploads()).thenReturn(listOf(uploadItem)) whenever(repository.getUploadItem(ArgumentMatchers.anyInt())) .thenReturn(uploadItem) whenever(uploadItem.uploadMediaDetails).thenReturn(listOf()) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt index 233b0de32..ac01d237f 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt @@ -15,6 +15,7 @@ import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.upload.structure.depictions.DepictModel import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import io.reactivex.Completable +import io.reactivex.Observable import io.reactivex.Single import org.junit.Before import org.junit.Test @@ -196,7 +197,7 @@ class UploadRepositoryUnitTest { fun testGetCaptionQuality() { assertEquals( repository.getCaptionQuality(uploadItem), - uploadModel.getCaptionQuality(uploadItem), + uploadModel.getCaptionQuality(uploadItem) ) } @@ -386,7 +387,8 @@ class UploadRepositoryUnitTest { fun testGetCategories() { assertEquals( repository.getCategories(listOf("Test")), - categoriesModel.getCategoriesByName(mutableListOf("Test")), + categoriesModel.getCategoriesByName(mutableListOf("Test")) + ?: Observable.empty>() ) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt index 892d501fd..e0d339eee 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt @@ -1,10 +1,10 @@ package fr.free.nrw.commons.upload.structure.depictions -import com.nhaarman.mockitokotlin2.mock import depictedItem import entity import entityId import fr.free.nrw.commons.wikidata.WikidataProperties +import io.mockk.mockk import org.junit.Assert import org.junit.Test import place @@ -53,7 +53,7 @@ class DepictedItemTest { entity( statements = mapOf( - WikidataProperties.IMAGE.propertyName to listOf(statement(snak(dataValue = mock()))), + WikidataProperties.IMAGE.propertyName to listOf(statement(snak(dataValue = mockk()))), ), ), ).imageUrl, From bafae821e26bd71e351a6a97f4815aca6c502cd1 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Sat, 23 Nov 2024 18:15:46 +0530 Subject: [PATCH 37/74] Migration of review module from Java to Kotlin (#5950) * Rename .java to .kt * Migrated repository module to Kotlin * Rename .java to .kt * Migrated review module to Kotlin --- .../navtab/MoreBottomSheetFragment.java | 2 +- .../nrw/commons/review/ReviewActivity.java | 334 ----------------- .../free/nrw/commons/review/ReviewActivity.kt | 336 ++++++++++++++++++ .../nrw/commons/review/ReviewController.java | 220 ------------ .../nrw/commons/review/ReviewController.kt | 231 ++++++++++++ .../review/{ReviewDao.java => ReviewDao.kt} | 21 +- .../free/nrw/commons/review/ReviewEntity.java | 19 - .../free/nrw/commons/review/ReviewEntity.kt | 13 + .../free/nrw/commons/review/ReviewHelper.kt | 4 +- .../commons/review/ReviewImageFragment.java | 262 -------------- .../nrw/commons/review/ReviewImageFragment.kt | 251 +++++++++++++ .../commons/review/ReviewPagerAdapter.java | 53 --- .../nrw/commons/review/ReviewPagerAdapter.kt | 37 ++ .../nrw/commons/review/ReviewViewPager.java | 30 -- .../nrw/commons/review/ReviewViewPager.kt | 25 ++ 15 files changed, 906 insertions(+), 932 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewController.kt rename app/src/main/java/fr/free/nrw/commons/review/{ReviewDao.java => ReviewDao.kt} (54%) delete mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.kt diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java index 0bd8333e3..9ea59488e 100644 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java @@ -232,7 +232,7 @@ public class MoreBottomSheetFragment extends BottomSheetDialogFragment { } protected void onPeerReviewClicked() { - ReviewActivity.startYourself(getActivity(), getString(R.string.title_activity_review)); + ReviewActivity.Companion.startYourself(getActivity(), getString(R.string.title_activity_review)); } } diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java deleted file mode 100644 index 40d743a19..000000000 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java +++ /dev/null @@ -1,334 +0,0 @@ -package fr.free.nrw.commons.review; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AccountUtil; -import fr.free.nrw.commons.databinding.ActivityReviewBinding; -import fr.free.nrw.commons.delete.DeleteHelper; -import fr.free.nrw.commons.media.MediaDetailFragment; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.util.Locale; -import javax.inject.Inject; - -public class ReviewActivity extends BaseActivity { - - - private ActivityReviewBinding binding; - - MediaDetailFragment mediaDetailFragment; - public ReviewPagerAdapter reviewPagerAdapter; - public ReviewController reviewController; - @Inject - ReviewHelper reviewHelper; - @Inject - DeleteHelper deleteHelper; - /** - * Represent fragment for ReviewImage - * Use to call some methods of ReviewImage fragment - */ - private ReviewImageFragment reviewImageFragment; - - /** - * Flag to check whether there are any non-hidden categories in the File - */ - private boolean hasNonHiddenCategories = false; - - final String SAVED_MEDIA = "saved_media"; - private Media media; - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (media != null) { - outState.putParcelable(SAVED_MEDIA, media); - } - } - - /** - * Consumers should be simply using this method to use this activity. - * - * @param context - * @param title Page title - */ - public static void startYourself(Context context, String title) { - Intent reviewActivity = new Intent(context, ReviewActivity.class); - reviewActivity.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - reviewActivity.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - context.startActivity(reviewActivity); - } - - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - - public Media getMedia() { - return media; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivityReviewBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - setSupportActionBar(binding.toolbarBinding.toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - reviewController = new ReviewController(deleteHelper, this); - - reviewPagerAdapter = new ReviewPagerAdapter(getSupportFragmentManager()); - binding.viewPagerReview.setAdapter(reviewPagerAdapter); - binding.pagerIndicatorReview.setViewPager(binding.viewPagerReview); - binding.pbReviewImage.setVisibility(View.VISIBLE); - - Drawable d[]=binding.skipImage.getCompoundDrawablesRelative(); - d[2].setColorFilter(getApplicationContext().getResources().getColor(R.color.button_blue), PorterDuff.Mode.SRC_IN); - - if (savedInstanceState != null && savedInstanceState.getParcelable(SAVED_MEDIA) != null) { - updateImage(savedInstanceState.getParcelable(SAVED_MEDIA)); // Use existing media if we have one - setUpMediaDetailOnOrientation(); - } else { - runRandomizer(); //Run randomizer whenever everything is ready so that a first random image will be added - } - - binding.skipImage.setOnClickListener(view -> { - reviewImageFragment = getInstanceOfReviewImageFragment(); - reviewImageFragment.disableButtons(); - runRandomizer(); - }); - - binding.reviewImageView.setOnClickListener(view ->setUpMediaDetailFragment()); - - binding.skipImage.setOnTouchListener((view, event) -> { - if (event.getAction() == MotionEvent.ACTION_UP && event.getRawX() >= ( - binding.skipImage.getRight() - binding.skipImage - .getCompoundDrawables()[2].getBounds().width())) { - showSkipImageInfo(); - return true; - } - return false; - }); - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - @SuppressLint("CheckResult") - public boolean runRandomizer() { - hasNonHiddenCategories = false; - binding.pbReviewImage.setVisibility(View.VISIBLE); - binding.viewPagerReview.setCurrentItem(0); - // Finds non-hidden categories from Media instance - compositeDisposable.add(reviewHelper.getRandomMedia() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::checkWhetherFileIsUsedInWikis)); - return true; - } - - /** - * Check whether media is used or not in any Wiki Page - */ - @SuppressLint("CheckResult") - private void checkWhetherFileIsUsedInWikis(final Media media) { - compositeDisposable.add(reviewHelper.checkFileUsage(media.getFilename()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - // result false indicates media is not used in any wiki - if (!result) { - // Finds non-hidden categories from Media instance - findNonHiddenCategories(media); - } else { - runRandomizer(); - } - })); - } - - /** - * Finds non-hidden categories and updates current image - */ - private void findNonHiddenCategories(Media media) { - for(String key : media.getCategoriesHiddenStatus().keySet()) { - Boolean value = media.getCategoriesHiddenStatus().get(key); - // If non-hidden category is found then set hasNonHiddenCategories to true - // so that category review cannot be skipped - if(!value) { - hasNonHiddenCategories = true; - break; - } - } - reviewImageFragment = getInstanceOfReviewImageFragment(); - reviewImageFragment.disableButtons(); - updateImage(media); - } - - @SuppressLint("CheckResult") - private void updateImage(Media media) { - reviewHelper.addViewedImagesToDB(media.getPageId()); - this.media = media; - String fileName = media.getFilename(); - if (fileName.length() == 0) { - ViewUtil.showShortSnackbar(binding.drawerLayout, R.string.error_review); - return; - } - - //If The Media User and Current Session Username is same then Skip the Image - if (media.getUser() != null && media.getUser().equals(AccountUtil.getUserName(getApplicationContext()))) { - runRandomizer(); - return; - } - - binding.reviewImageView.setImageURI(media.getImageUrl()); - - reviewController.onImageRefreshed(media); //file name is updated - compositeDisposable.add(reviewHelper.getFirstRevisionOfFile(fileName) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(revision -> { - reviewController.firstRevision = revision; - reviewPagerAdapter.updateFileInformation(); - @SuppressLint({"StringFormatInvalid", "LocalSuppress"}) String caption = String.format(getString(R.string.review_is_uploaded_by), fileName, revision.getUser()); - binding.tvImageCaption.setText(caption); - binding.pbReviewImage.setVisibility(View.GONE); - reviewImageFragment = getInstanceOfReviewImageFragment(); - reviewImageFragment.enableButtons(); - })); - binding.viewPagerReview.setCurrentItem(0); - } - - public void swipeToNext() { - int nextPos = binding.viewPagerReview.getCurrentItem() + 1; - // If currently at category fragment, then check whether the media has any non-hidden category - if (nextPos <= 3) { - binding.viewPagerReview.setCurrentItem(nextPos); - if (nextPos == 2) { - // The media has no non-hidden category. Such media are already flagged by server-side bots, so no need to review manually. - if (!hasNonHiddenCategories) { - swipeToNext(); - return; - } - } - } else { - runRandomizer(); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - compositeDisposable.clear(); - binding = null; - } - - public void showSkipImageInfo(){ - DialogUtil.showAlertDialog(ReviewActivity.this, - getString(R.string.skip_image).toUpperCase(Locale.ROOT), - getString(R.string.skip_image_explanation), - getString(android.R.string.ok), - "", - null, - null); - } - - public void showReviewImageInfo() { - DialogUtil.showAlertDialog(ReviewActivity.this, - getString(R.string.title_activity_review), - getString(R.string.review_image_explanation), - getString(android.R.string.ok), - "", - null, - null); - } - - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_review_activty, menu); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_image_info: - showReviewImageInfo(); - return true; - } - return super.onOptionsItemSelected(item); - } - - /** - * this function return the instance of reviewImageFragment - */ - public ReviewImageFragment getInstanceOfReviewImageFragment(){ - int currentItemOfReviewPager = binding.viewPagerReview.getCurrentItem(); - reviewImageFragment = (ReviewImageFragment) reviewPagerAdapter.instantiateItem(binding.viewPagerReview, currentItemOfReviewPager); - return reviewImageFragment; - } - - /** - * set up the media detail fragment when click on the review image - */ - private void setUpMediaDetailFragment() { - if (binding.mediaDetailContainer.getVisibility() == View.GONE && media != null) { - binding.mediaDetailContainer.setVisibility(View.VISIBLE); - binding.reviewActivityContainer.setVisibility(View.INVISIBLE); - FragmentManager fragmentManager = getSupportFragmentManager(); - mediaDetailFragment = new MediaDetailFragment(); - Bundle bundle = new Bundle(); - bundle.putParcelable("media", media); - mediaDetailFragment.setArguments(bundle); - fragmentManager.beginTransaction().add(R.id.mediaDetailContainer, mediaDetailFragment). - addToBackStack("MediaDetail").commit(); - } - } - - /** - * handle the back pressed event of this activity - * this function call every time when back button is pressed - */ - @Override - public void onBackPressed() { - if (binding.mediaDetailContainer.getVisibility() == View.VISIBLE) { - binding.mediaDetailContainer.setVisibility(View.GONE); - binding.reviewActivityContainer.setVisibility(View.VISIBLE); - } - super.onBackPressed(); - } - - /** - * set up media detail fragment after orientation change - */ - private void setUpMediaDetailOnOrientation() { - Fragment mediaDetailFragment = getSupportFragmentManager() - .findFragmentById(R.id.mediaDetailContainer); - if (mediaDetailFragment != null) { - binding.mediaDetailContainer.setVisibility(View.VISIBLE); - binding.reviewActivityContainer.setVisibility(View.INVISIBLE); - getSupportFragmentManager().beginTransaction() - .replace(R.id.mediaDetailContainer, mediaDetailFragment).commit(); - } - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt new file mode 100644 index 000000000..44b0f9bc1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt @@ -0,0 +1,336 @@ +package fr.free.nrw.commons.review + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.graphics.PorterDuff +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.AccountUtil +import fr.free.nrw.commons.databinding.ActivityReviewBinding +import fr.free.nrw.commons.delete.DeleteHelper +import fr.free.nrw.commons.media.MediaDetailFragment +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.DialogUtil +import fr.free.nrw.commons.utils.ViewUtil +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import java.util.Locale +import javax.inject.Inject + +class ReviewActivity : BaseActivity() { + + private lateinit var binding: ActivityReviewBinding + + private var mediaDetailFragment: MediaDetailFragment? = null + lateinit var reviewPagerAdapter: ReviewPagerAdapter + lateinit var reviewController: ReviewController + + @Inject + lateinit var reviewHelper: ReviewHelper + + @Inject + lateinit var deleteHelper: DeleteHelper + + /** + * Represent fragment for ReviewImage + * Use to call some methods of ReviewImage fragment + */ + private var reviewImageFragment: ReviewImageFragment? = null + private var hasNonHiddenCategories = false + var media: Media? = null + + private val SAVED_MEDIA = "saved_media" + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + media?.let { + outState.putParcelable(SAVED_MEDIA, it) + } + } + + /** + * Consumers should be simply using this method to use this activity. + * + * @param context + * @param title Page title + */ + companion object { + fun startYourself(context: Context, title: String) { + val reviewActivity = Intent(context, ReviewActivity::class.java) + reviewActivity.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + reviewActivity.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + context.startActivity(reviewActivity) + } + } + + @SuppressLint("ClickableViewAccessibility") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityReviewBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.toolbarBinding?.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + reviewController = ReviewController(deleteHelper, this) + + reviewPagerAdapter = ReviewPagerAdapter(supportFragmentManager) + binding.viewPagerReview.adapter = reviewPagerAdapter + binding.pagerIndicatorReview.setViewPager(binding.viewPagerReview) + binding.pbReviewImage.visibility = View.VISIBLE + + binding.skipImage.compoundDrawablesRelative[2]?.setColorFilter( + resources.getColor(R.color.button_blue), + PorterDuff.Mode.SRC_IN + ) + + if (savedInstanceState?.getParcelable(SAVED_MEDIA) != null) { + updateImage(savedInstanceState.getParcelable(SAVED_MEDIA)!!) + setUpMediaDetailOnOrientation() + } else { + runRandomizer() + } + + binding.skipImage.setOnClickListener { + reviewImageFragment = getInstanceOfReviewImageFragment() + reviewImageFragment?.disableButtons() + runRandomizer() + } + + binding.reviewImageView.setOnClickListener { + setUpMediaDetailFragment() + } + + binding.skipImage.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_UP && + event.rawX >= (binding.skipImage.right - binding.skipImage.compoundDrawables[2].bounds.width()) + ) { + showSkipImageInfo() + true + } else { + false + } + } + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + @SuppressLint("CheckResult") + fun runRandomizer(): Boolean { + hasNonHiddenCategories = false + binding.pbReviewImage.visibility = View.VISIBLE + binding.viewPagerReview.currentItem = 0 + + compositeDisposable.add( + reviewHelper.getRandomMedia() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(::checkWhetherFileIsUsedInWikis) + ) + return true + } + + /** + * Check whether media is used or not in any Wiki Page + */ + @SuppressLint("CheckResult") + private fun checkWhetherFileIsUsedInWikis(media: Media) { + compositeDisposable.add( + reviewHelper.checkFileUsage(media.filename) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { result -> + if (!result) { + findNonHiddenCategories(media) + } else { + runRandomizer() + } + } + ) + } + + /** + * Finds non-hidden categories and updates current image + */ + private fun findNonHiddenCategories(media: Media) { + this.media = media + // If non-hidden category is found then set hasNonHiddenCategories to true + // so that category review cannot be skipped + hasNonHiddenCategories = media.categoriesHiddenStatus.values.any { !it } + reviewImageFragment = getInstanceOfReviewImageFragment() + reviewImageFragment?.disableButtons() + updateImage(media) + } + + @SuppressLint("CheckResult") + private fun updateImage(media: Media) { + reviewHelper.addViewedImagesToDB(media.pageId) + this.media = media + val fileName = media.filename + + if (fileName.isNullOrEmpty()) { + ViewUtil.showShortSnackbar(binding.drawerLayout, R.string.error_review) + return + } + + //If The Media User and Current Session Username is same then Skip the Image + if (media.user == AccountUtil.getUserName(applicationContext)) { + runRandomizer() + return + } + + binding.reviewImageView.setImageURI(media.imageUrl) + + reviewController.onImageRefreshed(media) // filename is updated + compositeDisposable.add( + reviewHelper.getFirstRevisionOfFile(fileName) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { revision -> + reviewController.firstRevision = revision + reviewPagerAdapter.updateFileInformation() + val caption = getString( + R.string.review_is_uploaded_by, + fileName, + revision.user + ) + binding.tvImageCaption.text = caption + binding.pbReviewImage.visibility = View.GONE + reviewImageFragment = getInstanceOfReviewImageFragment() + reviewImageFragment?.enableButtons() + } + ) + binding.viewPagerReview.currentItem = 0 + } + + fun swipeToNext() { + val nextPos = binding.viewPagerReview.currentItem + 1 + + // If currently at category fragment, then check whether the media has any non-hidden category + if (nextPos <= 3) { + binding.viewPagerReview.currentItem = nextPos + if (nextPos == 2 && !hasNonHiddenCategories) + { + // The media has no non-hidden category. Such media are already flagged by server-side bots, so no need to review manually. + swipeToNext() + } + } else { + runRandomizer() + } + } + + public override fun onDestroy() { + super.onDestroy() + compositeDisposable.clear() + } + + fun showSkipImageInfo() { + DialogUtil.showAlertDialog( + this, + getString(R.string.skip_image).uppercase(Locale.ROOT), + getString(R.string.skip_image_explanation), + getString(android.R.string.ok), + null, + null, + null + ) + } + + fun showReviewImageInfo() { + DialogUtil.showAlertDialog( + this, + getString(R.string.title_activity_review), + getString(R.string.review_image_explanation), + getString(android.R.string.ok), + null, + null, + null + ) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_review_activty, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.menu_image_info -> { + showReviewImageInfo() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + /** + * this function return the instance of reviewImageFragment + */ + private fun getInstanceOfReviewImageFragment(): ReviewImageFragment? { + val currentItemOfReviewPager = binding.viewPagerReview.currentItem + return reviewPagerAdapter.instantiateItem( + binding.viewPagerReview, + currentItemOfReviewPager + ) as? ReviewImageFragment + } + + /** + * set up the media detail fragment when click on the review image + */ + private fun setUpMediaDetailFragment() { + if (binding.mediaDetailContainer.visibility == View.GONE && media != null) { + binding.mediaDetailContainer.visibility = View.VISIBLE + binding.reviewActivityContainer.visibility = View.INVISIBLE + val fragmentManager = supportFragmentManager + mediaDetailFragment = MediaDetailFragment().apply { + arguments = Bundle().apply { + putParcelable("media", media) + } + } + fragmentManager.beginTransaction() + .add(R.id.mediaDetailContainer, mediaDetailFragment!!) + .addToBackStack("MediaDetail") + .commit() + } + } + + /** + * handle the back pressed event of this activity + * this function call every time when back button is pressed + */ + @Deprecated("This method has been deprecated in favor of using the" + + "{@link OnBackPressedDispatcher} via {@link #getOnBackPressedDispatcher()}." + + "The OnBackPressedDispatcher controls how back button events are dispatched" + + "to one or more {@link OnBackPressedCallback} objects.") + override fun onBackPressed() { + if (binding.mediaDetailContainer.visibility == View.VISIBLE) { + binding.mediaDetailContainer.visibility = View.GONE + binding.reviewActivityContainer.visibility = View.VISIBLE + } + super.onBackPressed() + } + + /** + * set up media detail fragment after orientation change + */ + private fun setUpMediaDetailOnOrientation() { + val fragment = supportFragmentManager.findFragmentById(R.id.mediaDetailContainer) + fragment?.let { + binding.mediaDetailContainer.visibility = View.VISIBLE + binding.reviewActivityContainer.visibility = View.INVISIBLE + supportFragmentManager.beginTransaction() + .replace(R.id.mediaDetailContainer, it) + .commit() + } + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java deleted file mode 100644 index e3d5b2256..000000000 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java +++ /dev/null @@ -1,220 +0,0 @@ -package fr.free.nrw.commons.review; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.NotificationManager; -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; - -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; -import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage; - -import java.util.ArrayList; -import java.util.concurrent.Callable; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.actions.PageEditClient; -import fr.free.nrw.commons.actions.ThanksClient; -import fr.free.nrw.commons.delete.DeleteHelper; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; -import io.reactivex.ObservableSource; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; - -@Singleton -public class ReviewController { - private static final int NOTIFICATION_SEND_THANK = 0x102; - private static final int NOTIFICATION_CHECK_CATEGORY = 0x101; - protected static ArrayList categories; - @Inject - ThanksClient thanksClient; - - @Inject - SessionManager sessionManager; - private final DeleteHelper deleteHelper; - @Nullable - MwQueryPage.Revision firstRevision; // TODO: maybe we can expand this class to include fileName - @Inject - @Named("commons-page-edit") - PageEditClient pageEditClient; - private NotificationManager notificationManager; - private NotificationCompat.Builder notificationBuilder; - private Media media; - - ReviewController(DeleteHelper deleteHelper, Context context) { - this.deleteHelper = deleteHelper; - CommonsApplication.createNotificationChannel(context.getApplicationContext()); - notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationBuilder = new NotificationCompat.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL); - } - - void onImageRefreshed(Media media) { - this.media = media; - } - - public Media getMedia() { - return media; - } - - public enum DeleteReason { - SPAM, - COPYRIGHT_VIOLATION - } - - void reportSpam(@NonNull Activity activity, ReviewCallback reviewCallback) { - Timber.d("Report spam for %s", media.getFilename()); - deleteHelper.askReasonAndExecute(media, - activity, - activity.getResources().getString(R.string.review_spam_report_question), - DeleteReason.SPAM, - reviewCallback); - } - - void reportPossibleCopyRightViolation(@NonNull Activity activity, ReviewCallback reviewCallback) { - Timber.d("Report spam for %s", media.getFilename()); - deleteHelper.askReasonAndExecute(media, - activity, - activity.getResources().getString(R.string.review_c_violation_report_question), - DeleteReason.COPYRIGHT_VIOLATION, - reviewCallback); - } - - @SuppressLint("CheckResult") - void reportWrongCategory(@NonNull Activity activity, ReviewCallback reviewCallback) { - Context context = activity.getApplicationContext(); - ApplicationlessInjection - .getInstance(context) - .getCommonsApplicationComponent() - .inject(this); - - ViewUtil.showShortToast(context, context.getString(R.string.check_category_toast, media.getDisplayTitle())); - - publishProgress(context, 0); - String summary = context.getString(R.string.check_category_edit_summary); - Observable.defer((Callable>) () -> - pageEditClient.appendEdit(media.getFilename(), "\n{{subst:chc}}\n", summary)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((result) -> { - publishProgress(context, 2); - String message; - String title; - - if (result) { - title = context.getString(R.string.check_category_success_title); - message = context.getString(R.string.check_category_success_message, media.getDisplayTitle()); - reviewCallback.onSuccess(); - } else { - title = context.getString(R.string.check_category_failure_title); - message = context.getString(R.string.check_category_failure_message, media.getDisplayTitle()); - reviewCallback.onFailure(); - } - - showNotification(title, message); - - }, Timber::e); - } - - private void publishProgress(@NonNull Context context, int i) { - int[] messages = new int[]{R.string.getting_edit_token, R.string.check_category_adding_template}; - String message = ""; - if (0 < i && i < messages.length) { - message = context.getString(messages[i]); - } - - notificationBuilder.setContentTitle(context.getString(R.string.check_category_notification_title, media.getDisplayTitle())) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(message)) - .setSmallIcon(R.drawable.ic_launcher) - .setProgress(messages.length, i, false) - .setOngoing(true); - notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build()); - } - - @SuppressLint({"CheckResult", "StringFormatInvalid"}) - void sendThanks(@NonNull Activity activity) { - Context context = activity.getApplicationContext(); - ApplicationlessInjection - .getInstance(context) - .getCommonsApplicationComponent() - .inject(this); - ViewUtil.showShortToast(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle())); - - if (firstRevision == null) { - return; - } - - Observable.defer((Callable>) () -> thanksClient.thank(firstRevision.getRevisionId())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - displayThanksToast(context, result); - }, throwable -> { - if (throwable instanceof InvalidLoginTokenException) { - final String username = sessionManager.getUserName(); - final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( - activity, - activity.getString(R.string.invalid_login_message), - username - ); - - CommonsApplication.getInstance().clearApplicationData( - activity, logoutListener); - } else { - Timber.e(throwable); - } - }); - } - - @SuppressLint("StringFormatInvalid") - private void displayThanksToast(final Context context, final boolean result){ - final String message; - final String title; - if (result) { - title = context.getString(R.string.send_thank_success_title); - message = context.getString(R.string.send_thank_success_message, media.getDisplayTitle()); - } else { - title = context.getString(R.string.send_thank_failure_title); - message = context.getString(R.string.send_thank_failure_message, media.getDisplayTitle()); - } - - ViewUtil.showShortToast(context,message); - } - - private void showNotification(String title, String message) { - notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) - .setContentTitle(title) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(message)) - .setSmallIcon(R.drawable.ic_launcher) - .setProgress(0, 0, false) - .setOngoing(false) - .setPriority(NotificationCompat.PRIORITY_HIGH); - notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build()); - } - - public interface ReviewCallback { - void onSuccess(); - - void onFailure(); - - void onTokenException(Exception e); - - void disableButtons(); - - void enableButtons(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.kt new file mode 100644 index 000000000..62652bd5b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.kt @@ -0,0 +1,231 @@ +package fr.free.nrw.commons.review + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.NotificationManager +import android.content.Context + +import androidx.core.app.NotificationCompat + +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException +import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage + +import java.util.ArrayList +import java.util.concurrent.Callable + +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.actions.PageEditClient +import fr.free.nrw.commons.actions.ThanksClient +import fr.free.nrw.commons.delete.DeleteHelper +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.utils.ViewUtil +import io.reactivex.Observable +import io.reactivex.ObservableSource +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + + +@Singleton +class ReviewController @Inject constructor( + private val deleteHelper: DeleteHelper, + context: Context +) { + + companion object { + private const val NOTIFICATION_SEND_THANK = 0x102 + private const val NOTIFICATION_CHECK_CATEGORY = 0x101 + protected var categories: ArrayList = ArrayList() + } + + @Inject + lateinit var thanksClient: ThanksClient + + @Inject + lateinit var sessionManager: SessionManager + + @Inject + @field: Named("commons-page-edit") + lateinit var pageEditClient: PageEditClient + + var firstRevision: MwQueryPage.Revision? = null // TODO: maybe we can expand this class to include fileName + + private val notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private val notificationBuilder: NotificationCompat.Builder = + NotificationCompat.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) + + var media: Media? = null + + init { + CommonsApplication.createNotificationChannel(context.applicationContext) + } + + fun onImageRefreshed(media: Media) { + this.media = media + } + + enum class DeleteReason { + SPAM, + COPYRIGHT_VIOLATION + } + + fun reportSpam(activity: Activity, reviewCallback: ReviewCallback) { + Timber.d("Report spam for %s", media?.filename) + deleteHelper.askReasonAndExecute( + media, + activity, + activity.resources.getString(R.string.review_spam_report_question), + DeleteReason.SPAM, + reviewCallback + ) + } + + fun reportPossibleCopyRightViolation(activity: Activity, reviewCallback: ReviewCallback) { + Timber.d("Report copyright violation for %s", media?.filename) + deleteHelper.askReasonAndExecute( + media, + activity, + activity.resources.getString(R.string.review_c_violation_report_question), + DeleteReason.COPYRIGHT_VIOLATION, + reviewCallback + ) + } + + @SuppressLint("CheckResult") + fun reportWrongCategory(activity: Activity, reviewCallback: ReviewCallback) { + val context = activity.applicationContext + ApplicationlessInjection + .getInstance(context) + .commonsApplicationComponent + .inject(this) + + ViewUtil.showShortToast( + context, + context.getString(R.string.check_category_toast, media?.displayTitle) + ) + + publishProgress(context, 0) + val summary = context.getString(R.string.check_category_edit_summary) + + Observable.defer { + pageEditClient.appendEdit(media?.filename ?: "", "\n{{subst:chc}}\n", summary) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ result -> + publishProgress(context, 2) + val (title, message) = if (result) { + reviewCallback.onSuccess() + context.getString(R.string.check_category_success_title) to + context.getString(R.string.check_category_success_message, media?.displayTitle) + } else { + reviewCallback.onFailure() + context.getString(R.string.check_category_failure_title) to + context.getString(R.string.check_category_failure_message, media?.displayTitle) + } + showNotification(title, message) + }, Timber::e) + } + + private fun publishProgress(context: Context, progress: Int) { + val messages = arrayOf( + R.string.getting_edit_token, + R.string.check_category_adding_template + ) + + val message = if (progress in 1 until messages.size) { + context.getString(messages[progress]) + } else "" + + notificationBuilder.setContentTitle( + context.getString( + R.string.check_category_notification_title, + media?.displayTitle + ) + ) + .setStyle(NotificationCompat.BigTextStyle().bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(messages.size, progress, false) + .setOngoing(true) + + notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build()) + } + + @SuppressLint("CheckResult") + fun sendThanks(activity: Activity) { + val context = activity.applicationContext + ApplicationlessInjection + .getInstance(context) + .commonsApplicationComponent + .inject(this) + + ViewUtil.showShortToast( + context, + context.getString(R.string.send_thank_toast, media?.displayTitle) + ) + + if (firstRevision == null) return + + Observable.defer { + thanksClient.thank(firstRevision!!.revisionId) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ result -> + displayThanksToast(context, result) + }, { throwable -> + if (throwable is InvalidLoginTokenException) { + val username = sessionManager.userName + val logoutListener = CommonsApplication.BaseLogoutListener( + activity, + activity.getString(R.string.invalid_login_message), + username + ) + CommonsApplication.instance.clearApplicationData(activity, logoutListener) + } else { + Timber.e(throwable) + } + }) + } + + @SuppressLint("StringFormatInvalid") + private fun displayThanksToast(context: Context, result: Boolean) { + val (title, message) = if (result) { + context.getString(R.string.send_thank_success_title) to + context.getString(R.string.send_thank_success_message, media?.displayTitle) + } else { + context.getString(R.string.send_thank_failure_title) to + context.getString(R.string.send_thank_failure_message, media?.displayTitle) + } + + ViewUtil.showShortToast(context, message) + } + + private fun showNotification(title: String, message: String) { + notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) + .setContentTitle(title) + .setStyle(NotificationCompat.BigTextStyle().bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(0, 0, false) + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_HIGH) + + notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build()) + } + + interface ReviewCallback { + fun onSuccess() + fun onFailure() + fun onTokenException(e: Exception) + fun disableButtons() + fun enableButtons() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewDao.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewDao.kt similarity index 54% rename from app/src/main/java/fr/free/nrw/commons/review/ReviewDao.java rename to app/src/main/java/fr/free/nrw/commons/review/ReviewDao.kt index c3e8c90a8..1dc9b6ae8 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewDao.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewDao.kt @@ -1,15 +1,15 @@ -package fr.free.nrw.commons.review; +package fr.free.nrw.commons.review -import androidx.room.Dao; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query /** * Dao interface for reviewed images database */ @Dao -public interface ReviewDao { +interface ReviewDao { /** * Inserts reviewed/skipped image identifier into the database @@ -17,7 +17,7 @@ public interface ReviewDao { * @param reviewEntity */ @Insert(onConflict = OnConflictStrategy.IGNORE) - void insert(ReviewEntity reviewEntity); + fun insert(reviewEntity: ReviewEntity) /** * Checks if the image has already been reviewed/skipped by the user @@ -26,7 +26,6 @@ public interface ReviewDao { * @param imageId * @return */ - @Query( "SELECT EXISTS (SELECT * from `reviewed-images` where imageId = (:imageId))") - Boolean isReviewedAlready(String imageId); - -} \ No newline at end of file + @Query("SELECT EXISTS (SELECT * from `reviewed-images` where imageId = (:imageId))") + fun isReviewedAlready(imageId: String): Boolean +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.java deleted file mode 100644 index 071111b15..000000000 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.java +++ /dev/null @@ -1,19 +0,0 @@ -package fr.free.nrw.commons.review; - -import androidx.annotation.NonNull; -import androidx.room.Entity; -import androidx.room.PrimaryKey; - -/** - * Entity to store reviewed/skipped images identifier - */ -@Entity(tableName = "reviewed-images") -public class ReviewEntity { - @PrimaryKey - @NonNull - String imageId; - - public ReviewEntity(String imageId) { - this.imageId = imageId; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.kt new file mode 100644 index 000000000..473c143c7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.kt @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.review + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Entity to store reviewed/skipped images identifier + */ +@Entity(tableName = "reviewed-images") +data class ReviewEntity( + @PrimaryKey + val imageId: String +) diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.kt index 8a77c11ed..17296a5c8 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.kt @@ -77,7 +77,7 @@ class ReviewHelper * @param image * @return */ - fun getReviewStatus(image: String?): Boolean = dao?.isReviewedAlready(image) ?: false + fun getReviewStatus(image: String?): Boolean = image?.let { dao?.isReviewedAlready(it) } ?: false /** * Gets the first revision of the file from filename @@ -132,7 +132,7 @@ class ReviewHelper */ fun addViewedImagesToDB(imageId: String?) { Completable - .fromAction { dao!!.insert(ReviewEntity(imageId)) } + .fromAction { imageId?.let { ReviewEntity(it) }?.let { dao!!.insert(it) } } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java deleted file mode 100644 index 7e0cd0ee3..000000000 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java +++ /dev/null @@ -1,262 +0,0 @@ -package fr.free.nrw.commons.review; - -import android.graphics.Color; -import android.os.Bundle; -import android.text.Html; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; -import fr.free.nrw.commons.databinding.FragmentReviewImageBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import java.util.ArrayList; -import java.util.List; -import javax.inject.Inject; - -public class ReviewImageFragment extends CommonsDaggerSupportFragment { - - static final int CATEGORY = 2; - private static final int SPAM = 0; - private static final int COPYRIGHT = 1; - private static final int THANKS = 3; - - private int position; - - private FragmentReviewImageBinding binding; - - @Inject - SessionManager sessionManager; - - - // Constant variable used to store user's key name for onSaveInstanceState method - private final String SAVED_USER = "saved_user"; - - // Variable that stores the value of user - private String user; - - public void update(final int position) { - this.position = position; - } - - private String updateCategoriesQuestion() { - final Media media = getReviewActivity().getMedia(); - if (media != null && media.getCategoriesHiddenStatus() != null && isAdded()) { - // Filter category name attribute from all categories - final List categories = new ArrayList<>(); - for(final String key : media.getCategoriesHiddenStatus().keySet()) { - String value = String.valueOf(key); - // Each category returned has a format like "Category:" - // so remove the prefix "Category:" - final int index = key.indexOf("Category:"); - if(index == 0) { - value = key.substring(9); - } - categories.add(value); - } - String catString = TextUtils.join(", ", categories); - if (catString != null && !catString.equals("") && binding.tvReviewQuestionContext != null) { - catString = "" + catString + ""; - final String stringToConvertHtml = String.format(getResources().getString(R.string.review_category_explanation), catString); - return Html.fromHtml(stringToConvertHtml).toString(); - } - } - return getResources().getString(R.string.review_no_category); - } - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - position = getArguments().getInt("position"); - binding = FragmentReviewImageBinding.inflate(inflater, container, false); - - final String question; - String explanation=null; - String yesButtonText; - final String noButtonText; - - binding.buttonYes.setOnClickListener(view -> onYesButtonClicked()); - - switch (position) { - case SPAM: - question = getString(R.string.review_spam); - explanation = getString(R.string.review_spam_explanation); - yesButtonText = getString(R.string.yes); - noButtonText = getString(R.string.no); - binding.buttonNo.setOnClickListener(view -> getReviewActivity() - .reviewController.reportSpam(requireActivity(), getReviewCallback())); - break; - case COPYRIGHT: - enableButtons(); - question = getString(R.string.review_copyright); - explanation = getString(R.string.review_copyright_explanation); - yesButtonText = getString(R.string.yes); - noButtonText = getString(R.string.no); - binding.buttonNo.setOnClickListener(view -> getReviewActivity() - .reviewController - .reportPossibleCopyRightViolation(requireActivity(), getReviewCallback())); - break; - case CATEGORY: - enableButtons(); - question = getString(R.string.review_category); - explanation = updateCategoriesQuestion(); - yesButtonText = getString(R.string.yes); - noButtonText = getString(R.string.no); - binding.buttonNo.setOnClickListener(view -> { - getReviewActivity() - .reviewController - .reportWrongCategory(requireActivity(), getReviewCallback()); - getReviewActivity().swipeToNext(); - }); - break; - case THANKS: - enableButtons(); - question = getString(R.string.review_thanks); - - if (getReviewActivity().reviewController.firstRevision != null) { - user = getReviewActivity().reviewController.firstRevision.getUser(); - } else { - if(savedInstanceState != null) { - user = savedInstanceState.getString(SAVED_USER); - } - } - - //if the user is null because of whatsoever reason, review will not be sent anyways - if (!TextUtils.isEmpty(user)) { - explanation = getString(R.string.review_thanks_explanation, user); - } - - // Note that the yes and no buttons are swapped in this section - yesButtonText = getString(R.string.review_thanks_yes_button_text); - noButtonText = getString(R.string.review_thanks_no_button_text); - binding.buttonYes.setTextColor(Color.parseColor("#116aaa")); - binding.buttonNo.setTextColor(Color.parseColor("#228b22")); - binding.buttonNo.setOnClickListener(view -> { - getReviewActivity().reviewController.sendThanks(getReviewActivity()); - getReviewActivity().swipeToNext(); - }); - break; - default: - enableButtons(); - question = "How did we get here?"; - explanation = "No idea."; - yesButtonText = "yes"; - noButtonText = "no"; - } - - binding.tvReviewQuestion.setText(question); - binding.tvReviewQuestionContext.setText(explanation); - binding.buttonYes.setText(yesButtonText); - binding.buttonNo.setText(noButtonText); - return binding.getRoot(); - } - - - /** - * This method will be called when configuration changes happen - * - * @param outState - */ - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - - //Save user name when configuration changes happen - outState.putString(SAVED_USER, user); - } - - private ReviewController.ReviewCallback getReviewCallback() { - return new ReviewController - .ReviewCallback() { - @Override - public void onSuccess() { - getReviewActivity().runRandomizer(); - } - - @Override - public void onFailure() { - //do nothing - } - - @Override - public void onTokenException(final Exception e) { - if (e instanceof InvalidLoginTokenException){ - final String username = sessionManager.getUserName(); - final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( - getActivity(), - requireActivity().getString(R.string.invalid_login_message), - username - ); - - CommonsApplication.getInstance().clearApplicationData( - requireActivity(), logoutListener); - - } - } - - /** - * This function is called when an image is being loaded - * to disable the review buttons - */ - @Override - public void disableButtons() { - ReviewImageFragment.this.disableButtons(); - } - - /** - * This function is called when an image has - * been loaded to enable the review buttons. - */ - @Override - public void enableButtons() { - ReviewImageFragment.this.enableButtons(); - } - }; - } - - /** - * This function is called when an image has - * been loaded to enable the review buttons. - */ - public void enableButtons() { - binding.buttonYes.setEnabled(true); - binding.buttonYes.setAlpha(1); - binding.buttonNo.setEnabled(true); - binding.buttonNo.setAlpha(1); - } - - /** - * This function is called when an image is being loaded - * to disable the review buttons - */ - public void disableButtons() { - binding.buttonYes.setEnabled(false); - binding.buttonYes.setAlpha(0.5f); - binding.buttonNo.setEnabled(false); - binding.buttonNo.setAlpha(0.5f); - } - - void onYesButtonClicked() { - getReviewActivity().swipeToNext(); - } - - private ReviewActivity getReviewActivity() { - return (ReviewActivity) requireActivity(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.kt new file mode 100644 index 000000000..691c61f56 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.kt @@ -0,0 +1,251 @@ +package fr.free.nrw.commons.review + +import android.graphics.Color +import android.os.Bundle +import android.text.Html +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException +import fr.free.nrw.commons.databinding.FragmentReviewImageBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import java.util.ArrayList +import javax.inject.Inject + + +class ReviewImageFragment : CommonsDaggerSupportFragment() { + + companion object { + const val CATEGORY = 2 + private const val SPAM = 0 + private const val COPYRIGHT = 1 + private const val THANKS = 3 + } + + private var position: Int = 0 + private var binding: FragmentReviewImageBinding? = null + + @Inject + lateinit var sessionManager: SessionManager + + // Constant variable used to store user's key name for onSaveInstanceState method + private val SAVED_USER = "saved_user" + + // Variable that stores the value of user + private var user: String? = null + + fun update(position: Int) { + this.position = position + } + + private fun updateCategoriesQuestion(): String { + val media = reviewActivity.media + if (media?.categoriesHiddenStatus != null && isAdded) { + // Filter category name attribute from all categories + val categories = media.categoriesHiddenStatus.keys.map { key -> + var value = key + // Each category returned has a format like "Category:" + // so remove the prefix "Category:" + if (key.startsWith("Category:")) { + value = key.substring(9) + } + value + } + + val catString = categories.joinToString(", ") + if (catString.isNotEmpty() && binding?.tvReviewQuestionContext != null) { + val formattedCatString = "$catString" + val stringToConvertHtml = getString( + R.string.review_category_explanation, + formattedCatString + ) + return Html.fromHtml(stringToConvertHtml).toString() + } + } + return getString(R.string.review_no_category) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + position = requireArguments().getInt("position") + binding = FragmentReviewImageBinding.inflate(inflater, container, false) + + val question: String + var explanation: String? = null + val yesButtonText: String + val noButtonText: String + + binding?.buttonYes?.setOnClickListener { onYesButtonClicked() } + + when (position) { + SPAM -> { + question = getString(R.string.review_spam) + explanation = getString(R.string.review_spam_explanation) + yesButtonText = getString(R.string.yes) + noButtonText = getString(R.string.no) + binding?.buttonNo?.setOnClickListener { + reviewActivity.reviewController.reportSpam(requireActivity(), reviewCallback) + } + } + COPYRIGHT -> { + enableButtons() + question = getString(R.string.review_copyright) + explanation = getString(R.string.review_copyright_explanation) + yesButtonText = getString(R.string.yes) + noButtonText = getString(R.string.no) + binding?.buttonNo?.setOnClickListener { + reviewActivity.reviewController.reportPossibleCopyRightViolation( + requireActivity(), + reviewCallback + ) + } + } + CATEGORY -> { + enableButtons() + question = getString(R.string.review_category) + explanation = updateCategoriesQuestion() + yesButtonText = getString(R.string.yes) + noButtonText = getString(R.string.no) + binding?.buttonNo?.setOnClickListener { + reviewActivity.reviewController.reportWrongCategory( + requireActivity(), + reviewCallback + ) + reviewActivity.swipeToNext() + } + } + THANKS -> { + enableButtons() + question = getString(R.string.review_thanks) + + user = reviewActivity.reviewController.firstRevision?.user + ?: savedInstanceState?.getString(SAVED_USER) + + //if the user is null because of whatsoever reason, review will not be sent anyways + if (!user.isNullOrEmpty()) { + explanation = getString(R.string.review_thanks_explanation, user) + } + + // Note that the yes and no buttons are swapped in this section + yesButtonText = getString(R.string.review_thanks_yes_button_text) + noButtonText = getString(R.string.review_thanks_no_button_text) + binding?.buttonYes?.setTextColor(Color.parseColor("#116aaa")) + binding?.buttonNo?.setTextColor(Color.parseColor("#228b22")) + binding?.buttonNo?.setOnClickListener { + reviewActivity.reviewController.sendThanks(requireActivity()) + reviewActivity.swipeToNext() + } + } + else -> { + enableButtons() + question = "How did we get here?" + explanation = "No idea." + yesButtonText = "yes" + noButtonText = "no" + } + } + + binding?.apply { + tvReviewQuestion.text = question + tvReviewQuestionContext.text = explanation + buttonYes.text = yesButtonText + buttonNo.text = noButtonText + } + return binding?.root + } + + /** + * This method will be called when configuration changes happen + * + * @param outState + */ + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + //Save user name when configuration changes happen + outState.putString(SAVED_USER, user) + } + + private val reviewCallback: ReviewController.ReviewCallback + get() = object : ReviewController.ReviewCallback { + override fun onSuccess() { + reviewActivity.runRandomizer() + } + + override fun onFailure() { + //do nothing + } + + override fun onTokenException(e: Exception) { + if (e is InvalidLoginTokenException) { + val username = sessionManager.userName + val logoutListener = activity?.let { + CommonsApplication.BaseLogoutListener( + it, + getString(R.string.invalid_login_message), + username + ) + } + + if (logoutListener != null) { + CommonsApplication.instance.clearApplicationData( + requireActivity(), logoutListener + ) + } + } + } + + override fun disableButtons() { + this@ReviewImageFragment.disableButtons() + } + + override fun enableButtons() { + this@ReviewImageFragment.enableButtons() + } + } + + /** + * This function is called when an image has + * been loaded to enable the review buttons. + */ + fun enableButtons() { + binding?.apply { + buttonYes.isEnabled = true + buttonYes.alpha = 1f + buttonNo.isEnabled = true + buttonNo.alpha = 1f + } + } + + /** + * This function is called when an image is being loaded + * to disable the review buttons + */ + fun disableButtons() { + binding?.apply { + buttonYes.isEnabled = false + buttonYes.alpha = 0.5f + buttonNo.isEnabled = false + buttonNo.alpha = 0.5f + } + } + + fun onYesButtonClicked() { + reviewActivity.swipeToNext() + } + + private val reviewActivity: ReviewActivity + get() = requireActivity() as ReviewActivity + + override fun onDestroy() { + super.onDestroy() + binding = null + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java deleted file mode 100644 index 16b55c6e9..000000000 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java +++ /dev/null @@ -1,53 +0,0 @@ -package fr.free.nrw.commons.review; - -import android.os.Bundle; - -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentStatePagerAdapter; - -public class ReviewPagerAdapter extends FragmentStatePagerAdapter { - private ReviewImageFragment[] reviewImageFragments; - - /** - * this function return the instance of ReviewviewPage current item - */ - @Override - public Object instantiateItem(@NonNull ViewGroup container, int position) { - return super.instantiateItem(container, position); - } - - ReviewPagerAdapter(FragmentManager fm) { - super(fm); - reviewImageFragments = new ReviewImageFragment[]{ - new ReviewImageFragment(), - new ReviewImageFragment(), - new ReviewImageFragment(), - new ReviewImageFragment() - }; - } - - @Override - public int getCount() { - return reviewImageFragments.length; - } - - void updateFileInformation() { - for (int i = 0; i < getCount(); i++) { - ReviewImageFragment fragment = reviewImageFragments[i]; - fragment.update(i); - } - } - - - @Override - public Fragment getItem(int position) { - Bundle bundle = new Bundle(); - bundle.putInt("position", position); - reviewImageFragments[position].setArguments(bundle); - return reviewImageFragments[position]; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.kt new file mode 100644 index 000000000..9bbe14e65 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.kt @@ -0,0 +1,37 @@ +package fr.free.nrw.commons.review + +import android.os.Bundle + +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter + + +class ReviewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) { + private val reviewImageFragments: Array = arrayOf( + ReviewImageFragment(), + ReviewImageFragment(), + ReviewImageFragment(), + ReviewImageFragment() + ) + + override fun getCount(): Int { + return reviewImageFragments.size + } + + fun updateFileInformation() { + for (i in 0 until count) { + val fragment = reviewImageFragments[i] + fragment.update(i) + } + } + + override fun getItem(position: Int): Fragment { + val bundle = Bundle().apply { + putInt("position", position) + } + reviewImageFragments[position].arguments = bundle + return reviewImageFragments[position] + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.java deleted file mode 100644 index 95740aac0..000000000 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.java +++ /dev/null @@ -1,30 +0,0 @@ -package fr.free.nrw.commons.review; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; - -import androidx.viewpager.widget.ViewPager; - -public class ReviewViewPager extends ViewPager { - - public ReviewViewPager(Context context) { - super(context); - } - - public ReviewViewPager(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - // Never allow swiping to switch between pages - return false; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - // Never allow swiping to switch between pages - return false; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.kt new file mode 100644 index 000000000..39de49189 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.review + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent + +import androidx.viewpager.widget.ViewPager + +class ReviewViewPager @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : ViewPager(context, attrs) { + + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + // Never allow swiping to switch between pages + return false + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + // Never allow swiping to switch between pages + return false + } +} From 00cfd835213b4f2216126ccbac762ae8506415b6 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Sun, 24 Nov 2024 15:47:05 +0530 Subject: [PATCH 38/74] Migrated quiz module from Java to Kotlin (#5952) * Rename .java to .kt * Migrated quiz module to Kotlin * unit test failing fixed * unit test failing fixed --- .../free/nrw/commons/quiz/QuizActivity.java | 146 ------------- .../fr/free/nrw/commons/quiz/QuizActivity.kt | 154 ++++++++++++++ .../fr/free/nrw/commons/quiz/QuizChecker.java | 167 --------------- .../fr/free/nrw/commons/quiz/QuizChecker.kt | 175 ++++++++++++++++ .../free/nrw/commons/quiz/QuizController.java | 63 ------ .../free/nrw/commons/quiz/QuizController.kt | 76 +++++++ .../nrw/commons/quiz/QuizResultActivity.java | 188 ----------------- .../nrw/commons/quiz/QuizResultActivity.kt | 192 ++++++++++++++++++ .../nrw/commons/quiz/RadioGroupHelper.java | 64 ------ .../free/nrw/commons/quiz/RadioGroupHelper.kt | 61 ++++++ 10 files changed, 658 insertions(+), 628 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java create mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.kt diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java deleted file mode 100644 index 8c087b17b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java +++ /dev/null @@ -1,146 +0,0 @@ -package fr.free.nrw.commons.quiz; - -import android.content.Intent; -import android.os.Bundle; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; - -import com.facebook.drawee.drawable.ProgressBarDrawable; -import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; - -import fr.free.nrw.commons.databinding.ActivityQuizBinding; -import java.util.ArrayList; - -import fr.free.nrw.commons.R; - -public class QuizActivity extends AppCompatActivity { - - private ActivityQuizBinding binding; - private final QuizController quizController = new QuizController(); - private ArrayList quiz = new ArrayList<>(); - private int questionIndex = 0; - private int score; - /** - * isPositiveAnswerChecked : represents yes click event - */ - private boolean isPositiveAnswerChecked; - /** - * isNegativeAnswerChecked : represents no click event - */ - private boolean isNegativeAnswerChecked; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivityQuizBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - quizController.initialize(this); - setSupportActionBar(binding.toolbar.toolbar); - binding.nextButton.setOnClickListener(view -> notKnowAnswer()); - displayQuestion(); - } - - /** - * to move to next question and check whether answer is selected or not - */ - public void setNextQuestion(){ - if ( questionIndex <= quiz.size() && (isPositiveAnswerChecked || isNegativeAnswerChecked)) { - evaluateScore(); - } - } - - public void notKnowAnswer(){ - customAlert("Information", quiz.get(questionIndex).getAnswerMessage()); - } - - /** - * to give warning before ending quiz - */ - @Override - public void onBackPressed() { - new AlertDialog.Builder(this) - .setTitle(getResources().getString(R.string.warning)) - .setMessage(getResources().getString(R.string.quiz_back_button)) - .setPositiveButton(R.string.continue_message, (dialog, which) -> { - final Intent intent = new Intent(this, QuizResultActivity.class); - dialog.dismiss(); - intent.putExtra("QuizResult", score); - startActivity(intent); - }) - .setNegativeButton("Cancel", (dialogInterface, i) -> dialogInterface.dismiss()) - .create() - .show(); - } - - /** - * to display the question - */ - public void displayQuestion() { - quiz = quizController.getQuiz(); - binding.question.questionText.setText(quiz.get(questionIndex).getQuestion()); - binding.questionTitle.setText( - getResources().getString(R.string.question) + - quiz.get(questionIndex).getQuestionNumber() - ); - binding.question.questionImage.setHierarchy(GenericDraweeHierarchyBuilder - .newInstance(getResources()) - .setFailureImage(VectorDrawableCompat.create(getResources(), - R.drawable.ic_error_outline_black_24dp, getTheme())) - .setProgressBarImage(new ProgressBarDrawable()) - .build()); - - binding.question.questionImage.setImageURI(quiz.get(questionIndex).getUrl()); - isPositiveAnswerChecked = false; - isNegativeAnswerChecked = false; - binding.answer.quizPositiveAnswer.setOnClickListener(view -> { - isPositiveAnswerChecked = true; - setNextQuestion(); - }); - binding.answer.quizNegativeAnswer.setOnClickListener(view -> { - isNegativeAnswerChecked = true; - setNextQuestion(); - }); - } - - /** - * to evaluate score and check whether answer is correct or wrong - */ - public void evaluateScore() { - if ((quiz.get(questionIndex).isAnswer() && isPositiveAnswerChecked) || - (!quiz.get(questionIndex).isAnswer() && isNegativeAnswerChecked) ){ - customAlert(getResources().getString(R.string.correct), - quiz.get(questionIndex).getAnswerMessage()); - score++; - } else { - customAlert(getResources().getString(R.string.wrong), - quiz.get(questionIndex).getAnswerMessage()); - } - } - - /** - * to display explanation after each answer, update questionIndex and move to next question - * @param title the alert title - * @param Message the alert message - */ - public void customAlert(final String title, final String Message) { - new AlertDialog.Builder(this) - .setTitle(title) - .setMessage(Message) - .setPositiveButton(R.string.continue_message, (dialog, which) -> { - questionIndex++; - if (questionIndex == quiz.size()) { - final Intent intent = new Intent(this, QuizResultActivity.class); - dialog.dismiss(); - intent.putExtra("QuizResult", score); - startActivity(intent); - } else { - displayQuestion(); - } - }) - .create() - .show(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt new file mode 100644 index 000000000..a243c2637 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt @@ -0,0 +1,154 @@ +package fr.free.nrw.commons.quiz + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle + +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat + +import com.facebook.drawee.drawable.ProgressBarDrawable +import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder + +import fr.free.nrw.commons.databinding.ActivityQuizBinding +import java.util.ArrayList + +import fr.free.nrw.commons.R + + +class QuizActivity : AppCompatActivity() { + + private lateinit var binding: ActivityQuizBinding + private val quizController = QuizController() + private var quiz = ArrayList() + private var questionIndex = 0 + private var score = 0 + + /** + * isPositiveAnswerChecked : represents yes click event + */ + private var isPositiveAnswerChecked = false + + /** + * isNegativeAnswerChecked : represents no click event + */ + private var isNegativeAnswerChecked = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityQuizBinding.inflate(layoutInflater) + setContentView(binding.root) + + quizController.initialize(this) + setSupportActionBar(binding.toolbar.toolbar) + binding.nextButton.setOnClickListener { notKnowAnswer() } + displayQuestion() + } + + /** + * To move to next question and check whether answer is selected or not + */ + fun setNextQuestion() { + if (questionIndex <= quiz.size && (isPositiveAnswerChecked || isNegativeAnswerChecked)) { + evaluateScore() + } + } + + private fun notKnowAnswer() { + customAlert("Information", quiz[questionIndex].answerMessage) + } + + /** + * To give warning before ending quiz + */ + override fun onBackPressed() { + AlertDialog.Builder(this) + .setTitle(getString(R.string.warning)) + .setMessage(getString(R.string.quiz_back_button)) + .setPositiveButton(R.string.continue_message) { dialog, _ -> + val intent = Intent(this, QuizResultActivity::class.java) + dialog.dismiss() + intent.putExtra("QuizResult", score) + startActivity(intent) + } + .setNegativeButton("Cancel") { dialogInterface, _ -> dialogInterface.dismiss() } + .create() + .show() + } + + /** + * To display the question + */ + @SuppressLint("SetTextI18n") + private fun displayQuestion() { + quiz = quizController.getQuiz() + binding.question.questionText.text = quiz[questionIndex].question + binding.questionTitle.text = getString(R.string.question) + quiz[questionIndex].questionNumber + + binding.question.questionImage.hierarchy = GenericDraweeHierarchyBuilder + .newInstance(resources) + .setFailureImage(VectorDrawableCompat.create(resources, R.drawable.ic_error_outline_black_24dp, theme)) + .setProgressBarImage(ProgressBarDrawable()) + .build() + + binding.question.questionImage.setImageURI(quiz[questionIndex].getUrl()) + isPositiveAnswerChecked = false + isNegativeAnswerChecked = false + + binding.answer.quizPositiveAnswer.setOnClickListener { + isPositiveAnswerChecked = true + setNextQuestion() + } + binding.answer.quizNegativeAnswer.setOnClickListener { + isNegativeAnswerChecked = true + setNextQuestion() + } + } + + /** + * To evaluate score and check whether answer is correct or wrong + */ + fun evaluateScore() { + if ( + (quiz[questionIndex].isAnswer && isPositiveAnswerChecked) + || + (!quiz[questionIndex].isAnswer && isNegativeAnswerChecked) + ) { + customAlert( + getString(R.string.correct), + quiz[questionIndex].answerMessage + ) + score++ + } else { + customAlert( + getString(R.string.wrong), + quiz[questionIndex].answerMessage + ) + } + } + + /** + * To display explanation after each answer, update questionIndex and move to next question + * @param title The alert title + * @param message The alert message + */ + fun customAlert(title: String, message: String) { + AlertDialog.Builder(this) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.continue_message) { dialog, _ -> + questionIndex++ + if (questionIndex == quiz.size) { + val intent = Intent(this, QuizResultActivity::class.java) + dialog.dismiss() + intent.putExtra("QuizResult", score) + startActivity(intent) + } else { + displayQuestion() + } + } + .create() + .show() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java deleted file mode 100644 index 201c5bfc6..000000000 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java +++ /dev/null @@ -1,167 +0,0 @@ -package fr.free.nrw.commons.quiz; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Intent; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.WelcomeActivity; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import fr.free.nrw.commons.utils.DialogUtil; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; - -/** - * fetches the number of images uploaded and number of images reverted. - * Then it calculates the percentage of the images reverted - * if the percentage of images reverted after last quiz exceeds 50% and number of images uploaded is - * greater than 50, then quiz is popped up - */ -@Singleton -public class QuizChecker { - - private int revertCount ; - private int totalUploadCount ; - private boolean isRevertCountFetched; - private boolean isUploadCountFetched; - - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - - private final SessionManager sessionManager; - private final OkHttpJsonApiClient okHttpJsonApiClient; - private final JsonKvStore revertKvStore; - - private static final int UPLOAD_COUNT_THRESHOLD = 5; - private static final String REVERT_PERCENTAGE_FOR_MESSAGE = "50%"; - private final String REVERT_SHARED_PREFERENCE = "revertCount"; - private final String UPLOAD_SHARED_PREFERENCE = "uploadCount"; - - /** - * constructor to set the parameters for quiz - * @param sessionManager - * @param okHttpJsonApiClient - */ - @Inject - public QuizChecker(SessionManager sessionManager, - OkHttpJsonApiClient okHttpJsonApiClient, - @Named("default_preferences") JsonKvStore revertKvStore) { - this.sessionManager = sessionManager; - this.okHttpJsonApiClient = okHttpJsonApiClient; - this.revertKvStore = revertKvStore; - } - - public void initQuizCheck(Activity activity) { - calculateRevertParameterAndShowQuiz(activity); - } - - public void cleanup() { - compositeDisposable.clear(); - } - - /** - * to fet the total number of images uploaded - */ - private void setUploadCount() { - compositeDisposable.add(okHttpJsonApiClient - .getUploadCount(sessionManager.getUserName()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::setTotalUploadCount, - t -> Timber.e(t, "Fetching upload count failed") - )); - } - - /** - * set the sub Title of Contibutions Activity and - * call function to check for quiz - * @param uploadCount user's upload count - */ - private void setTotalUploadCount(int uploadCount) { - totalUploadCount = uploadCount - revertKvStore.getInt(UPLOAD_SHARED_PREFERENCE, 0); - if ( totalUploadCount < 0){ - totalUploadCount = 0; - revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0); - } - isUploadCountFetched = true; - } - - /** - * To call the API to get reverts count in form of JSONObject - */ - private void setRevertCount() { - compositeDisposable.add(okHttpJsonApiClient - .getAchievements(sessionManager.getUserName()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null) { - setRevertParameter(response.getDeletedUploads()); - } - }, throwable -> Timber.e(throwable, "Fetching feedback failed")) - ); - } - - /** - * to calculate the number of images reverted after previous quiz - * @param revertCountFetched count of deleted uploads - */ - private void setRevertParameter(int revertCountFetched) { - revertCount = revertCountFetched - revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0); - if (revertCount < 0){ - revertCount = 0; - revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0); - } - isRevertCountFetched = true; - } - - /** - * to check whether the criterion to call quiz is satisfied - */ - private void calculateRevertParameterAndShowQuiz(Activity activity) { - setUploadCount(); - setRevertCount(); - if ( revertCount < 0 || totalUploadCount < 0){ - revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0); - revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0); - return; - } - if (isRevertCountFetched && isUploadCountFetched && - totalUploadCount >= UPLOAD_COUNT_THRESHOLD && - (revertCount * 100) / totalUploadCount >= 50) { - callQuiz(activity); - } - } - - /** - * Alert which prompts to quiz - */ - @SuppressLint("StringFormatInvalid") - private void callQuiz(Activity activity) { - DialogUtil.showAlertDialog(activity, - activity.getString(R.string.quiz), - activity.getString(R.string.quiz_alert_message, REVERT_PERCENTAGE_FOR_MESSAGE), - activity.getString(R.string.about_translate_proceed), - activity.getString(android.R.string.cancel), - () -> startQuizActivity(activity), - null); - } - - private void startQuizActivity(Activity activity) { - int newRevetSharedPrefs = revertCount + revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0); - revertKvStore.putInt(REVERT_SHARED_PREFERENCE, newRevetSharedPrefs); - int newUploadCount = totalUploadCount + revertKvStore.getInt(UPLOAD_SHARED_PREFERENCE, 0); - revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, newUploadCount); - Intent i = new Intent(activity, WelcomeActivity.class); - i.putExtra("isQuiz", true); - activity.startActivity(i); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt new file mode 100644 index 000000000..ec74ecf6f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt @@ -0,0 +1,175 @@ +package fr.free.nrw.commons.quiz + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent + +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +import fr.free.nrw.commons.R +import fr.free.nrw.commons.WelcomeActivity +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.utils.DialogUtil +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + + +/** + * Fetches the number of images uploaded and number of images reverted. + * Then it calculates the percentage of the images reverted. + * If the percentage of images reverted after the last quiz exceeds 50% and number of images uploaded is + * greater than 50, then the quiz is popped up. + */ +@Singleton +class QuizChecker @Inject constructor( + private val sessionManager: SessionManager, + private val okHttpJsonApiClient: OkHttpJsonApiClient, + @Named("default_preferences") private val revertKvStore: JsonKvStore +) { + + private var revertCount = 0 + private var totalUploadCount = 0 + private var isRevertCountFetched = false + private var isUploadCountFetched = false + + private val compositeDisposable = CompositeDisposable() + + private val UPLOAD_COUNT_THRESHOLD = 5 + private val REVERT_PERCENTAGE_FOR_MESSAGE = "50%" + private val REVERT_SHARED_PREFERENCE = "revertCount" + private val UPLOAD_SHARED_PREFERENCE = "uploadCount" + + /** + * Initializes quiz check by calculating revert parameters and showing quiz if necessary + */ + fun initQuizCheck(activity: Activity) { + calculateRevertParameterAndShowQuiz(activity) + } + + /** + * Clears disposables to avoid memory leaks + */ + fun cleanup() { + compositeDisposable.clear() + } + + /** + * Fetches the total number of images uploaded + */ + private fun setUploadCount() { + compositeDisposable.add( + okHttpJsonApiClient.getUploadCount(sessionManager.userName) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { uploadCount -> setTotalUploadCount(uploadCount) }, + { t -> Timber.e(t, "Fetching upload count failed") } + ) + ) + } + + /** + * Sets the total upload count after subtracting stored preference + * @param uploadCount User's upload count + */ + private fun setTotalUploadCount(uploadCount: Int) { + totalUploadCount = uploadCount - revertKvStore.getInt( + UPLOAD_SHARED_PREFERENCE, + 0 + ) + if (totalUploadCount < 0) { + totalUploadCount = 0 + revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0) + } + isUploadCountFetched = true + } + + /** + * Fetches the revert count using the API + */ + private fun setRevertCount() { + compositeDisposable.add( + okHttpJsonApiClient.getAchievements(sessionManager.userName) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response -> + response?.let { setRevertParameter(it.deletedUploads) } + }, + { throwable -> Timber.e(throwable, "Fetching feedback failed") } + ) + ) + } + + /** + * Calculates the number of images reverted after the previous quiz + * @param revertCountFetched Count of deleted uploads + */ + private fun setRevertParameter(revertCountFetched: Int) { + revertCount = revertCountFetched - revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0) + if (revertCount < 0) { + revertCount = 0 + revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0) + } + isRevertCountFetched = true + } + + /** + * Checks whether the criteria for calling the quiz are satisfied + */ + private fun calculateRevertParameterAndShowQuiz(activity: Activity) { + setUploadCount() + setRevertCount() + + if (revertCount < 0 || totalUploadCount < 0) { + revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0) + revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0) + return + } + + if (isRevertCountFetched && isUploadCountFetched && + totalUploadCount >= UPLOAD_COUNT_THRESHOLD && + (revertCount * 100) / totalUploadCount >= 50 + ) { + callQuiz(activity) + } + } + + /** + * Displays an alert prompting the user to take the quiz + */ + @SuppressLint("StringFormatInvalid") + private fun callQuiz(activity: Activity) { + DialogUtil.showAlertDialog( + activity, + activity.getString(R.string.quiz), + activity.getString(R.string.quiz_alert_message, REVERT_PERCENTAGE_FOR_MESSAGE), + activity.getString(R.string.about_translate_proceed), + activity.getString(android.R.string.cancel), + { startQuizActivity(activity) }, + null + ) + } + + /** + * Starts the quiz activity and updates preferences for revert and upload counts + */ + private fun startQuizActivity(activity: Activity) { + val newRevertSharedPrefs = revertCount + revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0) + revertKvStore.putInt(REVERT_SHARED_PREFERENCE, newRevertSharedPrefs) + + val newUploadCount = totalUploadCount + revertKvStore.getInt(UPLOAD_SHARED_PREFERENCE, 0) + revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, newUploadCount) + + val intent = Intent(activity, WelcomeActivity::class.java).apply { + putExtra("isQuiz", true) + } + activity.startActivity(intent) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.java deleted file mode 100644 index a7b2c94ef..000000000 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.java +++ /dev/null @@ -1,63 +0,0 @@ -package fr.free.nrw.commons.quiz; - -import android.content.Context; - -import java.util.ArrayList; - -import fr.free.nrw.commons.R; - -/** - * controls the quiz in the Activity - */ -public class QuizController { - - ArrayList quiz = new ArrayList<>(); - - private final String URL_FOR_SELFIE = "https://i.imgur.com/0fMYcpM.jpg"; - private final String URL_FOR_TAJ_MAHAL = "https://upload.wikimedia.org/wikipedia/commons/1/15/Taj_Mahal-03.jpg"; - private final String URL_FOR_BLURRY_IMAGE = "https://i.imgur.com/Kepb5jR.jpg"; - private final String URL_FOR_SCREENSHOT = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Social_media_app_mockup_screenshot.svg/500px-Social_media_app_mockup_screenshot.svg.png"; - private final String URL_FOR_EVENT = "https://upload.wikimedia.org/wikipedia/commons/5/51/HouseBuildingInNorthernVietnam.jpg"; - - public void initialize(Context context){ - QuizQuestion q1 = new QuizQuestion(1, - context.getString(R.string.quiz_question_string), - URL_FOR_SELFIE, - false, - context.getString(R.string.selfie_answer)); - quiz.add(q1); - - QuizQuestion q2 = new QuizQuestion(2, - context.getString(R.string.quiz_question_string), - URL_FOR_TAJ_MAHAL, - true, - context.getString(R.string.taj_mahal_answer)); - quiz.add(q2); - - QuizQuestion q3 = new QuizQuestion(3, - context.getString(R.string.quiz_question_string), - URL_FOR_BLURRY_IMAGE, - false, - context.getString(R.string.blurry_image_answer)); - quiz.add(q3); - - QuizQuestion q4 = new QuizQuestion(4, - context.getString(R.string.quiz_screenshot_question), - URL_FOR_SCREENSHOT, - false, - context.getString(R.string.screenshot_answer)); - quiz.add(q4); - - QuizQuestion q5 = new QuizQuestion(5, - context.getString(R.string.quiz_question_string), - URL_FOR_EVENT, - true, - context.getString(R.string.construction_event_answer)); - quiz.add(q5); - - } - - public ArrayList getQuiz() { - return quiz; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt new file mode 100644 index 000000000..3cb4f52a6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt @@ -0,0 +1,76 @@ +package fr.free.nrw.commons.quiz + +import android.content.Context + +import java.util.ArrayList + +import fr.free.nrw.commons.R + + +/** + * Controls the quiz in the Activity + */ +class QuizController { + + private val quiz: ArrayList = ArrayList() + + private val URL_FOR_SELFIE = "https://i.imgur.com/0fMYcpM.jpg" + private val URL_FOR_TAJ_MAHAL = "https://upload.wikimedia.org/wikipedia/commons/1/15/Taj_Mahal-03.jpg" + private val URL_FOR_BLURRY_IMAGE = "https://i.imgur.com/Kepb5jR.jpg" + private val URL_FOR_SCREENSHOT = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Social_media_app_mockup_screenshot.svg/500px-Social_media_app_mockup_screenshot.svg.png" + private val URL_FOR_EVENT = "https://upload.wikimedia.org/wikipedia/commons/5/51/HouseBuildingInNorthernVietnam.jpg" + + fun initialize(context: Context) { + val q1 = QuizQuestion( + 1, + context.getString(R.string.quiz_question_string), + URL_FOR_SELFIE, + false, + context.getString(R.string.selfie_answer) + ) + quiz.add(q1) + + val q2 = QuizQuestion( + 2, + context.getString(R.string.quiz_question_string), + URL_FOR_TAJ_MAHAL, + true, + context.getString(R.string.taj_mahal_answer) + ) + quiz.add(q2) + + val q3 = QuizQuestion( + 3, + context.getString(R.string.quiz_question_string), + URL_FOR_BLURRY_IMAGE, + false, + context.getString(R.string.blurry_image_answer) + ) + quiz.add(q3) + + val q4 = QuizQuestion( + 4, + context.getString(R.string.quiz_screenshot_question), + URL_FOR_SCREENSHOT, + false, + context.getString(R.string.screenshot_answer) + ) + quiz.add(q4) + + val q5 = QuizQuestion( + 5, + context.getString(R.string.quiz_question_string), + URL_FOR_EVENT, + true, + context.getString(R.string.construction_event_answer) + ) + quiz.add(q5) + } + + fun getQuiz(): ArrayList { + return quiz + } +} + + + diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java deleted file mode 100644 index ec6d1070d..000000000 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java +++ /dev/null @@ -1,188 +0,0 @@ -package fr.free.nrw.commons.quiz; - -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; - -import fr.free.nrw.commons.databinding.ActivityQuizResultBinding; -import java.io.File; -import java.io.FileOutputStream; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.MainActivity; - - -/** - * Displays the final score of quiz and congratulates the user - */ -public class QuizResultActivity extends AppCompatActivity { - - private ActivityQuizResultBinding binding; - private final int NUMBER_OF_QUESTIONS = 5; - private final int MULTIPLIER_TO_GET_PERCENTAGE = 20; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivityQuizResultBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - setSupportActionBar(binding.toolbar.toolbar); - - binding.quizResultNext.setOnClickListener(view -> launchContributionActivity()); - - if ( getIntent() != null) { - Bundle extras = getIntent().getExtras(); - int score = extras.getInt("QuizResult"); - setScore(score); - }else{ - startActivityWithFlags( - this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, - Intent.FLAG_ACTIVITY_SINGLE_TOP); - super.onBackPressed(); - } - } - - @Override - protected void onDestroy() { - binding = null; - super.onDestroy(); - } - - /** - * to calculate and display percentage and score - * @param score - */ - public void setScore(int score) { - final int scorePercent = score * MULTIPLIER_TO_GET_PERCENTAGE; - binding.resultProgressBar.setProgress(scorePercent); - binding.tvResultProgress.setText(score +" / " + NUMBER_OF_QUESTIONS); - final String message = getResources().getString(R.string.congratulatory_message_quiz,scorePercent + "%"); - binding.congratulatoryMessage.setText(message); - } - - /** - * to go to Contibutions Activity - */ - public void launchContributionActivity(){ - startActivityWithFlags( - this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, - Intent.FLAG_ACTIVITY_SINGLE_TOP); - } - - @Override - public void onBackPressed() { - startActivityWithFlags( - this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, - Intent.FLAG_ACTIVITY_SINGLE_TOP); - super.onBackPressed(); - } - - /** - * Function to call intent to an activity - * @param context - * @param cls - * @param flags - * @param - */ - public static void startActivityWithFlags(Context context, Class cls, int... flags) { - Intent intent = new Intent(context, cls); - for (int flag: flags) { - intent.addFlags(flag); - } - context.startActivity(intent); - } - - /** - * to inflate menu - * @param menu - * @return - */ - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_about, menu); - return true; - } - - /** - * if share option selected then take screenshot and launch alert - * @param item - * @return - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - if (id == R.id.share_app_icon) { - View rootView = getWindow().getDecorView().findViewById(android.R.id.content); - Bitmap screenShot = getScreenShot(rootView); - showAlert(screenShot); - } - - return super.onOptionsItemSelected(item); - } - - /** - * to store the screenshot of image in bitmap variable temporarily - * @param view - * @return - */ - public static Bitmap getScreenShot(View view) { - View screenView = view.getRootView(); - screenView.setDrawingCacheEnabled(true); - Bitmap bitmap = Bitmap.createBitmap(screenView.getDrawingCache()); - screenView.setDrawingCacheEnabled(false); - return bitmap; - } - - /** - * share the screenshot through social media - * @param bitmap - */ - void shareScreen(Bitmap bitmap) { - try { - File file = new File(this.getExternalCacheDir(),"screen.png"); - FileOutputStream fOut = new FileOutputStream(file); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut); - fOut.flush(); - fOut.close(); - file.setReadable(true, false); - final Intent intent = new Intent(android.content.Intent.ACTION_SEND); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)); - intent.setType("image/png"); - startActivity(Intent.createChooser(intent, getString(R.string.share_image_via))); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * It display the alertDialog with Image of screenshot - * @param screenshot - */ - public void showAlert(Bitmap screenshot) { - AlertDialog.Builder alertadd = new AlertDialog.Builder(QuizResultActivity.this); - LayoutInflater factory = LayoutInflater.from(QuizResultActivity.this); - final View view = factory.inflate(R.layout.image_alert_layout, null); - ImageView screenShotImage = view.findViewById(R.id.alert_image); - screenShotImage.setImageBitmap(screenshot); - TextView shareMessage = view.findViewById(R.id.alert_text); - shareMessage.setText(R.string.quiz_result_share_message); - alertadd.setView(view); - alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot)); - alertadd.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()); - alertadd.show(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt new file mode 100644 index 000000000..1d4821ee3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt @@ -0,0 +1,192 @@ +package fr.free.nrw.commons.quiz + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.ImageView +import android.widget.TextView + +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity + +import fr.free.nrw.commons.databinding.ActivityQuizResultBinding +import java.io.File +import java.io.FileOutputStream + +import fr.free.nrw.commons.R +import fr.free.nrw.commons.contributions.MainActivity + + +/** + * Displays the final score of quiz and congratulates the user + */ +class QuizResultActivity : AppCompatActivity() { + + private var binding: ActivityQuizResultBinding? = null + private val NUMBER_OF_QUESTIONS = 5 + private val MULTIPLIER_TO_GET_PERCENTAGE = 20 + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityQuizResultBinding.inflate(layoutInflater) + setContentView(binding?.root) + + setSupportActionBar(binding?.toolbar?.toolbar) + + binding?.quizResultNext?.setOnClickListener { + launchContributionActivity() + } + + intent?.extras?.let { extras -> + val score = extras.getInt("QuizResult", 0) + setScore(score) + } ?: run { + startActivityWithFlags( + this, MainActivity::class.java, + Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP + ) + super.onBackPressed() + } + } + + override fun onDestroy() { + binding = null + super.onDestroy() + } + + /** + * To calculate and display percentage and score + * @param score + */ + @SuppressLint("StringFormatInvalid", "SetTextI18n") + fun setScore(score: Int) { + val scorePercent = score * MULTIPLIER_TO_GET_PERCENTAGE + binding?.resultProgressBar?.progress = scorePercent + binding?.tvResultProgress?.text = "$score / $NUMBER_OF_QUESTIONS" + val message = resources.getString(R.string.congratulatory_message_quiz, "$scorePercent%") + binding?.congratulatoryMessage?.text = message + } + + /** + * To go to Contributions Activity + */ + fun launchContributionActivity() { + startActivityWithFlags( + this, MainActivity::class.java, + Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP + ) + } + + override fun onBackPressed() { + startActivityWithFlags( + this, MainActivity::class.java, + Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP + ) + super.onBackPressed() + } + + /** + * Function to call intent to an activity + * @param context + * @param cls + * @param flags + */ + companion object { + fun startActivityWithFlags(context: Context, cls: Class, vararg flags: Int) { + val intent = Intent(context, cls) + flags.forEach { flag -> intent.addFlags(flag) } + context.startActivity(intent) + } + } + + /** + * To inflate menu + * @param menu + * @return + */ + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.menu_about, menu) + return true + } + + /** + * If share option selected then take screenshot and launch alert + * @param item + * @return + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.share_app_icon) { + val rootView = window.decorView.findViewById(android.R.id.content) + val screenShot = getScreenShot(rootView) + showAlert(screenShot) + } + return super.onOptionsItemSelected(item) + } + + /** + * To store the screenshot of image in bitmap variable temporarily + * @param view + * @return + */ + fun getScreenShot(view: View): Bitmap { + val screenView = view.rootView + screenView.isDrawingCacheEnabled = true + val bitmap = Bitmap.createBitmap(screenView.drawingCache) + screenView.isDrawingCacheEnabled = false + return bitmap + } + + /** + * Share the screenshot through social media + * @param bitmap + */ + @SuppressLint("SetWorldReadable") + fun shareScreen(bitmap: Bitmap) { + try { + val file = File(this.externalCacheDir, "screen.png") + FileOutputStream(file).use { fOut -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut) + fOut.flush() + } + file.setReadable(true, false) + val intent = Intent(Intent.ACTION_SEND).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)) + type = "image/png" + } + startActivity(Intent.createChooser(intent, getString(R.string.share_image_via))) + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * It displays the AlertDialog with Image of screenshot + * @param screenshot + */ + fun showAlert(screenshot: Bitmap) { + val alertadd = AlertDialog.Builder(this) + val factory = LayoutInflater.from(this) + val view = factory.inflate(R.layout.image_alert_layout, null) + val screenShotImage = view.findViewById(R.id.alert_image) + screenShotImage.setImageBitmap(screenshot) + val shareMessage = view.findViewById(R.id.alert_text) + shareMessage.setText(R.string.quiz_result_share_message) + alertadd.setView(view) + alertadd.setPositiveButton(R.string.about_translate_proceed) { dialog, _ -> + shareScreen(screenshot) + } + alertadd.setNegativeButton(android.R.string.cancel) { dialog, _ -> + dialog.cancel() + } + alertadd.show() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.java b/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.java deleted file mode 100644 index 79756871d..000000000 --- a/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.java +++ /dev/null @@ -1,64 +0,0 @@ -package fr.free.nrw.commons.quiz; - -import android.app.Activity; -import android.view.View; -import android.widget.CompoundButton; -import android.widget.RadioButton; - -import java.util.ArrayList; -import java.util.List; - -/** - * Used to group to or more radio buttons to ensure - * that at a particular time only one of them is selected - */ -public class RadioGroupHelper { - - public List radioButtons = new ArrayList<>(); - - /** - * Constructor to group radio buttons - * @param radios - */ - public RadioGroupHelper(RadioButton... radios) { - super(); - for (RadioButton rb : radios) { - add(rb); - } - } - - /** - * Constructor to group radio buttons - * @param activity - * @param radiosIDs - */ - public RadioGroupHelper(Activity activity, int... radiosIDs) { - this(activity.findViewById(android.R.id.content),radiosIDs); - } - - /** - * Constructor to group radio buttons - * @param rootView - * @param radiosIDs - */ - public RadioGroupHelper(View rootView, int... radiosIDs) { - super(); - for (int radioButtonID : radiosIDs) { - add(rootView.findViewById(radioButtonID)); - } - } - - private void add(CompoundButton button){ - this.radioButtons.add(button); - button.setOnClickListener(onClickListener); - } - - /** - * listener to ensure only one of the radio button is selected - */ - View.OnClickListener onClickListener = v -> { - for (CompoundButton rb : radioButtons) { - if (rb != v) rb.setChecked(false); - } - }; -} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.kt b/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.kt new file mode 100644 index 000000000..8afdf94c5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.kt @@ -0,0 +1,61 @@ +package fr.free.nrw.commons.quiz + +import android.app.Activity +import android.view.View +import android.widget.CompoundButton +import android.widget.RadioButton + +import java.util.ArrayList + +/** + * Used to group to or more radio buttons to ensure + * that at a particular time only one of them is selected + */ +class RadioGroupHelper { + + val radioButtons: MutableList = ArrayList() + + /** + * Constructor to group radio buttons + * @param radios + */ + constructor(vararg radios: RadioButton) { + for (rb in radios) { + add(rb) + } + } + + /** + * Constructor to group radio buttons + * @param activity + * @param radiosIDs + */ + constructor(activity: Activity, vararg radiosIDs: Int) : this( + *radiosIDs.map { id -> activity.findViewById(id) }.toTypedArray() + ) + + /** + * Constructor to group radio buttons + * @param rootView + * @param radiosIDs + */ + constructor(rootView: View, vararg radiosIDs: Int) { + for (radioButtonID in radiosIDs) { + add(rootView.findViewById(radioButtonID)) + } + } + + private fun add(button: CompoundButton) { + radioButtons.add(button) + button.setOnClickListener(onClickListener) + } + + /** + * listener to ensure only one of the radio button is selected + */ + private val onClickListener = View.OnClickListener { v -> + for (rb in radioButtons) { + if (rb != v) rb.isChecked = false + } + } +} From 874773b88131f47829325bb12dc9a3247adf70fa Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 25 Nov 2024 13:01:54 +0100 Subject: [PATCH 39/74] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-iw/strings.xml | 10 ++++++++++ app/src/main/res/values-mk/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 8 ++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 2ea71f748..be45099a9 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -149,6 +149,7 @@ חיפוש קטגוריות חפשו פריטים שהמדיה שלך מציגה (הר, טאג\' מהאל, וכו\') שמירה + תפריט גלישה רענון רשימה (לא הועלה עדיין שום דבר) @@ -821,6 +822,15 @@ ממתינות נכשלו לא היה אפשר לטעון את נתוני המקום + מחיקת תיקייה + אישור מחיקה + למחוק את התיקייה %1$s על כל %2$d פריטיה? + מחיקה + ביטול + התיקייה %1$s נמחקה + מחיקת התיקייה %1$s נכשלה + שגיאה בהעברת תוכן התיקייה לאשפה: %1$s + נכשל אחזור נתיב התיקייה למזהה ההקבצה: %1$d אין עדיין תמונה למקום הזה, אפשר פשוט לצלם אחת! למקום הזה כבר יש תמונה. עכשיו מתבצעת בדיקה האם למקום הזה יש תמונה. diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 61d71ea68..6d705c4a7 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -653,7 +653,7 @@ Погл. категориска страница Погл. страница на предметот Јазик на прилогот - Острани толкување и опис + Отстрани толкување и опис Прочитајте повеќе На сите јазици Изберете местоположба diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 54acc5273..9b95e69d1 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -846,6 +846,14 @@ В ожидании Не удалось Не удалось загрузить данные о месте + Удалить папку + Подтвердите удаление + Вы уверены, что хотите удалить папку %1$s содержащую %2$d шт. вложенных элементов? + Удалить + Отмена + Папка %1$s успешно удалена + Не получилось удалить папку %1$s + Ошибка при удалении содержимого папки: %1$s У этого места пока нет фотографий, так что сделайте несколько! У этого места уже есть фотография. Сейчас проверим, есть ли у этого места фотография. From 381f9eca0c73338332546ffb9c1f39797b242cef Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Tue, 26 Nov 2024 17:59:31 +0530 Subject: [PATCH 40/74] Migrated notification module from Java to Kotlin (#5955) * Rename .java to .kt * Migration of notification module from Java to Kotlin --- .../contributions/ContributionsFragment.java | 2 +- .../commons/contributions/MainActivity.java | 2 +- .../notification/NotificationActivity.java | 288 ------------------ .../notification/NotificationActivity.kt | 247 +++++++++++++++ ...catinAdapter.kt => NotificationAdapter.kt} | 2 +- .../notification/NotificationController.java | 33 -- .../notification/NotificationController.kt | 25 ++ .../notification/NotificationHelper.java | 73 ----- .../notification/NotificationHelper.kt | 73 +++++ .../NotificationWorkerFragment.java | 31 -- .../NotificationWorkerFragment.kt | 20 ++ .../notification/models/NotificationType.java | 28 -- .../notification/models/NotificationType.kt | 33 ++ 13 files changed, 401 insertions(+), 456 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt rename app/src/main/java/fr/free/nrw/commons/notification/{NotificatinAdapter.kt => NotificationAdapter.kt} (92%) delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationController.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.kt diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index 1699f35f0..bffafaef1 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -289,7 +289,7 @@ public class ContributionsFragment }); } notification.setOnClickListener(view -> { - NotificationActivity.startYourself(getContext(), "unread"); + NotificationActivity.Companion.startYourself(getContext(), "unread"); }); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 849ef3450..03027f287 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -414,7 +414,7 @@ public class MainActivity extends BaseActivity return true; case R.id.notifications: // Starts notification activity on click to notification icon - NotificationActivity.startYourself(this, "unread"); + NotificationActivity.Companion.startYourself(this, "unread"); return true; default: return super.onOptionsItemSelected(item); diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java deleted file mode 100644 index b57df4948..000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ /dev/null @@ -1,288 +0,0 @@ -package fr.free.nrw.commons.notification; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import com.google.android.material.snackbar.Snackbar; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.databinding.ActivityNotificationBinding; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; -import fr.free.nrw.commons.notification.models.Notification; -import fr.free.nrw.commons.notification.models.NotificationType; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.NetworkUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; -import io.reactivex.ObservableSource; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Callable; -import javax.inject.Inject; -import kotlin.Unit; -import timber.log.Timber; - -/** - * Created by root on 18.12.2017. - */ - -public class NotificationActivity extends BaseActivity { - private ActivityNotificationBinding binding; - - @Inject - NotificationController controller; - - @Inject - SessionManager sessionManager; - - private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment"; - private NotificationWorkerFragment mNotificationWorkerFragment; - private NotificatinAdapter adapter; - private List notificationList; - MenuItem notificationMenuItem; - /** - * Boolean isRead is true if this notification activity is for read section of notification. - */ - private boolean isRead; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - isRead = getIntent().getStringExtra("title").equals("read"); - binding = ActivityNotificationBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - mNotificationWorkerFragment = (NotificationWorkerFragment) getFragmentManager() - .findFragmentByTag(TAG_NOTIFICATION_WORKER_FRAGMENT); - initListView(); - setPageTitle(); - setSupportActionBar(binding.toolbar.toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - /** - * If this is unread section of the notifications, removeNotification method - * Marks the notification as read, - * Removes the notification from unread, - * Displays the Snackbar. - * - * Otherwise returns (read section). - * - * @param notification - */ - @SuppressLint("CheckResult") - public void removeNotification(Notification notification) { - if (isRead) { - return; - } - Disposable disposable = Observable.defer((Callable>) - () -> controller.markAsRead(notification)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - if (result) { - notificationList.remove(notification); - setItems(notificationList); - adapter.notifyDataSetChanged(); - ViewUtil.showLongSnackbar(binding.container,getString(R.string.notification_mark_read)); - if (notificationList.size() == 0) { - setEmptyView(); - binding.container.setVisibility(View.GONE); - binding.noNotificationBackground.setVisibility(View.VISIBLE); - } - } else { - adapter.notifyDataSetChanged(); - setItems(notificationList); - ViewUtil.showLongToast(this,getString(R.string.some_error)); - } - }, throwable -> { - if (throwable instanceof InvalidLoginTokenException) { - final String username = sessionManager.getUserName(); - final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( - this, - getString(R.string.invalid_login_message), - username - ); - - CommonsApplication.getInstance().clearApplicationData( - this, logoutListener); - } else { - Timber.e(throwable, "Error occurred while loading notifications"); - throwable.printStackTrace(); - ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications); - binding.progressBar.setVisibility(View.GONE); - ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications); - } - binding.progressBar.setVisibility(View.GONE); - }); - getCompositeDisposable().add(disposable); - } - - - - private void initListView() { - binding.listView.setLayoutManager(new LinearLayoutManager(this)); - DividerItemDecoration itemDecor = new DividerItemDecoration(binding.listView.getContext(), DividerItemDecoration.VERTICAL); - binding.listView.addItemDecoration(itemDecor); - if (isRead) { - refresh(true); - } else { - refresh(false); - } - adapter = new NotificatinAdapter(item -> { - Timber.d("Notification clicked %s", item.getLink()); - if (item.getNotificationType() == NotificationType.EMAIL){ - ViewUtil.showLongSnackbar(binding.container,getString(R.string.check_your_email_inbox)); - } else { - handleUrl(item.getLink()); - } - removeNotification(item); - return Unit.INSTANCE; - }); - binding.listView.setAdapter(adapter); - } - - private void refresh(boolean archived) { - if (!NetworkUtils.isInternetConnectionEstablished(this)) { - binding.progressBar.setVisibility(View.GONE); - Snackbar.make(binding.container, R.string.no_internet, Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.retry, view -> refresh(archived)).show(); - } else { - addNotifications(archived); - } - binding.progressBar.setVisibility(View.VISIBLE); - binding.noNotificationBackground.setVisibility(View.GONE); - binding.container.setVisibility(View.VISIBLE); - } - - @SuppressLint("CheckResult") - private void addNotifications(boolean archived) { - Timber.d("Add notifications"); - if (mNotificationWorkerFragment == null) { - binding.progressBar.setVisibility(View.VISIBLE); - getCompositeDisposable().add(controller.getNotifications(archived) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(notificationList -> { - Collections.reverse(notificationList); - Timber.d("Number of notifications is %d", notificationList.size()); - this.notificationList = notificationList; - if (notificationList.size()==0){ - setEmptyView(); - binding.container.setVisibility(View.GONE); - binding.noNotificationBackground.setVisibility(View.VISIBLE); - } else { - setItems(notificationList); - } - binding.progressBar.setVisibility(View.GONE); - }, throwable -> { - Timber.e(throwable, "Error occurred while loading notifications "); - ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications); - binding.progressBar.setVisibility(View.GONE); - })); - } else { - notificationList = mNotificationWorkerFragment.getNotificationList(); - setItems(notificationList); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_notifications, menu); - notificationMenuItem = menu.findItem(R.id.archived); - setMenuItemTitle(); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.getItemId()) { - case R.id.archived: - if (item.getTitle().equals(getString(R.string.menu_option_read))) { - NotificationActivity.startYourself(NotificationActivity.this, "read"); - }else if (item.getTitle().equals(getString(R.string.menu_option_unread))) { - onBackPressed(); - } - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void handleUrl(String url) { - if (url == null || url.equals("")) { - return; - } - Utils.handleWebUrl(this, Uri.parse(url)); - } - - private void setItems(List notificationList) { - if (notificationList == null || notificationList.isEmpty()) { - ViewUtil.showShortSnackbar(binding.container, R.string.no_notifications); - /*progressBar.setVisibility(View.GONE); - recyclerView.setVisibility(View.GONE);*/ - binding.container.setVisibility(View.GONE); - setEmptyView(); - binding.noNotificationBackground.setVisibility(View.VISIBLE); - return; - } - binding.container.setVisibility(View.VISIBLE); - binding.noNotificationBackground.setVisibility(View.GONE); - adapter.setItems(notificationList); - } - - public static void startYourself(Context context, String title) { - Intent intent = new Intent(context, NotificationActivity.class); - intent.putExtra("title", title); - - context.startActivity(intent); - } - - private void setPageTitle() { - if (getSupportActionBar() != null) { - if (isRead) { - getSupportActionBar().setTitle(R.string.read_notifications); - } else { - getSupportActionBar().setTitle(R.string.notifications); - } - } - } - - private void setEmptyView() { - if (isRead) { - binding.noNotificationText.setText(R.string.no_read_notification); - }else { - binding.noNotificationText.setText(R.string.no_notification); - } - } - - private void setMenuItemTitle() { - if (isRead) { - notificationMenuItem.setTitle(R.string.menu_option_unread); - - }else { - notificationMenuItem.setTitle(R.string.menu_option_read); - - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt new file mode 100644 index 000000000..1d87a8f82 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt @@ -0,0 +1,247 @@ +package fr.free.nrw.commons.notification + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException +import fr.free.nrw.commons.databinding.ActivityNotificationBinding +import fr.free.nrw.commons.notification.models.Notification +import fr.free.nrw.commons.notification.models.NotificationType +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.NetworkUtils +import fr.free.nrw.commons.utils.ViewUtil +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import javax.inject.Inject + +/** + * Created by root on 18.12.2017. + */ +class NotificationActivity : BaseActivity() { + + private lateinit var binding: ActivityNotificationBinding + + @Inject + lateinit var controller: NotificationController + + @Inject + lateinit var sessionManager: SessionManager + + private val tagNotificationWorkerFragment = "NotificationWorkerFragment" + private var mNotificationWorkerFragment: NotificationWorkerFragment? = null + private lateinit var adapter: NotificationAdapter + private var notificationList: MutableList = mutableListOf() + private var notificationMenuItem: MenuItem? = null + + /** + * Boolean isRead is true if this notification activity is for read section of notification. + */ + private var isRead: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + isRead = intent.getStringExtra("title") == "read" + binding = ActivityNotificationBinding.inflate(layoutInflater) + setContentView(binding.root) + mNotificationWorkerFragment = supportFragmentManager.findFragmentByTag( + tagNotificationWorkerFragment + ) as? NotificationWorkerFragment + initListView() + setPageTitle() + setSupportActionBar(binding.toolbar.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + @SuppressLint("CheckResult", "NotifyDataSetChanged") + fun removeNotification(notification: Notification) { + if (isRead) return + + val disposable = Observable.defer { controller.markAsRead(notification) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ result -> + if (result) { + notificationList.remove(notification) + setItems(notificationList) + adapter.notifyDataSetChanged() + ViewUtil.showLongSnackbar(binding.container, getString(R.string.notification_mark_read)) + if (notificationList.isEmpty()) { + setEmptyView() + binding.container.visibility = View.GONE + binding.noNotificationBackground.visibility = View.VISIBLE + } + } else { + adapter.notifyDataSetChanged() + setItems(notificationList) + ViewUtil.showLongToast(this, getString(R.string.some_error)) + } + }, { throwable -> + if (throwable is InvalidLoginTokenException) { + val username = sessionManager.getUserName() + val logoutListener = CommonsApplication.BaseLogoutListener( + this, + getString(R.string.invalid_login_message), + username + ) + CommonsApplication.instance.clearApplicationData(this, logoutListener) + } else { + Timber.e(throwable, "Error occurred while loading notifications") + ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications) + } + binding.progressBar.visibility = View.GONE + }) + compositeDisposable.add(disposable) + } + + private fun initListView() { + binding.listView.layoutManager = LinearLayoutManager(this) + val itemDecor = DividerItemDecoration(binding.listView.context, DividerItemDecoration.VERTICAL) + binding.listView.addItemDecoration(itemDecor) + refresh(isRead) + adapter = NotificationAdapter { item -> + Timber.d("Notification clicked %s", item.link) + if (item.notificationType == NotificationType.EMAIL) { + ViewUtil.showLongSnackbar(binding.container, getString(R.string.check_your_email_inbox)) + } else { + handleUrl(item.link) + } + removeNotification(item) + } + binding.listView.adapter = adapter + } + + private fun refresh(archived: Boolean) { + if (!NetworkUtils.isInternetConnectionEstablished(this)) { + binding.progressBar.visibility = View.GONE + Snackbar.make(binding.container, R.string.no_internet, Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.retry) { refresh(archived) } + .show() + } else { + addNotifications(archived) + } + binding.progressBar.visibility = View.VISIBLE + binding.noNotificationBackground.visibility = View.GONE + binding.container.visibility = View.VISIBLE + } + + @SuppressLint("CheckResult") + private fun addNotifications(archived: Boolean) { + Timber.d("Add notifications") + if (mNotificationWorkerFragment == null) { + binding.progressBar.visibility = View.VISIBLE + compositeDisposable.add(controller.getNotifications(archived) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ notificationList -> + notificationList.reversed() + Timber.d("Number of notifications is %d", notificationList.size) + this.notificationList = notificationList.toMutableList() + if (notificationList.isEmpty()) { + setEmptyView() + binding.container.visibility = View.GONE + binding.noNotificationBackground.visibility = View.VISIBLE + } else { + setItems(notificationList) + } + binding.progressBar.visibility = View.GONE + }, { throwable -> + Timber.e(throwable, "Error occurred while loading notifications") + ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications) + binding.progressBar.visibility = View.GONE + })) + } else { + notificationList = mNotificationWorkerFragment?.notificationList?.toMutableList() ?: mutableListOf() + setItems(notificationList) + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_notifications, menu) + notificationMenuItem = menu.findItem(R.id.archived) + setMenuItemTitle() + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.archived -> { + if (item.title == getString(R.string.menu_option_read)) { + startYourself(this, "read") + } else if (item.title == getString(R.string.menu_option_unread)) { + onBackPressed() + } + true + } + else -> super.onOptionsItemSelected(item) + } + } + + private fun handleUrl(url: String?) { + if (url.isNullOrEmpty()) return + Utils.handleWebUrl(this, Uri.parse(url)) + } + + private fun setItems(notificationList: List?) { + if (notificationList.isNullOrEmpty()) { + ViewUtil.showShortSnackbar(binding.container, R.string.no_notifications) + binding.container.visibility = View.GONE + setEmptyView() + binding.noNotificationBackground.visibility = View.VISIBLE + return + } + binding.container.visibility = View.VISIBLE + binding.noNotificationBackground.visibility = View.GONE + adapter.items = notificationList + } + + private fun setPageTitle() { + supportActionBar?.title = if (isRead) { + getString(R.string.read_notifications) + } else { + getString(R.string.notifications) + } + } + + private fun setEmptyView() { + binding.noNotificationText.text = if (isRead) { + getString(R.string.no_read_notification) + } else { + getString(R.string.no_notification) + } + } + + private fun setMenuItemTitle() { + notificationMenuItem?.title = if (isRead) { + getString(R.string.menu_option_unread) + } else { + getString(R.string.menu_option_read) + } + } + + companion object { + fun startYourself(context: Context, title: String) { + val intent = Intent(context, NotificationActivity::class.java) + intent.putExtra("title", title) + context.startActivity(intent) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificatinAdapter.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapter.kt similarity index 92% rename from app/src/main/java/fr/free/nrw/commons/notification/NotificatinAdapter.kt rename to app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapter.kt index 41d7d4883..637443ecf 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificatinAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapter.kt @@ -3,7 +3,7 @@ package fr.free.nrw.commons.notification import fr.free.nrw.commons.notification.models.Notification import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter -internal class NotificatinAdapter( +internal class NotificationAdapter( onNotificationClicked: (Notification) -> Unit, ) : BaseDelegateAdapter( notificationDelegate(onNotificationClicked), diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java deleted file mode 100644 index de1f372d2..000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java +++ /dev/null @@ -1,33 +0,0 @@ -package fr.free.nrw.commons.notification; - -import fr.free.nrw.commons.notification.models.Notification; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import io.reactivex.Observable; -import io.reactivex.Single; - -/** - * Created by root on 19.12.2017. - */ -@Singleton -public class NotificationController { - - private NotificationClient notificationClient; - - - @Inject - public NotificationController(NotificationClient notificationClient) { - this.notificationClient = notificationClient; - } - - public Single> getNotifications(boolean archived) { - return notificationClient.getNotifications(archived); - } - - Observable markAsRead(Notification notification) { - return notificationClient.markNotificationAsRead(notification.getNotificationId()); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.kt new file mode 100644 index 000000000..870d658cb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.notification + +import fr.free.nrw.commons.notification.models.Notification +import javax.inject.Inject +import javax.inject.Singleton + +import io.reactivex.Observable +import io.reactivex.Single + +/** + * Created by root on 19.12.2017. + */ +@Singleton +class NotificationController @Inject constructor( + private val notificationClient: NotificationClient +) { + + fun getNotifications(archived: Boolean): Single> { + return notificationClient.getNotifications(archived) + } + + fun markAsRead(notification: Notification): Observable { + return notificationClient.markNotificationAsRead(notification.notificationId) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java deleted file mode 100644 index b63d3a4c1..000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java +++ /dev/null @@ -1,73 +0,0 @@ -package fr.free.nrw.commons.notification; - -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import androidx.core.app.NotificationCompat; -import javax.inject.Inject; -import javax.inject.Singleton; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import static androidx.core.app.NotificationCompat.DEFAULT_ALL; -import static androidx.core.app.NotificationCompat.PRIORITY_HIGH; - -/** - * Helper class that can be used to build a generic notification - * Going forward all notifications should be built using this helper class - */ -@Singleton -public class NotificationHelper { - - public static final int NOTIFICATION_DELETE = 1; - public static final int NOTIFICATION_EDIT_CATEGORY = 2; - public static final int NOTIFICATION_EDIT_COORDINATES = 3; - public static final int NOTIFICATION_EDIT_DESCRIPTION = 4; - public static final int NOTIFICATION_EDIT_DEPICTIONS = 5; - - private final NotificationManager notificationManager; - private final NotificationCompat.Builder notificationBuilder; - - @Inject - public NotificationHelper(final Context context) { - notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationBuilder = new NotificationCompat - .Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) - .setOnlyAlertOnce(true); - } - - /** - * Public interface to build and show a notification in the notification bar - * @param context passed context - * @param notificationTitle title of the notification - * @param notificationMessage message to be displayed in the notification - * @param notificationId the notificationID - * @param intent the intent to be fired when the notification is clicked - */ - public void showNotification( - final Context context, - final String notificationTitle, - final String notificationMessage, - final int notificationId, - final Intent intent - ) { - notificationBuilder.setDefaults(DEFAULT_ALL) - .setContentTitle(notificationTitle) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(notificationMessage)) - .setSmallIcon(R.drawable.ic_launcher) - .setProgress(0, 0, false) - .setOngoing(false) - .setPriority(PRIORITY_HIGH); - - int flags = PendingIntent.FLAG_UPDATE_CURRENT; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - flags |= PendingIntent.FLAG_IMMUTABLE; // This flag was introduced in API 23 - } - - final PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, flags); - notificationBuilder.setContentIntent(pendingIntent); - notificationManager.notify(notificationId, notificationBuilder.build()); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.kt new file mode 100644 index 000000000..101a8fccc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.kt @@ -0,0 +1,73 @@ +package fr.free.nrw.commons.notification + +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import javax.inject.Inject +import javax.inject.Singleton +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.R +import androidx.core.app.NotificationCompat.DEFAULT_ALL +import androidx.core.app.NotificationCompat.PRIORITY_HIGH + +/** + * Helper class that can be used to build a generic notification + * Going forward all notifications should be built using this helper class + */ +@Singleton +class NotificationHelper @Inject constructor( + context: Context +) { + + companion object { + const val NOTIFICATION_DELETE = 1 + const val NOTIFICATION_EDIT_CATEGORY = 2 + const val NOTIFICATION_EDIT_COORDINATES = 3 + const val NOTIFICATION_EDIT_DESCRIPTION = 4 + const val NOTIFICATION_EDIT_DEPICTIONS = 5 + } + + private val notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + private val notificationBuilder: NotificationCompat.Builder = NotificationCompat + .Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) + .setOnlyAlertOnce(true) + + /** + * Public interface to build and show a notification in the notification bar + * @param context passed context + * @param notificationTitle title of the notification + * @param notificationMessage message to be displayed in the notification + * @param notificationId the notificationID + * @param intent the intent to be fired when the notification is clicked + */ + fun showNotification( + context: Context, + notificationTitle: String, + notificationMessage: String, + notificationId: Int, + intent: Intent + ) { + notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) + .setContentTitle(notificationTitle) + .setStyle(NotificationCompat.BigTextStyle().bigText(notificationMessage)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(0, 0, false) + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_HIGH) + + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + + val pendingIntent = PendingIntent.getActivity(context, 1, intent, flags) + notificationBuilder.setContentIntent(pendingIntent) + notificationManager.notify(notificationId, notificationBuilder.build()) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java deleted file mode 100644 index ffee5eac2..000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java +++ /dev/null @@ -1,31 +0,0 @@ -package fr.free.nrw.commons.notification; - -import android.app.Fragment; -import android.os.Bundle; - -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.notification.models.Notification; -import java.util.List; - -/** - * Created by knightshade on 25/2/18. - */ - -public class NotificationWorkerFragment extends Fragment { - private List notificationList; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); - } - - public void setNotificationList(List notificationList){ - this.notificationList = notificationList; - } - - public List getNotificationList(){ - return notificationList; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.kt new file mode 100644 index 000000000..928651b9a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.notification + +import android.app.Fragment +import android.os.Bundle + +import fr.free.nrw.commons.notification.models.Notification + + +/** + * Created by knightshade on 25/2/18. + */ +class NotificationWorkerFragment : Fragment() { + + var notificationList: List? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + retainInstance = true + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.java b/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.java deleted file mode 100644 index fb9ae7e99..000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.java +++ /dev/null @@ -1,28 +0,0 @@ -package fr.free.nrw.commons.notification.models; - -public enum NotificationType { - THANK_YOU_EDIT("thank-you-edit"), - EDIT_USER_TALK("edit-user-talk"), - MENTION("mention"), - EMAIL("email"), - WELCOME("welcome"), - UNKNOWN("unknown"); - private String type; - - NotificationType(String type) { - this.type = type; - } - - public String getType() { - return type; - } - - public static NotificationType handledValueOf(String name) { - for (NotificationType e : values()) { - if (e.getType().equals(name)) { - return e; - } - } - return UNKNOWN; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.kt b/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.kt new file mode 100644 index 000000000..9034b3c59 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.kt @@ -0,0 +1,33 @@ +package fr.free.nrw.commons.notification.models + +enum class NotificationType(private val type: String) { + THANK_YOU_EDIT("thank-you-edit"), + + EDIT_USER_TALK("edit-user-talk"), + + MENTION("mention"), + + EMAIL("email"), + + WELCOME("welcome"), + + UNKNOWN("unknown"); + + // Getter for the type property + fun getType(): String { + return type + } + + companion object { + // Returns the corresponding NotificationType for a given name or UNKNOWN + // if no match is found + fun handledValueOf(name: String): NotificationType { + for (e in values()) { + if (e.type == name) { + return e + } + } + return UNKNOWN + } + } +} From 238023056fbc97ec95f98d1870b3e0e57bf53e5b Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Wed, 27 Nov 2024 19:28:46 +0530 Subject: [PATCH 41/74] Migrated navtab module from Java to Kotlin (#5965) * Rename .java to .kt * Migrated navtab module from Java to Kotlin * Migrated navtab module from Java to Kotlin --- .../navtab/MoreBottomSheetFragment.java | 238 ----------------- .../commons/navtab/MoreBottomSheetFragment.kt | 242 ++++++++++++++++++ .../MoreBottomSheetLoggedOutFragment.java | 142 ---------- .../MoreBottomSheetLoggedOutFragment.kt | 151 +++++++++++ .../fr/free/nrw/commons/navtab/NavTab.java | 95 ------- .../java/fr/free/nrw/commons/navtab/NavTab.kt | 79 ++++++ .../navtab/NavTabFragmentPagerAdapter.java | 38 --- .../navtab/NavTabFragmentPagerAdapter.kt | 36 +++ .../free/nrw/commons/navtab/NavTabLayout.java | 41 --- .../free/nrw/commons/navtab/NavTabLayout.kt | 47 ++++ .../nrw/commons/navtab/NavTabLoggedOut.java | 79 ------ .../nrw/commons/navtab/NavTabLoggedOut.kt | 65 +++++ 12 files changed, 620 insertions(+), 633 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.kt diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java deleted file mode 100644 index 9ea59488e..000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java +++ /dev/null @@ -1,238 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import fr.free.nrw.commons.AboutActivity; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.CommonsApplication.ActivityLogoutListener; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.WelcomeActivity; -import fr.free.nrw.commons.actions.PageEditClient; -import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetBinding; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.feedback.FeedbackContentCreator; -import fr.free.nrw.commons.feedback.model.Feedback; -import fr.free.nrw.commons.feedback.FeedbackDialog; -import fr.free.nrw.commons.kvstore.BasicKvStore; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.logging.CommonsLogSender; -import fr.free.nrw.commons.profile.ProfileActivity; -import fr.free.nrw.commons.review.ReviewActivity; -import fr.free.nrw.commons.settings.SettingsActivity; -import io.reactivex.Single; -import io.reactivex.SingleSource; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.util.concurrent.Callable; -import javax.inject.Inject; -import javax.inject.Named; - -public class MoreBottomSheetFragment extends BottomSheetDialogFragment { - - @Inject - CommonsLogSender commonsLogSender; - - private TextView moreProfile; - - @Inject @Named("default_preferences") - JsonKvStore store; - - @Inject - @Named("commons-page-edit") - PageEditClient pageEditClient; - - private static final String GITHUB_ISSUES_URL = "https://github.com/commons-app/apps-android-commons/issues"; - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - final @NonNull FragmentMoreBottomSheetBinding binding = - FragmentMoreBottomSheetBinding.inflate(inflater, container, false); - moreProfile = binding.moreProfile; - - if(store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED)){ - binding.morePeerReview.setVisibility(View.GONE); - } - - binding.moreLogout.setOnClickListener(v -> onLogoutClicked()); - binding.moreFeedback.setOnClickListener(v -> onFeedbackClicked()); - binding.moreAbout.setOnClickListener(v -> onAboutClicked()); - binding.moreTutorial.setOnClickListener(v -> onTutorialClicked()); - binding.moreSettings.setOnClickListener(v -> onSettingsClicked()); - binding.moreProfile.setOnClickListener(v -> onProfileClicked()); - binding.morePeerReview.setOnClickListener(v -> onPeerReviewClicked()); - binding.moreFeedbackGithub.setOnClickListener(v -> onFeedbackGithubClicked()); - - setUserName(); - return binding.getRoot(); - } - - private void onFeedbackGithubClicked() { - final Intent intent; - intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(GITHUB_ISSUES_URL)); - startActivity(intent); - } - - @Override - public void onAttach(@NonNull final Context context) { - super.onAttach(context); - ApplicationlessInjection - .getInstance(requireActivity().getApplicationContext()) - .getCommonsApplicationComponent() - .inject(this); - } - - /** - * Set the username and user achievements level (if available) in navigationHeader. - */ - private void setUserName() { - BasicKvStore store = new BasicKvStore(this.getContext(), getUserName()); - String level = store.getString("userAchievementsLevel","0"); - if (level.equals("0")) { - moreProfile.setText(getUserName() + " (" + getString(R.string.see_your_achievements) + ")"); - } - else { - moreProfile.setText(getUserName() + " (" + getString(R.string.level) + " " + level + ")"); - } - } - - private String getUserName(){ - final AccountManager accountManager = AccountManager.get(getActivity()); - final Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE); - if (allAccounts.length != 0) { - return allAccounts[0].name; - } - return ""; - } - - - protected void onLogoutClicked() { - new AlertDialog.Builder(requireActivity()) - .setMessage(R.string.logout_verification) - .setCancelable(false) - .setPositiveButton(R.string.yes, (dialog, which) -> { - final CommonsApplication app = (CommonsApplication) - requireContext().getApplicationContext(); - app.clearApplicationData(requireContext(), new ActivityLogoutListener(requireActivity(), getContext())); - }) - .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) - .show(); - } - - protected void onFeedbackClicked() { - showFeedbackDialog(); - } - - /** - * Creates and shows a dialog asking feedback from users - */ - private void showFeedbackDialog() { - new FeedbackDialog(getContext(), this::uploadFeedback).show(); - } - - /** - * uploads feedback data on the server - */ - void uploadFeedback(final Feedback feedback) { - final FeedbackContentCreator feedbackContentCreator = new FeedbackContentCreator(getContext(), feedback); - - final Single single = - pageEditClient.createNewSection( - "Commons:Mobile_app/Feedback", - feedbackContentCreator.getSectionTitle(), - feedbackContentCreator.getSectionText(), - "New feedback on version " + feedback.getVersion() + " of the app" - ) - .flatMapSingle(Single::just) - .firstOrError(); - - Single.defer((Callable>) () -> single) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(aBoolean -> { - if (aBoolean) { - Toast.makeText(getContext(), getString(R.string.thanks_feedback), Toast.LENGTH_SHORT) - .show(); - } else { - Toast.makeText(getContext(), getString(R.string.error_feedback), - Toast.LENGTH_SHORT).show(); - } - }); - } - - /** - * This method shows the alert dialog when a user wants to send feedback about the app. - */ - private void showAlertDialog() { - new AlertDialog.Builder(requireActivity()) - .setMessage(R.string.feedback_sharing_data_alert) - .setCancelable(false) - .setPositiveButton(R.string.ok, (dialog, which) -> sendFeedback()) - .show(); - } - - /** - * This method collects the feedback message and starts the activity with implicit intent - * to available email client. - */ - private void sendFeedback() { - final String technicalInfo = commonsLogSender.getExtraInfo(); - - final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO); - feedbackIntent.setType("message/rfc822"); - feedbackIntent.setData(Uri.parse("mailto:")); - feedbackIntent.putExtra(Intent.EXTRA_EMAIL, - new String[]{CommonsApplication.FEEDBACK_EMAIL}); - feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, - CommonsApplication.FEEDBACK_EMAIL_SUBJECT); - feedbackIntent.putExtra(Intent.EXTRA_TEXT, String.format( - "\n\n%s\n%s", CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER, technicalInfo)); - try { - startActivity(feedbackIntent); - } catch (final ActivityNotFoundException e) { - Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show(); - } - } - - protected void onAboutClicked() { - final Intent intent = new Intent(getActivity(), AboutActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - requireActivity().startActivity(intent); - } - - protected void onTutorialClicked() { - WelcomeActivity.startYourself(getActivity()); - } - - protected void onSettingsClicked() { - final Intent intent = new Intent(getActivity(), SettingsActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - requireActivity().startActivity(intent); - } - - protected void onProfileClicked() { - ProfileActivity.startYourself(getActivity(), getUserName(), false); - } - - protected void onPeerReviewClicked() { - ReviewActivity.Companion.startYourself(getActivity(), getString(R.string.title_activity_review)); - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt new file mode 100644 index 000000000..857e18ec3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt @@ -0,0 +1,242 @@ +package fr.free.nrw.commons.navtab + +import android.accounts.AccountManager +import android.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import fr.free.nrw.commons.AboutActivity +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.CommonsApplication.ActivityLogoutListener +import fr.free.nrw.commons.R +import fr.free.nrw.commons.WelcomeActivity +import fr.free.nrw.commons.actions.PageEditClient +import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetBinding +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.feedback.FeedbackContentCreator +import fr.free.nrw.commons.feedback.FeedbackDialog +import fr.free.nrw.commons.feedback.model.Feedback +import fr.free.nrw.commons.kvstore.BasicKvStore +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.logging.CommonsLogSender +import fr.free.nrw.commons.profile.ProfileActivity +import fr.free.nrw.commons.review.ReviewActivity +import fr.free.nrw.commons.settings.SettingsActivity +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject +import javax.inject.Named + + +class MoreBottomSheetFragment : BottomSheetDialogFragment() { + + @Inject + lateinit var commonsLogSender: CommonsLogSender + + @Inject + @field: Named("default_preferences") + lateinit var store: JsonKvStore + + @Inject + @field: Named("commons-page-edit") + lateinit var pageEditClient: PageEditClient + + companion object { + private const val GITHUB_ISSUES_URL = + "https://github.com/commons-app/apps-android-commons/issues" + } + + private var binding: FragmentMoreBottomSheetBinding? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentMoreBottomSheetBinding.inflate(inflater, container, false) + + if (store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED)) { + binding?.morePeerReview?.visibility = View.GONE + } + + binding?.apply { + moreLogout.setOnClickListener { onLogoutClicked() } + moreFeedback.setOnClickListener { onFeedbackClicked() } + moreAbout.setOnClickListener { onAboutClicked() } + moreTutorial.setOnClickListener { onTutorialClicked() } + moreSettings.setOnClickListener { onSettingsClicked() } + moreProfile.setOnClickListener { onProfileClicked() } + morePeerReview.setOnClickListener { onPeerReviewClicked() } + moreFeedbackGithub.setOnClickListener { onFeedbackGithubClicked() } + } + + setUserName() + return binding?.root + } + + private fun onFeedbackGithubClicked() { + val intent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(GITHUB_ISSUES_URL) + } + startActivity(intent) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + ApplicationlessInjection + .getInstance(requireActivity().applicationContext) + .commonsApplicationComponent + .inject(this) + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } + + /** + * Set the username and user achievements level (if available) in navigationHeader. + */ + private fun setUserName() { + val store = BasicKvStore(requireContext(), getUserName()) + val level = store.getString("userAchievementsLevel", "0") + binding?.moreProfile?.text = if (level == "0") { + "${getUserName()} (${getString(R.string.see_your_achievements)})" + } else { + "${getUserName()} (${getString(R.string.level)} $level)" + } + } + + private fun getUserName(): String { + val accountManager = AccountManager.get(requireActivity()) + val allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE) + return if (allAccounts.isNotEmpty()) { + allAccounts[0].name + } else { + "" + } + } + + fun onLogoutClicked() { + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.logout_verification) + .setCancelable(false) + .setPositiveButton(R.string.yes) { _, _ -> + val app = requireContext().applicationContext as CommonsApplication + app.clearApplicationData(requireContext(), ActivityLogoutListener(requireActivity(), requireContext())) + } + .setNegativeButton(R.string.no) { dialog, _ -> dialog.cancel() } + .show() + } + + fun onFeedbackClicked() { + showFeedbackDialog() + } + + /** + * Creates and shows a dialog asking feedback from users + */ + private fun showFeedbackDialog() { + FeedbackDialog(requireContext()) { uploadFeedback(it) }.show() + } + + /** + * Uploads feedback data on the server + */ + @SuppressLint("CheckResult") + fun uploadFeedback(feedback: Feedback) { + val feedbackContentCreator = FeedbackContentCreator(requireContext(), feedback) + + val single = pageEditClient.createNewSection( + "Commons:Mobile_app/Feedback", + feedbackContentCreator.sectionTitle, + feedbackContentCreator.sectionText, + "New feedback on version ${feedback.version} of the app" + ) + .flatMapSingle { Single.just(it) } + .firstOrError() + + Single.defer { single } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { success -> + val messageResId = if (success) { + R.string.thanks_feedback + } else { + R.string.error_feedback + } + Toast.makeText(requireContext(), getString(messageResId), Toast.LENGTH_SHORT).show() + } + } + + /** + * This method shows the alert dialog when a user wants to send feedback about the app. + */ + private fun showAlertDialog() { + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.feedback_sharing_data_alert) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> sendFeedback() } + .show() + } + + /** + * This method collects the feedback message and starts the activity with implicit intent + * to the available email client. + */ + @SuppressLint("IntentReset") + private fun sendFeedback() { + val technicalInfo = commonsLogSender.getExtraInfo() + + val feedbackIntent = Intent(Intent.ACTION_SENDTO).apply { + type = "message/rfc822" + data = Uri.parse("mailto:") + putExtra(Intent.EXTRA_EMAIL, arrayOf(CommonsApplication.FEEDBACK_EMAIL)) + putExtra(Intent.EXTRA_SUBJECT, CommonsApplication.FEEDBACK_EMAIL_SUBJECT) + putExtra(Intent.EXTRA_TEXT, "\n\n${CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER}\n$technicalInfo") + } + + try { + startActivity(feedbackIntent) + } catch (e: ActivityNotFoundException) { + Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show() + } + } + + fun onAboutClicked() { + val intent = Intent(activity, AboutActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + requireActivity().startActivity(intent) + } + + fun onTutorialClicked() { + WelcomeActivity.startYourself(requireActivity()) + } + + fun onSettingsClicked() { + val intent = Intent(activity, SettingsActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + requireActivity().startActivity(intent) + } + + fun onProfileClicked() { + ProfileActivity.startYourself(requireActivity(), getUserName(), false) + } + + fun onPeerReviewClicked() { + ReviewActivity.startYourself(requireActivity(), getString(R.string.title_activity_review)) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java deleted file mode 100644 index 3537d7f7b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java +++ /dev/null @@ -1,142 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import fr.free.nrw.commons.AboutActivity; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetLoggedOutBinding; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.logging.CommonsLogSender; -import fr.free.nrw.commons.settings.SettingsActivity; -import javax.inject.Inject; -import javax.inject.Named; -import timber.log.Timber; - -public class MoreBottomSheetLoggedOutFragment extends BottomSheetDialogFragment { - - private FragmentMoreBottomSheetLoggedOutBinding binding; - @Inject - CommonsLogSender commonsLogSender; - @Inject - @Named("default_preferences") - JsonKvStore applicationKvStore; - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { - binding = FragmentMoreBottomSheetLoggedOutBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - binding.moreLogin.setOnClickListener(v -> onLogoutClicked()); - binding.moreFeedback.setOnClickListener(v -> onFeedbackClicked()); - binding.moreAbout.setOnClickListener(v -> onAboutClicked()); - binding.moreSettings.setOnClickListener(v -> onSettingsClicked()); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - - @Override - public void onAttach(@NonNull final Context context) { - super.onAttach(context); - ApplicationlessInjection - .getInstance(requireActivity().getApplicationContext()) - .getCommonsApplicationComponent() - .inject(this); - } - - public void onLogoutClicked() { - applicationKvStore.putBoolean("login_skipped", false); - final Intent intent = new Intent(getContext(), LoginActivity.class); - requireActivity().finish(); //Kill the activity from which you will go to next activity - startActivity(intent); - } - - public void onFeedbackClicked() { - showAlertDialog(); - } - - /** - * This method shows the alert dialog when a user wants to send feedback about the app. - */ - private void showAlertDialog() { - new AlertDialog.Builder(requireActivity()) - .setMessage(R.string.feedback_sharing_data_alert) - .setCancelable(false) - .setPositiveButton(R.string.ok, (dialog, which) -> { - sendFeedback(); - }) - .show(); - } - - /** - * This method collects the feedback message and starts and activity with implicit intent to - * available email client. - */ - private void sendFeedback() { - final String technicalInfo = commonsLogSender.getExtraInfo(); - - final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO); - feedbackIntent.setType("message/rfc822"); - feedbackIntent.setData(Uri.parse("mailto:")); - feedbackIntent.putExtra(Intent.EXTRA_EMAIL, - new String[]{CommonsApplication.FEEDBACK_EMAIL}); - feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, - CommonsApplication.FEEDBACK_EMAIL_SUBJECT); - feedbackIntent.putExtra(Intent.EXTRA_TEXT, String.format( - "\n\n%s\n%s", CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER, technicalInfo)); - try { - startActivity(feedbackIntent); - } catch (final ActivityNotFoundException e) { - Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show(); - } - } - - public void onAboutClicked() { - final Intent intent = new Intent(getActivity(), AboutActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - requireActivity().startActivity(intent); - } - - public void onSettingsClicked() { - final Intent intent = new Intent(getActivity(), SettingsActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - requireActivity().startActivity(intent); - } - - private class BaseLogoutListener implements CommonsApplication.LogoutListener { - - @Override - public void onLogoutComplete() { - Timber.d("Logout complete callback received."); - final Intent nearbyIntent = new Intent( - getContext(), LoginActivity.class); - nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(nearbyIntent); - requireActivity().finish(); - } - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.kt b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.kt new file mode 100644 index 000000000..96baf9e5e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.kt @@ -0,0 +1,151 @@ +package fr.free.nrw.commons.navtab + +import android.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import fr.free.nrw.commons.AboutActivity +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.LoginActivity +import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetLoggedOutBinding +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.logging.CommonsLogSender +import fr.free.nrw.commons.settings.SettingsActivity +import javax.inject.Inject +import javax.inject.Named +import timber.log.Timber + + +class MoreBottomSheetLoggedOutFragment : BottomSheetDialogFragment() { + + private var binding: FragmentMoreBottomSheetLoggedOutBinding? = null + + @Inject + lateinit var commonsLogSender: CommonsLogSender + + @Inject + @field: Named("default_preferences") + lateinit var applicationKvStore: JsonKvStore + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentMoreBottomSheetLoggedOutBinding.inflate( + inflater, + container, + false + ) + return binding?.root + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle? + ) { + binding?.apply { + moreLogin.setOnClickListener { onLogoutClicked() } + moreFeedback.setOnClickListener { onFeedbackClicked() } + moreAbout.setOnClickListener { onAboutClicked() } + moreSettings.setOnClickListener { onSettingsClicked() } + } + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } + + override fun onAttach(context: Context) { + super.onAttach(context) + ApplicationlessInjection + .getInstance(requireActivity().applicationContext) + .commonsApplicationComponent + .inject(this) + } + + fun onLogoutClicked() { + applicationKvStore.putBoolean("login_skipped", false) + val intent = Intent(context, LoginActivity::class.java) + requireActivity().finish() // Kill the activity from which you will go to next activity + startActivity(intent) + } + + fun onFeedbackClicked() { + showAlertDialog() + } + + /** + * This method shows the alert dialog when a user wants to send feedback about the app. + */ + private fun showAlertDialog() { + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.feedback_sharing_data_alert) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> sendFeedback() } + .show() + } + + /** + * This method collects the feedback message and starts an activity with an implicit intent to + * the available email client. + */ + @SuppressLint("IntentReset") + private fun sendFeedback() { + val technicalInfo = commonsLogSender.getExtraInfo() + + val feedbackIntent = Intent(Intent.ACTION_SENDTO).apply { + type = "message/rfc822" + data = Uri.parse("mailto:") + putExtra(Intent.EXTRA_EMAIL, arrayOf(CommonsApplication.FEEDBACK_EMAIL)) + putExtra(Intent.EXTRA_SUBJECT, CommonsApplication.FEEDBACK_EMAIL_SUBJECT) + putExtra( + Intent.EXTRA_TEXT, + "\n\n${CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER}\n$technicalInfo" + ) + } + + try { + startActivity(feedbackIntent) + } catch (e: ActivityNotFoundException) { + Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show() + } + } + + fun onAboutClicked() { + val intent = Intent(activity, AboutActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + requireActivity().startActivity(intent) + } + + fun onSettingsClicked() { + val intent = Intent(activity, SettingsActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + requireActivity().startActivity(intent) + } + + private inner class BaseLogoutListener : CommonsApplication.LogoutListener { + + override fun onLogoutComplete() { + Timber.d("Logout complete callback received.") + val nearbyIntent = Intent(context, LoginActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + } + startActivity(nearbyIntent) + requireActivity().finish() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java deleted file mode 100644 index 0a3123c1c..000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java +++ /dev/null @@ -1,95 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.fragment.app.Fragment; - -import fr.free.nrw.commons.bookmarks.BookmarkFragment; -import fr.free.nrw.commons.contributions.ContributionsFragment; -import fr.free.nrw.commons.explore.ExploreFragment; -import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment; -import fr.free.nrw.commons.wikidata.model.EnumCode; -import fr.free.nrw.commons.wikidata.model.EnumCodeMap; - -import fr.free.nrw.commons.R; - - -public enum NavTab implements EnumCode { - CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) { - @NonNull - @Override - public Fragment newInstance() { - return ContributionsFragment.newInstance(); - } - }, - NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp) { - @NonNull - @Override - public Fragment newInstance() { - return NearbyParentFragment.newInstance(); - } - }, - EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { - @NonNull - @Override - public Fragment newInstance() { - return ExploreFragment.newInstance(); - } - }, - BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) { - @NonNull - @Override - public Fragment newInstance() { - return BookmarkFragment.newInstance(); - } - }, - MORE(R.string.more, R.drawable.ic_menu_black_24dp) { - @NonNull - @Override - public Fragment newInstance() { - return null; - } - }; - - private static final EnumCodeMap MAP = new EnumCodeMap<>(NavTab.class); - - @StringRes - private final int text; - @DrawableRes - private final int icon; - - @NonNull - public static NavTab of(int code) { - return MAP.get(code); - } - - public static int size() { - return MAP.size(); - } - - @StringRes - public int text() { - return text; - } - - @DrawableRes - public int icon() { - return icon; - } - - @NonNull - public abstract Fragment newInstance(); - - @Override - public int code() { - // This enumeration is not marshalled so tying declaration order to presentation order is - // convenient and consistent. - return ordinal(); - } - - NavTab(@StringRes int text, @DrawableRes int icon) { - this.text = text; - this.icon = icon; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt b/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt new file mode 100644 index 000000000..4573fccad --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt @@ -0,0 +1,79 @@ +package fr.free.nrw.commons.navtab + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment + +import fr.free.nrw.commons.bookmarks.BookmarkFragment +import fr.free.nrw.commons.contributions.ContributionsFragment +import fr.free.nrw.commons.explore.ExploreFragment +import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment +import fr.free.nrw.commons.wikidata.model.EnumCode +import fr.free.nrw.commons.wikidata.model.EnumCodeMap + +import fr.free.nrw.commons.R + + +enum class NavTab( + @StringRes private val text: Int, + @DrawableRes private val icon: Int +) : EnumCode { + + CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) { + override fun newInstance(): Fragment { + return ContributionsFragment.newInstance() + } + }, + NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp) { + override fun newInstance(): Fragment { + return NearbyParentFragment.newInstance() + } + }, + EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { + override fun newInstance(): Fragment { + return ExploreFragment.newInstance() + } + }, + BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) { + override fun newInstance(): Fragment { + return BookmarkFragment.newInstance() + } + }, + MORE(R.string.more, R.drawable.ic_menu_black_24dp) { + override fun newInstance(): Fragment? { + return null + } + }; + + companion object { + private val MAP: EnumCodeMap = EnumCodeMap(NavTab::class.java) + + @JvmStatic + fun of(code: Int): NavTab { + return MAP[code] + } + + @JvmStatic + fun size(): Int { + return MAP.size() + } + } + + @StringRes + fun text(): Int { + return text + } + + @DrawableRes + fun icon(): Int { + return icon + } + + abstract fun newInstance(): Fragment? + + override fun code(): Int { + // This enumeration is not marshalled so tying declaration order to presentation order is + // convenient and consistent. + return ordinal + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java deleted file mode 100644 index 5384f2e01..000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java +++ /dev/null @@ -1,38 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import android.view.ViewGroup; - -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentPagerAdapter; - -public class NavTabFragmentPagerAdapter extends FragmentPagerAdapter { - - private Fragment currentFragment; - - public NavTabFragmentPagerAdapter(FragmentManager mgr) { - super(mgr); - } - - @Nullable - public Fragment getCurrentFragment() { - return currentFragment; - } - - @Override - public Fragment getItem(int pos) { - return NavTab.of(pos).newInstance(); - } - - @Override - public int getCount() { - return NavTab.size(); - } - - @Override - public void setPrimaryItem(ViewGroup container, int position, Object object) { - currentFragment = ((Fragment) object); - super.setPrimaryItem(container, position, object); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.kt b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.kt new file mode 100644 index 000000000..369c39ed6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.kt @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.navtab + +import android.view.ViewGroup + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter + + +class NavTabFragmentPagerAdapter( + mgr: FragmentManager +) : FragmentPagerAdapter(mgr, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + private var currentFragment: Fragment? = null + + fun getCurrentFragment(): Fragment? { + return currentFragment + } + + override fun getItem(pos: Int): Fragment { + return NavTab.of(pos).newInstance()!! + } + + override fun getCount(): Int { + return NavTab.size() + } + + override fun setPrimaryItem( + container: ViewGroup, + position: Int, + `object`: Any + ) { + currentFragment = `object` as Fragment + super.setPrimaryItem(container, position, `object`) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java deleted file mode 100644 index 399cbc789..000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java +++ /dev/null @@ -1,41 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.Menu; - -import com.google.android.material.bottomnavigation.BottomNavigationView; -import fr.free.nrw.commons.contributions.MainActivity; - - -public class NavTabLayout extends BottomNavigationView { - - public NavTabLayout(Context context) { - super(context); - setTabViews(); - } - - public NavTabLayout(Context context, AttributeSet attrs) { - super(context, attrs); - setTabViews(); - } - - public NavTabLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - setTabViews(); - } - - private void setTabViews() { - if (((MainActivity) getContext()).applicationKvStore.getBoolean("login_skipped") == true) { - for (int i = 0; i < NavTabLoggedOut.size(); i++) { - NavTabLoggedOut navTab = NavTabLoggedOut.of(i); - getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()); - } - } else { - for (int i = 0; i < NavTab.size(); i++) { - NavTab navTab = NavTab.of(i); - getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()); - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt new file mode 100644 index 000000000..8d5298cac --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt @@ -0,0 +1,47 @@ +package fr.free.nrw.commons.navtab + +import android.content.Context +import android.util.AttributeSet +import android.view.Menu + +import com.google.android.material.bottomnavigation.BottomNavigationView +import fr.free.nrw.commons.contributions.MainActivity + + +class NavTabLayout : BottomNavigationView { + + constructor(context: Context) : super(context) { + setTabViews() + } + + constructor( + context: Context, + attrs: AttributeSet? + ) : super(context, attrs) { + setTabViews() + } + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int + ) : super(context, attrs, defStyleAttr) { + setTabViews() + } + + private fun setTabViews() { + val isLoginSkipped = (context as MainActivity) + .applicationKvStore.getBoolean("login_skipped") + if (isLoginSkipped) { + for (i in 0 until NavTabLoggedOut.size()) { + val navTab = NavTabLoggedOut.of(i) + menu.add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()) + } + } else { + for (i in 0 until NavTab.size()) { + val navTab = NavTab.of(i) + menu.add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java deleted file mode 100644 index dc1c7ce6b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java +++ /dev/null @@ -1,79 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.fragment.app.Fragment; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.bookmarks.BookmarkFragment; -import fr.free.nrw.commons.explore.ExploreFragment; -import fr.free.nrw.commons.wikidata.model.EnumCode; -import fr.free.nrw.commons.wikidata.model.EnumCodeMap; - - -public enum NavTabLoggedOut implements EnumCode { - - EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { - @NonNull - @Override - public Fragment newInstance() { - return ExploreFragment.newInstance(); - } - }, - BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) { - @NonNull - @Override - public Fragment newInstance() { - return BookmarkFragment.newInstance(); - } - }, - MORE(R.string.more, R.drawable.ic_menu_black_24dp) { - @NonNull - @Override - public Fragment newInstance() { - return null; - } - }; - - private static final EnumCodeMap MAP = new EnumCodeMap<>( - NavTabLoggedOut.class); - - @StringRes - private final int text; - @DrawableRes - private final int icon; - - @NonNull - public static NavTabLoggedOut of(int code) { - return MAP.get(code); - } - - public static int size() { - return MAP.size(); - } - - @StringRes - public int text() { - return text; - } - - @DrawableRes - public int icon() { - return icon; - } - - @NonNull - public abstract Fragment newInstance(); - - @Override - public int code() { - // This enumeration is not marshalled so tying declaration order to presentation order is - // convenient and consistent. - return ordinal(); - } - - NavTabLoggedOut(@StringRes int text, @DrawableRes int icon) { - this.text = text; - this.icon = icon; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.kt b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.kt new file mode 100644 index 000000000..ad73f1bbd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.kt @@ -0,0 +1,65 @@ +package fr.free.nrw.commons.navtab + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import fr.free.nrw.commons.R +import fr.free.nrw.commons.bookmarks.BookmarkFragment +import fr.free.nrw.commons.explore.ExploreFragment +import fr.free.nrw.commons.wikidata.model.EnumCode +import fr.free.nrw.commons.wikidata.model.EnumCodeMap + + +enum class NavTabLoggedOut( + @StringRes private val text: Int, + @DrawableRes private val icon: Int +) : EnumCode { + + EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { + override fun newInstance(): Fragment { + return ExploreFragment.newInstance() + } + }, + BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) { + override fun newInstance(): Fragment { + return BookmarkFragment.newInstance() + } + }, + MORE(R.string.more, R.drawable.ic_menu_black_24dp) { + override fun newInstance(): Fragment? { + return null + } + }; + + companion object { + private val MAP: EnumCodeMap = EnumCodeMap(NavTabLoggedOut::class.java) + + @JvmStatic + fun of(code: Int): NavTabLoggedOut { + return MAP[code] + } + + @JvmStatic + fun size(): Int { + return MAP.size() + } + } + + @StringRes + fun text(): Int { + return text + } + + @DrawableRes + fun icon(): Int { + return icon + } + + abstract fun newInstance(): Fragment? + + override fun code(): Int { + // This enumeration is not marshalled so tying declaration order to presentation order is + // convenient and consistent. + return ordinal + } +} \ No newline at end of file From 0c969c365bd7ed45e1ca08e9acb8af5b54382ae4 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Thu, 28 Nov 2024 02:09:25 -0600 Subject: [PATCH 42/74] Convert auth package to kotlin (#5966) * Convert SessionManager to kotlin along with other small fixes * Convert WikiAccountAuthenticator to kotlin * Migrate WikiAccountAuthenticatorService to kotlin * Converted AccountUtil to kotlin * Convert SignupActivity to kotlin * Convert LoginActivity to kotlin * Merge from main --- .../ui/PasteSensitiveTextInputEditTextTest.kt | 2 +- .../fr/free/nrw/commons/auth/AccountUtil.java | 44 -- .../fr/free/nrw/commons/auth/AccountUtil.kt | 24 + .../free/nrw/commons/auth/LoginActivity.java | 456 ------------------ .../fr/free/nrw/commons/auth/LoginActivity.kt | 404 ++++++++++++++++ .../free/nrw/commons/auth/SessionManager.java | 148 ------ .../free/nrw/commons/auth/SessionManager.kt | 95 ++++ .../free/nrw/commons/auth/SignupActivity.java | 82 ---- .../free/nrw/commons/auth/SignupActivity.kt | 75 +++ .../auth/WikiAccountAuthenticator.java | 141 ------ .../commons/auth/WikiAccountAuthenticator.kt | 108 +++++ .../auth/WikiAccountAuthenticatorService.java | 31 -- .../auth/WikiAccountAuthenticatorService.kt | 22 + .../commons/di/CommonsApplicationModule.java | 6 - .../feedback/FeedbackContentCreator.java | 4 +- .../commons/media/MediaDetailFragment.java | 12 +- .../notification/NotificationActivity.kt | 2 +- .../free/nrw/commons/review/ReviewActivity.kt | 4 +- .../commons/upload/FailedUploadsFragment.kt | 2 +- .../nrw/commons/utils/AbstractTextWatcher.kt | 2 +- .../nrw/commons/TestCommonsApplication.kt | 4 - .../nrw/commons/auth/AccountUtilUnitTest.kt | 16 +- .../commons/auth/LoginActivityUnitTests.kt | 11 - ...WikiAccountAuthenticatorServiceUnitTest.kt | 16 +- .../auth/WikiAccountAuthenticatorUnitTest.kt | 5 +- 25 files changed, 752 insertions(+), 964 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt diff --git a/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt index aedbcb133..647c5bbda 100644 --- a/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt +++ b/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt @@ -17,7 +17,7 @@ class PasteSensitiveTextInputEditTextTest { @Before fun setup() { context = ApplicationProvider.getApplicationContext() - textView = PasteSensitiveTextInputEditText(context) + textView = PasteSensitiveTextInputEditText(context!!) } // this test has no real value, just % for test code coverage diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java deleted file mode 100644 index 53903769d..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java +++ /dev/null @@ -1,44 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; - -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.BuildConfig; -import timber.log.Timber; - -public class AccountUtil { - - public static final String AUTH_TOKEN_TYPE = "CommonsAndroid"; - - public AccountUtil() { - } - - /** - * @return Account|null - */ - @Nullable - public static Account account(Context context) { - try { - Account[] accounts = accountManager(context).getAccountsByType(BuildConfig.ACCOUNT_TYPE); - if (accounts.length > 0) { - return accounts[0]; - } - } catch (SecurityException e) { - Timber.e(e); - } - return null; - } - - @Nullable - public static String getUserName(Context context) { - Account account = account(context); - return account == null ? null : account.name; - } - - private static AccountManager accountManager(Context context) { - return AccountManager.get(context); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt new file mode 100644 index 000000000..aa86cd0d8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt @@ -0,0 +1,24 @@ +package fr.free.nrw.commons.auth + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import androidx.annotation.VisibleForTesting +import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE +import timber.log.Timber + +const val AUTH_TOKEN_TYPE: String = "CommonsAndroid" + +fun getUserName(context: Context): String? { + return account(context)?.name +} + +@VisibleForTesting +fun account(context: Context): Account? = try { + val accountManager = AccountManager.get(context) + val accounts = accountManager.getAccountsByType(ACCOUNT_TYPE) + if (accounts.isNotEmpty()) accounts[0] else null +} catch (e: SecurityException) { + Timber.e(e) + null +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java deleted file mode 100644 index 3ff61e511..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ /dev/null @@ -1,456 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.AccountAuthenticatorActivity; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; - -import android.widget.TextView; -import androidx.annotation.ColorRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.core.app.NavUtils; -import androidx.core.content.ContextCompat; -import fr.free.nrw.commons.auth.login.LoginClient; -import fr.free.nrw.commons.auth.login.LoginResult; -import fr.free.nrw.commons.databinding.ActivityLoginBinding; -import fr.free.nrw.commons.utils.ActivityUtils; -import java.util.Locale; -import fr.free.nrw.commons.auth.login.LoginCallback; - -import java.util.Objects; -import javax.inject.Inject; -import javax.inject.Named; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.utils.SystemThemeUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.disposables.CompositeDisposable; -import timber.log.Timber; - -import static android.view.KeyEvent.KEYCODE_ENTER; -import static android.view.View.VISIBLE; -import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; -import static fr.free.nrw.commons.CommonsApplication.LOGIN_MESSAGE_INTENT_KEY; -import static fr.free.nrw.commons.CommonsApplication.LOGIN_USERNAME_INTENT_KEY; - -public class LoginActivity extends AccountAuthenticatorActivity { - - @Inject - SessionManager sessionManager; - - @Inject - @Named("default_preferences") - JsonKvStore applicationKvStore; - - @Inject - LoginClient loginClient; - - @Inject - SystemThemeUtils systemThemeUtils; - - private ActivityLoginBinding binding; - ProgressDialog progressDialog; - private AppCompatDelegate delegate; - private LoginTextWatcher textWatcher = new LoginTextWatcher(); - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - final String saveProgressDailog="ProgressDailog_state"; - final String saveErrorMessage ="errorMessage"; - final String saveUsername="username"; - final String savePassword="password"; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ApplicationlessInjection - .getInstance(this.getApplicationContext()) - .getCommonsApplicationComponent() - .inject(this); - - boolean isDarkTheme = systemThemeUtils.isDeviceInNightMode(); - setTheme(isDarkTheme ? R.style.DarkAppTheme : R.style.LightAppTheme); - getDelegate().installViewFactory(); - getDelegate().onCreate(savedInstanceState); - - binding = ActivityLoginBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - String message = getIntent().getStringExtra(LOGIN_MESSAGE_INTENT_KEY); - String username = getIntent().getStringExtra(LOGIN_USERNAME_INTENT_KEY); - - binding.loginUsername.addTextChangedListener(textWatcher); - binding.loginPassword.addTextChangedListener(textWatcher); - binding.loginTwoFactor.addTextChangedListener(textWatcher); - - binding.skipLogin.setOnClickListener(view -> skipLogin()); - binding.forgotPassword.setOnClickListener(view -> forgotPassword()); - binding.aboutPrivacyPolicy.setOnClickListener(view -> onPrivacyPolicyClicked()); - binding.signUpButton.setOnClickListener(view -> signUp()); - binding.loginButton.setOnClickListener(view -> performLogin()); - - binding.loginPassword.setOnEditorActionListener(this::onEditorAction); - binding.loginPassword.setOnFocusChangeListener(this::onPasswordFocusChanged); - - if (ConfigUtils.isBetaFlavour()) { - binding.loginCredentials.setText(getString(R.string.login_credential)); - } else { - binding.loginCredentials.setVisibility(View.GONE); - } - if (message != null) { - showMessage(message, R.color.secondaryDarkColor); - } - if (username != null) { - binding.loginUsername.setText(username); - } - } - /** - * Hides the keyboard if the user's focus is not on the password (hasFocus is false). - * @param view The keyboard - * @param hasFocus Set to true if the keyboard has focus - */ - void onPasswordFocusChanged(View view, boolean hasFocus) { - if (!hasFocus) { - ViewUtil.hideKeyboard(view); - } - } - - boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { - if (binding.loginButton.isEnabled()) { - if (actionId == IME_ACTION_DONE) { - performLogin(); - return true; - } else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) { - performLogin(); - return true; - } - } - return false; - } - - - protected void skipLogin() { - new AlertDialog.Builder(this).setTitle(R.string.skip_login_title) - .setMessage(R.string.skip_login_message) - .setCancelable(false) - .setPositiveButton(R.string.yes, (dialog, which) -> { - dialog.cancel(); - performSkipLogin(); - }) - .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) - .show(); - } - - protected void forgotPassword() { - Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)); - } - - protected void onPrivacyPolicyClicked() { - Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)); - } - - protected void signUp() { - Intent intent = new Intent(this, SignupActivity.class); - startActivity(intent); - } - - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - getDelegate().onPostCreate(savedInstanceState); - } - - @Override - protected void onResume() { - super.onResume(); - - if (sessionManager.getCurrentAccount() != null - && sessionManager.isUserLoggedIn()) { - applicationKvStore.putBoolean("login_skipped", false); - startMainActivity(); - } - - if (applicationKvStore.getBoolean("login_skipped", false)) { - performSkipLogin(); - } - - } - - @Override - protected void onDestroy() { - compositeDisposable.clear(); - try { - // To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - } catch (Exception e) { - e.printStackTrace(); - } - binding.loginUsername.removeTextChangedListener(textWatcher); - binding.loginPassword.removeTextChangedListener(textWatcher); - binding.loginTwoFactor.removeTextChangedListener(textWatcher); - delegate.onDestroy(); - if(null!=loginClient) { - loginClient.cancel(); - } - binding = null; - super.onDestroy(); - } - - public void performLogin() { - Timber.d("Login to start!"); - final String username = Objects.requireNonNull(binding.loginUsername.getText()).toString(); - final String password = Objects.requireNonNull(binding.loginPassword.getText()).toString(); - final String twoFactorCode = Objects.requireNonNull(binding.loginTwoFactor.getText()).toString(); - - showLoggingProgressBar(); - loginClient.doLogin(username, password, twoFactorCode, Locale.getDefault().getLanguage(), - new LoginCallback() { - @Override - public void success(@NonNull LoginResult loginResult) { - runOnUiThread(()->{ - Timber.d("Login Success"); - hideProgress(); - onLoginSuccess(loginResult); - }); - } - - @Override - public void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token) { - runOnUiThread(()->{ - Timber.d("Requesting 2FA prompt"); - hideProgress(); - askUserForTwoFactorAuth(); - }); - } - - @Override - public void passwordResetPrompt(@Nullable String token) { - runOnUiThread(()->{ - Timber.d("Showing password reset prompt"); - hideProgress(); - showPasswordResetPrompt(); - }); - } - - @Override - public void error(@NonNull Throwable caught) { - runOnUiThread(()->{ - Timber.e(caught); - hideProgress(); - showMessageAndCancelDialog(caught.getLocalizedMessage()); - }); - } - }); - } - - - - private void hideProgress() { - progressDialog.dismiss(); - } - - private void showPasswordResetPrompt() { - showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword)); - } - - - /** - * This function is called when user skips the login. - * It redirects the user to Explore Activity. - */ - private void performSkipLogin() { - applicationKvStore.putBoolean("login_skipped", true); - MainActivity.startYourself(this); - finish(); - } - - private void showLoggingProgressBar() { - progressDialog = new ProgressDialog(this); - progressDialog.setIndeterminate(true); - progressDialog.setTitle(getString(R.string.logging_in_title)); - progressDialog.setMessage(getString(R.string.logging_in_message)); - progressDialog.setCanceledOnTouchOutside(false); - progressDialog.show(); - } - - private void onLoginSuccess(LoginResult loginResult) { - compositeDisposable.clear(); - sessionManager.setUserLoggedIn(true); - sessionManager.updateAccount(loginResult); - progressDialog.dismiss(); - showSuccessAndDismissDialog(); - startMainActivity(); - } - - @Override - protected void onStart() { - super.onStart(); - delegate.onStart(); - } - - @Override - protected void onStop() { - super.onStop(); - delegate.onStop(); - } - - @Override - protected void onPostResume() { - super.onPostResume(); - getDelegate().onPostResume(); - } - - @Override - public void setContentView(View view, ViewGroup.LayoutParams params) { - getDelegate().setContentView(view, params); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - @NonNull - public MenuInflater getMenuInflater() { - return getDelegate().getMenuInflater(); - } - - public void askUserForTwoFactorAuth() { - progressDialog.dismiss(); - binding.twoFactorContainer.setVisibility(VISIBLE); - binding.loginTwoFactor.setVisibility(VISIBLE); - binding.loginTwoFactor.requestFocus(); - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY); - showMessageAndCancelDialog(R.string.login_failed_2fa_needed); - } - - public void showMessageAndCancelDialog(@StringRes int resId) { - showMessage(resId, R.color.secondaryDarkColor); - if (progressDialog != null) { - progressDialog.cancel(); - } - } - - public void showMessageAndCancelDialog(String error) { - showMessage(error, R.color.secondaryDarkColor); - if (progressDialog != null) { - progressDialog.cancel(); - } - } - - public void showSuccessAndDismissDialog() { - showMessage(R.string.login_success, R.color.primaryDarkColor); - progressDialog.dismiss(); - } - - public void startMainActivity() { - ActivityUtils.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP); - finish(); - } - - private void showMessage(@StringRes int resId, @ColorRes int colorResId) { - binding.errorMessage.setText(getString(resId)); - binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); - binding.errorMessageContainer.setVisibility(VISIBLE); - } - - private void showMessage(String message, @ColorRes int colorResId) { - binding.errorMessage.setText(message); - binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); - binding.errorMessageContainer.setVisibility(VISIBLE); - } - - private AppCompatDelegate getDelegate() { - if (delegate == null) { - delegate = AppCompatDelegate.create(this, null); - } - return delegate; - } - - private class LoginTextWatcher implements TextWatcher { - @Override - public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int start, int count, int after) { - } - - @Override - public void afterTextChanged(Editable editable) { - boolean enabled = binding.loginUsername.getText().length() != 0 && - binding.loginPassword.getText().length() != 0 && - (BuildConfig.DEBUG || binding.loginTwoFactor.getText().length() != 0 || - binding.loginTwoFactor.getVisibility() != VISIBLE); - binding.loginButton.setEnabled(enabled); - } - } - - public static void startYourself(Context context) { - Intent intent = new Intent(context, LoginActivity.class); - context.startActivity(intent); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - // if progressDialog is visible during the configuration change then store state as true else false so that - // we maintain visibility of progressDailog after configuration change - if(progressDialog!=null&&progressDialog.isShowing()) { - outState.putBoolean(saveProgressDailog,true); - } else { - outState.putBoolean(saveProgressDailog,false); - } - outState.putString(saveErrorMessage,binding.errorMessage.getText().toString()); //Save the errorMessage - outState.putString(saveUsername,getUsername()); // Save the username - outState.putString(savePassword,getPassword()); // Save the password - } - private String getUsername() { - return binding.loginUsername.getText().toString(); - } - private String getPassword(){ - return binding.loginPassword.getText().toString(); - } - - @Override - protected void onRestoreInstanceState(final Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - binding.loginUsername.setText(savedInstanceState.getString(saveUsername)); - binding.loginPassword.setText(savedInstanceState.getString(savePassword)); - if(savedInstanceState.getBoolean(saveProgressDailog)) { - performLogin(); - } - String errorMessage=savedInstanceState.getString(saveErrorMessage); - if(sessionManager.isUserLoggedIn()) { - showMessage(R.string.login_success, R.color.primaryDarkColor); - } else { - showMessage(errorMessage, R.color.secondaryDarkColor); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt new file mode 100644 index 000000000..3aa9b26d9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt @@ -0,0 +1,404 @@ +package fr.free.nrw.commons.auth + +import android.accounts.AccountAuthenticatorActivity +import android.app.ProgressDialog +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.KeyEvent +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.app.NavUtils +import androidx.core.content.ContextCompat +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.auth.login.LoginCallback +import fr.free.nrw.commons.auth.login.LoginClient +import fr.free.nrw.commons.auth.login.LoginResult +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.ActivityLoginBinding +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.utils.AbstractTextWatcher +import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.utils.SystemThemeUtils +import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard +import io.reactivex.disposables.CompositeDisposable +import timber.log.Timber +import java.util.Locale +import javax.inject.Inject +import javax.inject.Named + +class LoginActivity : AccountAuthenticatorActivity() { + @Inject + lateinit var sessionManager: SessionManager + + @Inject + @field:Named("default_preferences") + lateinit var applicationKvStore: JsonKvStore + + @Inject + lateinit var loginClient: LoginClient + + @Inject + lateinit var systemThemeUtils: SystemThemeUtils + + private var binding: ActivityLoginBinding? = null + private var progressDialog: ProgressDialog? = null + private val textWatcher = AbstractTextWatcher(::onTextChanged) + private val compositeDisposable = CompositeDisposable() + private val delegate: AppCompatDelegate by lazy { + AppCompatDelegate.create(this, null) + } + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ApplicationlessInjection + .getInstance(this.applicationContext) + .commonsApplicationComponent + .inject(this) + + val isDarkTheme = systemThemeUtils.isDeviceInNightMode() + setTheme(if (isDarkTheme) R.style.DarkAppTheme else R.style.LightAppTheme) + delegate.installViewFactory() + delegate.onCreate(savedInstanceState) + + binding = ActivityLoginBinding.inflate(layoutInflater) + with(binding!!) { + setContentView(root) + + loginUsername.addTextChangedListener(textWatcher) + loginPassword.addTextChangedListener(textWatcher) + loginTwoFactor.addTextChangedListener(textWatcher) + + skipLogin.setOnClickListener { skipLogin() } + forgotPassword.setOnClickListener { forgotPassword() } + aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() } + signUpButton.setOnClickListener { signUp() } + loginButton.setOnClickListener { performLogin() } + loginPassword.setOnEditorActionListener(::onEditorAction) + + loginPassword.onFocusChangeListener = + View.OnFocusChangeListener(::onPasswordFocusChanged) + + if (isBetaFlavour) { + loginCredentials.text = getString(R.string.login_credential) + } else { + loginCredentials.visibility = View.GONE + } + + intent.getStringExtra(CommonsApplication.LOGIN_MESSAGE_INTENT_KEY)?.let { + showMessage(it, R.color.secondaryDarkColor) + } + + intent.getStringExtra(CommonsApplication.LOGIN_USERNAME_INTENT_KEY)?.let { + loginUsername.setText(it) + } + } + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + delegate.onPostCreate(savedInstanceState) + } + + override fun onResume() { + super.onResume() + + if (sessionManager.currentAccount != null && sessionManager.isUserLoggedIn) { + applicationKvStore.putBoolean("login_skipped", false) + startMainActivity() + } + + if (applicationKvStore.getBoolean("login_skipped", false)) { + performSkipLogin() + } + } + + override fun onDestroy() { + compositeDisposable.clear() + try { + // To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method + if (progressDialog?.isShowing == true) { + progressDialog!!.dismiss() + } + } catch (e: Exception) { + e.printStackTrace() + } + with(binding!!) { + loginUsername.removeTextChangedListener(textWatcher) + loginPassword.removeTextChangedListener(textWatcher) + loginTwoFactor.removeTextChangedListener(textWatcher) + } + delegate.onDestroy() + loginClient?.cancel() + binding = null + super.onDestroy() + } + + override fun onStart() { + super.onStart() + delegate.onStart() + } + + override fun onStop() { + super.onStop() + delegate.onStop() + } + + override fun onPostResume() { + super.onPostResume() + delegate.onPostResume() + } + + override fun setContentView(view: View, params: ViewGroup.LayoutParams) { + delegate.setContentView(view, params) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + NavUtils.navigateUpFromSameTask(this) + return true + } + } + return super.onOptionsItemSelected(item) + } + + override fun onSaveInstanceState(outState: Bundle) { + // if progressDialog is visible during the configuration change then store state as true else false so that + // we maintain visibility of progressDailog after configuration change + if (progressDialog != null && progressDialog!!.isShowing) { + outState.putBoolean(saveProgressDailog, true) + } else { + outState.putBoolean(saveProgressDailog, false) + } + outState.putString( + saveErrorMessage, + binding!!.errorMessage.text.toString() + ) //Save the errorMessage + outState.putString( + saveUsername, + binding!!.loginUsername.text.toString() + ) // Save the username + outState.putString( + savePassword, + binding!!.loginPassword.text.toString() + ) // Save the password + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + binding!!.loginUsername.setText(savedInstanceState.getString(saveUsername)) + binding!!.loginPassword.setText(savedInstanceState.getString(savePassword)) + if (savedInstanceState.getBoolean(saveProgressDailog)) { + performLogin() + } + val errorMessage = savedInstanceState.getString(saveErrorMessage) + if (sessionManager.isUserLoggedIn) { + showMessage(R.string.login_success, R.color.primaryDarkColor) + } else { + showMessage(errorMessage, R.color.secondaryDarkColor) + } + } + + /** + * Hides the keyboard if the user's focus is not on the password (hasFocus is false). + * @param view The keyboard + * @param hasFocus Set to true if the keyboard has focus + */ + private fun onPasswordFocusChanged(view: View, hasFocus: Boolean) { + if (!hasFocus) { + hideKeyboard(view) + } + } + + private fun onEditorAction(textView: TextView, actionId: Int, keyEvent: KeyEvent?) = + if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) { + performLogin() + true + } else false + + private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) = + actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER + + private fun skipLogin() { + AlertDialog.Builder(this) + .setTitle(R.string.skip_login_title) + .setMessage(R.string.skip_login_message) + .setCancelable(false) + .setPositiveButton(R.string.yes) { dialog: DialogInterface, which: Int -> + dialog.cancel() + performSkipLogin() + } + .setNegativeButton(R.string.no) { dialog: DialogInterface, which: Int -> + dialog.cancel() + } + .show() + } + + private fun forgotPassword() = + Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)) + + private fun onPrivacyPolicyClicked() = + Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)) + + private fun signUp() = + startActivity(Intent(this, SignupActivity::class.java)) + + @VisibleForTesting + fun performLogin() { + Timber.d("Login to start!") + val username = binding!!.loginUsername.text.toString() + val password = binding!!.loginPassword.text.toString() + val twoFactorCode = binding!!.loginTwoFactor.text.toString() + + showLoggingProgressBar() + loginClient.doLogin(username, + password, + twoFactorCode, + Locale.getDefault().language, + object : LoginCallback { + override fun success(loginResult: LoginResult) = runOnUiThread { + Timber.d("Login Success") + progressDialog!!.dismiss() + onLoginSuccess(loginResult) + } + + override fun twoFactorPrompt(caught: Throwable, token: String?) = runOnUiThread { + Timber.d("Requesting 2FA prompt") + progressDialog!!.dismiss() + askUserForTwoFactorAuth() + } + + override fun passwordResetPrompt(token: String?) = runOnUiThread { + Timber.d("Showing password reset prompt") + progressDialog!!.dismiss() + showPasswordResetPrompt() + } + + override fun error(caught: Throwable) = runOnUiThread { + Timber.e(caught) + progressDialog!!.dismiss() + showMessageAndCancelDialog(caught.localizedMessage ?: "") + } + } + ) + } + + private fun showPasswordResetPrompt() = + showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword)) + + /** + * This function is called when user skips the login. + * It redirects the user to Explore Activity. + */ + private fun performSkipLogin() { + applicationKvStore.putBoolean("login_skipped", true) + MainActivity.startYourself(this) + finish() + } + + private fun showLoggingProgressBar() { + progressDialog = ProgressDialog(this).apply { + isIndeterminate = true + setTitle(getString(R.string.logging_in_title)) + setMessage(getString(R.string.logging_in_message)) + setCanceledOnTouchOutside(false) + } + progressDialog!!.show() + } + + private fun onLoginSuccess(loginResult: LoginResult) { + compositeDisposable.clear() + sessionManager.setUserLoggedIn(true) + sessionManager.updateAccount(loginResult) + progressDialog!!.dismiss() + showSuccessAndDismissDialog() + startMainActivity() + } + + override fun getMenuInflater(): MenuInflater = + delegate.menuInflater + + @VisibleForTesting + fun askUserForTwoFactorAuth() { + progressDialog!!.dismiss() + with(binding!!) { + twoFactorContainer.visibility = View.VISIBLE + loginTwoFactor.visibility = View.VISIBLE + loginTwoFactor.requestFocus() + } + val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) + showMessageAndCancelDialog(R.string.login_failed_2fa_needed) + } + + @VisibleForTesting + fun showMessageAndCancelDialog(@StringRes resId: Int) { + showMessage(resId, R.color.secondaryDarkColor) + progressDialog?.cancel() + } + + @VisibleForTesting + fun showMessageAndCancelDialog(error: String) { + showMessage(error, R.color.secondaryDarkColor) + progressDialog?.cancel() + } + + @VisibleForTesting + fun showSuccessAndDismissDialog() { + showMessage(R.string.login_success, R.color.primaryDarkColor) + progressDialog!!.dismiss() + } + + @VisibleForTesting + fun startMainActivity() { + startActivityWithFlags(this, MainActivity::class.java, Intent.FLAG_ACTIVITY_SINGLE_TOP) + finish() + } + + private fun showMessage(@StringRes resId: Int, @ColorRes colorResId: Int) = with(binding!!) { + errorMessage.text = getString(resId) + errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId)) + errorMessageContainer.visibility = View.VISIBLE + } + + private fun showMessage(message: String?, @ColorRes colorResId: Int) = with(binding!!) { + errorMessage.text = message + errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId)) + errorMessageContainer.visibility = View.VISIBLE + } + + private fun onTextChanged(text: String) { + val enabled = + binding!!.loginUsername.text!!.length != 0 && binding!!.loginPassword.text!!.length != 0 && + (BuildConfig.DEBUG || binding!!.loginTwoFactor.text!!.length != 0 || binding!!.loginTwoFactor.visibility != View.VISIBLE) + binding!!.loginButton.isEnabled = enabled + } + + companion object { + fun startYourself(context: Context) = + context.startActivity(Intent(context, LoginActivity::class.java)) + + const val saveProgressDailog: String = "ProgressDailog_state" + const val saveErrorMessage: String = "errorMessage" + const val saveUsername: String = "username" + const val savePassword: String = "password" + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java deleted file mode 100644 index 7c2f4a334..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java +++ /dev/null @@ -1,148 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; -import android.os.Build; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.auth.login.LoginResult; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import io.reactivex.Completable; -import io.reactivex.Observable; - -/** - * Manage the current logged in user session. - */ -@Singleton -public class SessionManager { - private final Context context; - private Account currentAccount; // Unlike a savings account... ;-) - private JsonKvStore defaultKvStore; - - @Inject - public SessionManager(Context context, - @Named("default_preferences") JsonKvStore defaultKvStore) { - this.context = context; - this.currentAccount = null; - this.defaultKvStore = defaultKvStore; - } - - private boolean createAccount(@NonNull String userName, @NonNull String password) { - Account account = getCurrentAccount(); - if (account == null || TextUtils.isEmpty(account.name) || !account.name.equals(userName)) { - removeAccount(); - account = new Account(userName, BuildConfig.ACCOUNT_TYPE); - return accountManager().addAccountExplicitly(account, password, null); - } - return true; - } - - private void removeAccount() { - Account account = getCurrentAccount(); - if (account != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - accountManager().removeAccountExplicitly(account); - } else { - //noinspection deprecation - accountManager().removeAccount(account, null, null); - } - } - } - - public void updateAccount(LoginResult result) { - boolean accountCreated = createAccount(result.getUserName(), result.getPassword()); - if (accountCreated) { - setPassword(result.getPassword()); - } - } - - private void setPassword(@NonNull String password) { - Account account = getCurrentAccount(); - if (account != null) { - accountManager().setPassword(account, password); - } - } - - /** - * @return Account|null - */ - @Nullable - public Account getCurrentAccount() { - if (currentAccount == null) { - AccountManager accountManager = AccountManager.get(context); - Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE); - if (allAccounts.length != 0) { - currentAccount = allAccounts[0]; - } - } - return currentAccount; - } - - public boolean doesAccountExist() { - return getCurrentAccount() != null; - } - - @Nullable - public String getUserName() { - Account account = getCurrentAccount(); - return account == null ? null : account.name; - } - - @Nullable - public String getPassword() { - Account account = getCurrentAccount(); - return account == null ? null : accountManager().getPassword(account); - } - - private AccountManager accountManager() { - return AccountManager.get(context); - } - - public boolean isUserLoggedIn() { - return defaultKvStore.getBoolean("isUserLoggedIn", false); - } - - void setUserLoggedIn(boolean isLoggedIn) { - defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn); - } - - public void forceLogin(Context context) { - if (context != null) { - LoginActivity.startYourself(context); - } - } - - /** - * Returns a Completable that clears existing accounts from account manager - */ - public Completable logout() { - return Completable.fromObservable( - Observable.empty() - .doOnComplete( - () -> { - removeAccount(); - currentAccount = null; - } - ) - ); - } - - /** - * Return a corresponding boolean preference - * - * @param key - * @return - */ - public boolean getPreference(String key) { - return defaultKvStore.getBoolean(key); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt new file mode 100644 index 000000000..eba4a55f4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt @@ -0,0 +1,95 @@ +package fr.free.nrw.commons.auth + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.os.Build +import android.text.TextUtils +import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE +import fr.free.nrw.commons.auth.login.LoginResult +import fr.free.nrw.commons.kvstore.JsonKvStore +import io.reactivex.Completable +import io.reactivex.Observable +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +/** + * Manage the current logged in user session. + */ +@Singleton +class SessionManager @Inject constructor( + private val context: Context, + @param:Named("default_preferences") private val defaultKvStore: JsonKvStore +) { + private val accountManager: AccountManager get() = AccountManager.get(context) + + private var _currentAccount: Account? = null // Unlike a savings account... ;-) + val currentAccount: Account? get() { + if (_currentAccount == null) { + val allAccounts = AccountManager.get(context).getAccountsByType(ACCOUNT_TYPE) + if (allAccounts.isNotEmpty()) { + _currentAccount = allAccounts[0] + } + } + return _currentAccount + } + + val userName: String? + get() = currentAccount?.name + + var password: String? + get() = currentAccount?.let { accountManager.getPassword(it) } + private set(value) { + currentAccount?.let { accountManager.setPassword(it, value) } + } + + val isUserLoggedIn: Boolean + get() = defaultKvStore.getBoolean("isUserLoggedIn", false) + + fun updateAccount(result: LoginResult) { + if (createAccount(result.userName!!, result.password!!)) { + password = result.password + } + } + + fun doesAccountExist(): Boolean = + currentAccount != null + + fun setUserLoggedIn(isLoggedIn: Boolean) = + defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn) + + fun forceLogin(context: Context?) = + context?.let { LoginActivity.startYourself(it) } + + fun getPreference(key: String?): Boolean = + defaultKvStore.getBoolean(key) + + fun logout(): Completable = Completable.fromObservable( + Observable.empty() + .doOnComplete { + removeAccount() + _currentAccount = null + } + ) + + private fun createAccount(userName: String, password: String): Boolean { + var account = currentAccount + if (account == null || TextUtils.isEmpty(account.name) || account.name != userName) { + removeAccount() + account = Account(userName, ACCOUNT_TYPE) + return accountManager.addAccountExplicitly(account, password, null) + } + return true + } + + private fun removeAccount() { + currentAccount?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + accountManager.removeAccountExplicitly(it) + } else { + accountManager.removeAccount(it, null, null) + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java deleted file mode 100644 index be90bb4bb..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java +++ /dev/null @@ -1,82 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.content.res.Configuration; -import android.os.Build; -import android.os.Bundle; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.Toast; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.theme.BaseActivity; -import timber.log.Timber; - -public class SignupActivity extends BaseActivity { - - private WebView webView; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Timber.d("Signup Activity started"); - - webView = new WebView(this); - setContentView(webView); - - webView.setWebViewClient(new MyWebViewClient()); - WebSettings webSettings = webView.getSettings(); - /*Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can - trust Wikimedia's site... right?*/ - webSettings.setJavaScriptEnabled(true); - - webView.loadUrl(BuildConfig.SIGNUP_LANDING_URL); - } - - private class MyWebViewClient extends WebViewClient { - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url.equals(BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL)) { - //Signup success, so clear cookies, notify user, and load LoginActivity again - Timber.d("Overriding URL %s", url); - - Toast toast = Toast.makeText(SignupActivity.this, - R.string.account_created, Toast.LENGTH_LONG); - toast.show(); - // terminate on task completion. - finish(); - return true; - } else { - //If user clicks any other links in the webview - Timber.d("Not overriding URL, URL is: %s", url); - return false; - } - } - } - - @Override - public void onBackPressed() { - if (webView.canGoBack()) { - webView.goBack(); - } else { - super.onBackPressed(); - } - } - - /** - * Known bug in androidx.appcompat library version 1.1.0 being tracked here - * https://issuetracker.google.com/issues/141132133 - * App tries to put light/dark theme to webview and crashes in the process - * This code tries to prevent applying the theme when sdk is between api 21 to 25 - * @param overrideConfiguration - */ - @Override - public void applyOverrideConfiguration(final Configuration overrideConfiguration) { - if (Build.VERSION.SDK_INT <= 25 && - (getResources().getConfiguration().uiMode == getApplicationContext().getResources().getConfiguration().uiMode)) { - return; - } - super.applyOverrideConfiguration(overrideConfiguration); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt new file mode 100644 index 000000000..5b48ecd8f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt @@ -0,0 +1,75 @@ +package fr.free.nrw.commons.auth + +import android.annotation.SuppressLint +import android.content.res.Configuration +import android.os.Build +import android.os.Bundle +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.R +import fr.free.nrw.commons.theme.BaseActivity +import timber.log.Timber + +class SignupActivity : BaseActivity() { + private var webView: WebView? = null + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Timber.d("Signup Activity started") + + webView = WebView(this) + with(webView!!) { + setContentView(this) + webViewClient = MyWebViewClient() + // Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can + // trust Wikimedia's site... right? + settings.javaScriptEnabled = true + loadUrl(BuildConfig.SIGNUP_LANDING_URL) + } + } + + override fun onBackPressed() { + if (webView!!.canGoBack()) { + webView!!.goBack() + } else { + super.onBackPressed() + } + } + + /** + * Known bug in androidx.appcompat library version 1.1.0 being tracked here + * https://issuetracker.google.com/issues/141132133 + * App tries to put light/dark theme to webview and crashes in the process + * This code tries to prevent applying the theme when sdk is between api 21 to 25 + */ + override fun applyOverrideConfiguration(overrideConfiguration: Configuration) { + if (Build.VERSION.SDK_INT <= 25 && + (resources.configuration.uiMode == applicationContext.resources.configuration.uiMode) + ) return + super.applyOverrideConfiguration(overrideConfiguration) + } + + private inner class MyWebViewClient : WebViewClient() { + @Deprecated("Deprecated in Java") + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean = + if (url == BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL) { + //Signup success, so clear cookies, notify user, and load LoginActivity again + Timber.d("Overriding URL %s", url) + + Toast.makeText( + this@SignupActivity, R.string.account_created, Toast.LENGTH_LONG + ).show() + + // terminate on task completion. + finish() + true + } else { + //If user clicks any other links in the webview + Timber.d("Not overriding URL, URL is: %s", url) + false + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java deleted file mode 100644 index 643725604..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java +++ /dev/null @@ -1,141 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.AbstractAccountAuthenticator; -import android.accounts.Account; -import android.accounts.AccountAuthenticatorResponse; -import android.accounts.AccountManager; -import android.accounts.NetworkErrorException; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.BuildConfig; - -import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; - -/** - * Handles WikiMedia commons account Authentication - */ -public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { - private static final String[] SYNC_AUTHORITIES = {BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY}; - - @NonNull - private final Context context; - - public WikiAccountAuthenticator(@NonNull Context context) { - super(context); - this.context = context; - } - - /** - * Provides Bundle with edited Account Properties - */ - @Override - public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { - Bundle bundle = new Bundle(); - bundle.putString("test", "editProperties"); - return bundle; - } - - @Override - public Bundle addAccount(@NonNull AccountAuthenticatorResponse response, - @NonNull String accountType, @Nullable String authTokenType, - @Nullable String[] requiredFeatures, @Nullable Bundle options) - throws NetworkErrorException { - // account type not supported returns bundle without loginActivity Intent, it just contains "test" key - if (!supportedAccountType(accountType)) { - Bundle bundle = new Bundle(); - bundle.putString("test", "addAccount"); - return bundle; - } - - return addAccount(response); - } - - @Override - public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @Nullable Bundle options) - throws NetworkErrorException { - Bundle bundle = new Bundle(); - bundle.putString("test", "confirmCredentials"); - return bundle; - } - - @Override - public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @NonNull String authTokenType, - @Nullable Bundle options) - throws NetworkErrorException { - Bundle bundle = new Bundle(); - bundle.putString("test", "getAuthToken"); - return bundle; - } - - @Nullable - @Override - public String getAuthTokenLabel(@NonNull String authTokenType) { - return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null; - } - - @Nullable - @Override - public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @Nullable String authTokenType, - @Nullable Bundle options) - throws NetworkErrorException { - Bundle bundle = new Bundle(); - bundle.putString("test", "updateCredentials"); - return bundle; - } - - @Nullable - @Override - public Bundle hasFeatures(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @NonNull String[] features) - throws NetworkErrorException { - Bundle bundle = new Bundle(); - bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); - return bundle; - } - - private boolean supportedAccountType(@Nullable String type) { - return BuildConfig.ACCOUNT_TYPE.equals(type); - } - - /** - * Provides a bundle containing a Parcel - * the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type) - */ - private Bundle addAccount(AccountAuthenticatorResponse response) { - Intent intent = new Intent(context, LoginActivity.class); - intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); - - Bundle bundle = new Bundle(); - bundle.putParcelable(AccountManager.KEY_INTENT, intent); - - return bundle; - } - - @Override - public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response, - Account account) throws NetworkErrorException { - Bundle result = super.getAccountRemovalAllowed(response, account); - - if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) - && !result.containsKey(AccountManager.KEY_INTENT)) { - boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); - - if (allowed) { - for (String auth : SYNC_AUTHORITIES) { - ContentResolver.cancelSync(account, auth); - } - } - } - - return result; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt new file mode 100644 index 000000000..367989f14 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt @@ -0,0 +1,108 @@ +package fr.free.nrw.commons.auth + +import android.accounts.AbstractAccountAuthenticator +import android.accounts.Account +import android.accounts.AccountAuthenticatorResponse +import android.accounts.AccountManager +import android.accounts.NetworkErrorException +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.os.bundleOf +import fr.free.nrw.commons.BuildConfig + +private val SYNC_AUTHORITIES = arrayOf( + BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY +) + +/** + * Handles WikiMedia commons account Authentication + */ +class WikiAccountAuthenticator( + private val context: Context +) : AbstractAccountAuthenticator(context) { + /** + * Provides Bundle with edited Account Properties + */ + override fun editProperties( + response: AccountAuthenticatorResponse, + accountType: String + ) = bundleOf("test" to "editProperties") + + // account type not supported returns bundle without loginActivity Intent, it just contains "test" key + @Throws(NetworkErrorException::class) + override fun addAccount( + response: AccountAuthenticatorResponse, + accountType: String, + authTokenType: String?, + requiredFeatures: Array?, + options: Bundle? + ) = if (BuildConfig.ACCOUNT_TYPE == accountType) { + addAccount(response) + } else { + bundleOf("test" to "addAccount") + } + + @Throws(NetworkErrorException::class) + override fun confirmCredentials( + response: AccountAuthenticatorResponse, account: Account, options: Bundle? + ) = bundleOf("test" to "confirmCredentials") + + @Throws(NetworkErrorException::class) + override fun getAuthToken( + response: AccountAuthenticatorResponse, + account: Account, + authTokenType: String, + options: Bundle? + ) = bundleOf("test" to "getAuthToken") + + override fun getAuthTokenLabel(authTokenType: String) = + if (BuildConfig.ACCOUNT_TYPE == authTokenType) AUTH_TOKEN_TYPE else null + + @Throws(NetworkErrorException::class) + override fun updateCredentials( + response: AccountAuthenticatorResponse, + account: Account, + authTokenType: String?, + options: Bundle? + ) = bundleOf("test" to "updateCredentials") + + @Throws(NetworkErrorException::class) + override fun hasFeatures( + response: AccountAuthenticatorResponse, + account: Account, features: Array + ) = bundleOf(AccountManager.KEY_BOOLEAN_RESULT to false) + + /** + * Provides a bundle containing a Parcel + * the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type) + */ + private fun addAccount(response: AccountAuthenticatorResponse): Bundle { + val intent = Intent(context, LoginActivity::class.java) + .putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) + return bundleOf(AccountManager.KEY_INTENT to intent) + } + + @Throws(NetworkErrorException::class) + override fun getAccountRemovalAllowed( + response: AccountAuthenticatorResponse?, + account: Account? + ): Bundle { + val result = super.getAccountRemovalAllowed(response, account) + + if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) + && !result.containsKey(AccountManager.KEY_INTENT) + ) { + val allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT) + + if (allowed) { + for (auth in SYNC_AUTHORITIES) { + ContentResolver.cancelSync(account, auth) + } + } + } + + return result + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java deleted file mode 100644 index bb41f27aa..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java +++ /dev/null @@ -1,31 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.AbstractAccountAuthenticator; -import android.content.Intent; -import android.os.IBinder; - -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.di.CommonsDaggerService; - -/** - * Handles the Auth service of the App, see AndroidManifests for details - * (Uses Dagger 2 as injector) - */ -public class WikiAccountAuthenticatorService extends CommonsDaggerService { - - @Nullable - private AbstractAccountAuthenticator authenticator; - - @Override - public void onCreate() { - super.onCreate(); - authenticator = new WikiAccountAuthenticator(this); - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return authenticator == null ? null : authenticator.getIBinder(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt new file mode 100644 index 000000000..852536a48 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt @@ -0,0 +1,22 @@ +package fr.free.nrw.commons.auth + +import android.accounts.AbstractAccountAuthenticator +import android.content.Intent +import android.os.IBinder +import fr.free.nrw.commons.di.CommonsDaggerService + +/** + * Handles the Auth service of the App, see AndroidManifests for details + * (Uses Dagger 2 as injector) + */ +class WikiAccountAuthenticatorService : CommonsDaggerService() { + private var authenticator: AbstractAccountAuthenticator? = null + + override fun onCreate() { + super.onCreate() + authenticator = WikiAccountAuthenticator(this) + } + + override fun onBind(intent: Intent): IBinder? = + authenticator?.iBinder +} 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 cd7324c63..3f9344184 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 @@ -14,7 +14,6 @@ import dagger.Module; import dagger.Provides; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao; @@ -114,11 +113,6 @@ public class CommonsApplicationModule { return byName; } - @Provides - public AccountUtil providesAccountUtil(Context context) { - return new AccountUtil(); - } - /** * Provides an instance of CategoryContentProviderClient i.e. the categories * that are there in local storage diff --git a/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java b/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java index 1723da723..2a4b612c0 100644 --- a/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java +++ b/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java @@ -2,7 +2,7 @@ package fr.free.nrw.commons.feedback; import android.content.Context; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AccountUtil; +import fr.free.nrw.commons.auth.AccountUtilKt; import fr.free.nrw.commons.feedback.model.Feedback; import fr.free.nrw.commons.utils.LangCodeUtils; import java.util.Locale; @@ -43,7 +43,7 @@ public class FeedbackContentCreator { sectionTitleBuilder = new StringBuilder(); sectionTitleBuilder.append("Feedback from "); - sectionTitleBuilder.append(AccountUtil.getUserName(context)); + sectionTitleBuilder.append(AccountUtilKt.getUserName(context)); sectionTitleBuilder.append(" for version "); sectionTitleBuilder.append(feedback.getVersion()); sectionTitleBuilder.append(" on "); 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 ed20809ac..66f2221b8 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 @@ -53,7 +53,7 @@ import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.actions.ThanksClient; -import fr.free.nrw.commons.auth.AccountUtil; +import fr.free.nrw.commons.auth.AccountUtilKt; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; import fr.free.nrw.commons.category.CategoryClient; @@ -382,8 +382,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements enableProgressBar(); } - if (AccountUtil.getUserName(getContext()) != null && media != null - && AccountUtil.getUserName(getContext()).equals(media.getAuthor())) { + if (AccountUtilKt.getUserName(getContext()) != null && media != null + && AccountUtilKt.getUserName(getContext()).equals(media.getAuthor())) { binding.sendThanks.setVisibility(GONE); } else { binding.sendThanks.setVisibility(VISIBLE); @@ -485,7 +485,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements } private void onDeletionPageExists(Boolean deletionPageExists) { - if (AccountUtil.getUserName(getContext()) == null && !AccountUtil.getUserName(getContext()).equals(media.getAuthor())) { + if (AccountUtilKt.getUserName(getContext()) == null && !AccountUtilKt.getUserName(getContext()).equals(media.getAuthor())) { binding.nominateDeletion.setVisibility(GONE); binding.nominatedDeletionBanner.setVisibility(GONE); } else if (deletionPageExists) { @@ -1079,7 +1079,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements @SuppressLint("StringFormatInvalid") public void onDeleteButtonClicked(){ - if (AccountUtil.getUserName(getContext()) != null && AccountUtil.getUserName(getContext()).equals(media.getAuthor())) { + if (AccountUtilKt.getUserName(getContext()) != null && AccountUtilKt.getUserName(getContext()).equals(media.getAuthor())) { final ArrayAdapter languageAdapter = new ArrayAdapter<>(getActivity(), R.layout.simple_spinner_dropdown_list, reasonList); final Spinner spinner = new Spinner(getActivity()); @@ -1105,7 +1105,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements //Reviewer correct me if i have misunderstood something over here //But how does this if (delete.getVisibility() == View.VISIBLE) { // enableDeleteButton(true); makes sense ? - else if (AccountUtil.getUserName(getContext()) != null) { + else if (AccountUtilKt.getUserName(getContext()) != null) { final EditText input = new EditText(getActivity()); input.requestFocus(); AlertDialog d = DialogUtil.showAlertDialog(getActivity(), diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt index 1d87a8f82..1547f89ad 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt @@ -96,7 +96,7 @@ class NotificationActivity : BaseActivity() { } }, { throwable -> if (throwable is InvalidLoginTokenException) { - val username = sessionManager.getUserName() + val username = sessionManager.userName val logoutListener = CommonsApplication.BaseLogoutListener( this, getString(R.string.invalid_login_message), diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt index 44b0f9bc1..cd2cbc8ad 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt @@ -11,7 +11,7 @@ import android.view.MotionEvent import android.view.View import fr.free.nrw.commons.Media import fr.free.nrw.commons.R -import fr.free.nrw.commons.auth.AccountUtil +import fr.free.nrw.commons.auth.getUserName import fr.free.nrw.commons.databinding.ActivityReviewBinding import fr.free.nrw.commons.delete.DeleteHelper import fr.free.nrw.commons.media.MediaDetailFragment @@ -183,7 +183,7 @@ class ReviewActivity : BaseActivity() { } //If The Media User and Current Session Username is same then Skip the Image - if (media.user == AccountUtil.getUserName(applicationContext)) { + if (media.user == getUserName(applicationContext)) { runRandomizer() return } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt index c0e5097c0..dbbab7359 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt @@ -63,7 +63,7 @@ class FailedUploadsFragment : } if (StringUtils.isEmpty(userName)) { - userName = sessionManager.getUserName() + userName = sessionManager.userName } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt index dd06452f9..7e7275049 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt @@ -19,7 +19,7 @@ class AbstractTextWatcher( // No-op } - interface TextChange { + fun interface TextChange { fun onTextChanged(value: String) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt index 84ec5a2cb..4c38a30ff 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt @@ -6,7 +6,6 @@ import android.content.Context import androidx.collection.LruCache import com.google.gson.Gson import com.nhaarman.mockitokotlin2.mock -import fr.free.nrw.commons.auth.AccountUtil import fr.free.nrw.commons.data.DBOpenHelper import fr.free.nrw.commons.di.CommonsApplicationComponent import fr.free.nrw.commons.di.CommonsApplicationModule @@ -41,7 +40,6 @@ class TestCommonsApplication : Application() { class MockCommonsApplicationModule( appContext: Context, ) : CommonsApplicationModule(appContext) { - val accountUtil: AccountUtil = mock() val defaultSharedPreferences: JsonKvStore = mock() val locationServiceManager: LocationServiceManager = mock() val mockDbOpenHelper: DBOpenHelper = mock() @@ -58,8 +56,6 @@ class MockCommonsApplicationModule( override fun provideModificationContentProviderClient(context: Context?): ContentProviderClient = modificationClient - override fun providesAccountUtil(context: Context): AccountUtil = accountUtil - override fun providesDefaultKvStore( context: Context, gson: Gson, diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/AccountUtilUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/AccountUtilUnitTest.kt index b45b844c0..566484a02 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/AccountUtilUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/AccountUtilUnitTest.kt @@ -15,25 +15,17 @@ import org.robolectric.annotation.Config @Config(sdk = [21], application = TestCommonsApplication::class) class AccountUtilUnitTest { private lateinit var context: FakeContextWrapper - private lateinit var accountUtil: AccountUtil @Before @Throws(Exception::class) fun setUp() { context = FakeContextWrapper(ApplicationProvider.getApplicationContext()) - accountUtil = AccountUtil() - } - - @Test - @Throws(Exception::class) - fun checkNotNull() { - Assert.assertNotNull(accountUtil) } @Test @Throws(Exception::class) fun testGetUserName() { - Assert.assertEquals(AccountUtil.getUserName(context), "test@example.com") + Assert.assertEquals(getUserName(context), "test@example.com") } @Test @@ -41,13 +33,13 @@ class AccountUtilUnitTest { fun testGetUserNameWithException() { val context = FakeContextWrapperWithException(ApplicationProvider.getApplicationContext()) - Assert.assertEquals(AccountUtil.getUserName(context), null) + Assert.assertEquals(getUserName(context), null) } @Test @Throws(Exception::class) fun testAccount() { - Assert.assertEquals(AccountUtil.account(context)?.name, "test@example.com") + Assert.assertEquals(account(context)?.name, "test@example.com") } @Test @@ -55,6 +47,6 @@ class AccountUtilUnitTest { fun testAccountWithException() { val context = FakeContextWrapperWithException(ApplicationProvider.getApplicationContext()) - Assert.assertEquals(AccountUtil.account(context), null) + Assert.assertEquals(account(context), null) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt index 162f50584..871613ca5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt @@ -218,17 +218,6 @@ class LoginActivityUnitTests { method.invoke(activity) } - @Test - @Throws(Exception::class) - fun testHideProgress() { - val method: Method = - LoginActivity::class.java.getDeclaredMethod( - "hideProgress", - ) - method.isAccessible = true - method.invoke(activity) - } - @Test @Throws(Exception::class) fun testOnResume() { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorServiceUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorServiceUnitTest.kt index c1c209462..370c74a47 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorServiceUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorServiceUnitTest.kt @@ -3,18 +3,13 @@ package fr.free.nrw.commons.auth import org.junit.Assert import org.junit.Before import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations import java.lang.reflect.Field class WikiAccountAuthenticatorServiceUnitTest { - private lateinit var service: WikiAccountAuthenticatorService - - @Before - fun setUp() { - MockitoAnnotations.openMocks(this) - service = WikiAccountAuthenticatorService() - service.onBind(null) - } + private val service = WikiAccountAuthenticatorService() @Test fun checkNotNull() { @@ -23,10 +18,9 @@ class WikiAccountAuthenticatorServiceUnitTest { @Test fun testOnBindCaseNull() { - val field: Field = - WikiAccountAuthenticatorService::class.java.getDeclaredField("authenticator") + val field: Field = WikiAccountAuthenticatorService::class.java.getDeclaredField("authenticator") field.isAccessible = true field.set(service, null) - Assert.assertEquals(service.onBind(null), null) + Assert.assertEquals(service.onBind(mock()), null) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorUnitTest.kt index 856e5015d..d5e24790b 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorUnitTest.kt @@ -86,10 +86,7 @@ class WikiAccountAuthenticatorUnitTest { @Test fun testGetAuthTokenLabelCaseNonNull() { - Assert.assertEquals( - authenticator.getAuthTokenLabel(BuildConfig.ACCOUNT_TYPE), - AccountUtil.AUTH_TOKEN_TYPE, - ) + Assert.assertEquals(authenticator.getAuthTokenLabel(BuildConfig.ACCOUNT_TYPE), AUTH_TOKEN_TYPE) } @Test From 794dbb8f9273654ff0bf70ba83288b7ead7d2fd4 Mon Sep 17 00:00:00 2001 From: Rohit Verma <101377978+rohit9625@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:07:56 +0530 Subject: [PATCH 43/74] Fix modification on bottom sheet's data when coming from Nearby Banner and clicked on other pins (#5937) * refactor getIconFor method and remove call to highlightNearestPlace() It ensures single responsibility on getIconFor method * refactor and fix icon issue when coming from nearby banner --------- Co-authored-by: Nicolas Raoul --- .../fragments/NearbyParentFragment.java | 43 +++++++++++-------- .../main/res/layout/bottom_sheet_details.xml | 2 +- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index fdbc727bc..5da4e5478 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -965,6 +965,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment lastPlaceToCenter.location.getLatitude() - cameraShift, lastPlaceToCenter.getLocation().getLongitude(), 0)); } + highlightNearestPlace(place); } @@ -2001,7 +2002,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment * * @param nearestPlace nearest place, which has to be highlighted */ - private void highlightNearestPlace(Place nearestPlace) { + private void highlightNearestPlace(final Place nearestPlace) { + binding.bottomSheetDetails.icon.setVisibility(View.VISIBLE); passInfoToSheet(nearestPlace); hideBottomSheet(); bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); @@ -2015,32 +2017,37 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment * @return returns the drawable of marker according to the place information */ private @DrawableRes int getIconFor(Place place, Boolean isBookmarked) { - if (nearestPlace != null) { - if (place.name.equals(nearestPlace.name)) { - // Highlight nearest place only when user clicks on the home nearby banner - highlightNearestPlace(place); - return (isBookmarked ? - R.drawable.ic_custom_map_marker_purple_bookmarked : - R.drawable.ic_custom_map_marker_purple); - } + if (nearestPlace != null && place.name.equals(nearestPlace.name)) { + // Highlight nearest place only when user clicks on the home nearby banner +// highlightNearestPlace(place); + return (isBookmarked ? + R.drawable.ic_custom_map_marker_purple_bookmarked : + R.drawable.ic_custom_map_marker_purple + ); } + if (place.isMonument()) { return R.drawable.ic_custom_map_marker_monuments; - } else if (!place.pic.trim().isEmpty()) { + } + if (!place.pic.trim().isEmpty()) { return (isBookmarked ? R.drawable.ic_custom_map_marker_green_bookmarked : - R.drawable.ic_custom_map_marker_green); - } else if (!place.exists) { // Means that the topic of the Wikidata item does not exist in the real world anymore, for instance it is a past event, or a place that was destroyed + R.drawable.ic_custom_map_marker_green + ); + } + if (!place.exists) { // Means that the topic of the Wikidata item does not exist in the real world anymore, for instance it is a past event, or a place that was destroyed return (R.drawable.ic_clear_black_24dp); - }else if (place.name == "") { + } + if (place.name.isEmpty()) { return (isBookmarked ? R.drawable.ic_custom_map_marker_grey_bookmarked : - R.drawable.ic_custom_map_marker_grey); - } else { - return (isBookmarked ? - R.drawable.ic_custom_map_marker_red_bookmarked : - R.drawable.ic_custom_map_marker_red); + R.drawable.ic_custom_map_marker_grey + ); } + return (isBookmarked ? + R.drawable.ic_custom_map_marker_red_bookmarked : + R.drawable.ic_custom_map_marker_red + ); } /** diff --git a/app/src/main/res/layout/bottom_sheet_details.xml b/app/src/main/res/layout/bottom_sheet_details.xml index f026528b6..77d31a967 100644 --- a/app/src/main/res/layout/bottom_sheet_details.xml +++ b/app/src/main/res/layout/bottom_sheet_details.xml @@ -32,7 +32,7 @@ android:layout_width="@dimen/dimen_40" android:layout_height="@dimen/dimen_40" android:layout_marginLeft="@dimen/standard_gap" - android:visibility="gone"> + android:visibility="gone" /> Date: Thu, 28 Nov 2024 13:01:55 +0100 Subject: [PATCH 44/74] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-es/strings.xml | 21 +++++++++++++++++++++ app/src/main/res/values-iw/strings.xml | 6 +++--- app/src/main/res/values-qq/strings.xml | 1 + 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4e90f6864..f02ea7dac 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -29,6 +29,7 @@ * Jduranboger * Jelou * Johnny243 +* Josuert * Juanman * Keneth Urrutia * Ktranz @@ -166,6 +167,7 @@ Buscar categorías Buscar elementos que tu archivo multimedia representa (montaña, Taj Mahal, etc.) Guardar + Menú de desbordamiento Actualizar Lista (No hay subidas aún) @@ -522,6 +524,7 @@ No tienes notificaciones sin leer No tienes ninguna notificación leída Compartir registros usando + Revisa tu bandeja de entrada Ver leídas Ver no leidas Ocurrió un error mientras se elegían imagenes @@ -819,8 +822,26 @@ Por favor, escriba algunos comentarios. Discusión Escriba algo sobre el elemento \'%1$s\'. Será visible públicamente. + \' %1$s \' ya no existe, nunca se podrá tomar ninguna fotografía de él. + \' %1$s \' está en un lugar diferente. Especifique el lugar correcto a continuación y, si es posible, escriba la latitud y longitud correctas. + Otro problema o información (por favor explique a continuación). + Sus comentarios se publicarán en la siguiente página wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile_app/Feedback</a> + ¿Estás seguro de que deseas cancelar todas las subidas? Cancelando todas las subidas... Subidas Pendiente Falló + No se pudieron cargar los datos del lugar + Eliminar carpeta + Confirmar eliminación + ¿Está seguro de que deseas eliminar la carpeta %1$s que contiene %2$d elementos? + Eliminar + Cancelar + La carpeta %1$s se eliminó correctamente + No se pudo eliminar la carpeta %1$s + Error al eliminar el contenido de la carpeta: %1$s + No se pudo recuperar la ruta de la carpeta para el ID del bucket: %1$d + Este lugar aún no tiene foto, ¡ve y toma una! + Este lugar ya tiene una foto. + Ahora comprobando si este lugar tiene una foto. diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index be45099a9..a77bdfea9 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -542,9 +542,9 @@ %1$s הועלה על ידי: %2$s שפת התיאור כבררת מחדל העמדה למחיקה - הצלחה + זה עבד הקובץ %1$s הועמד למחיקה. - כשלון + זה לא עבד לא ניתן לבקש מחיקה תמונה עצמית (סלפי) שלא משמשת בשום ערך תמונה מטושטשת לגמרי @@ -825,7 +825,7 @@ מחיקת תיקייה אישור מחיקה למחוק את התיקייה %1$s על כל %2$d פריטיה? - מחיקה + למחוק ביטול התיקייה %1$s נמחקה מחיקת התיקייה %1$s נכשלה diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index e06969dc2..833743aef 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -204,4 +204,5 @@ {{Identical|Detail}} \"Set as avatar\" should be translated the same as {{msg-wm|Commons-android-strings-menu set avatar}}. {{Doc-commons-app-depicts}} + An answer to the question in {{msg-wm|Commons-android-strings-custom selector confirm deletion message}}. From 1afff73c24a7c5bb7486a3846620cff337b98906 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Thu, 28 Nov 2024 22:44:26 -0600 Subject: [PATCH 45/74] Migrate campaigns package to kotlin (#5969) * Convert ICampaignsView to kotlin along with simple fix * Convert CampaignView to Kotlin * Convert CampaignsPresenter to Kotlin * Convert CampaignsPresenter to kotlin --------- Co-authored-by: Nicolas Raoul --- .../nrw/commons/campaigns/CampaignView.java | 118 ----------------- .../nrw/commons/campaigns/CampaignView.kt | 121 +++++++++++++++++ .../commons/campaigns/CampaignsPresenter.java | 123 ------------------ .../commons/campaigns/CampaignsPresenter.kt | 107 +++++++++++++++ .../nrw/commons/campaigns/ICampaignsView.java | 11 -- .../nrw/commons/campaigns/ICampaignsView.kt | 11 ++ .../campaigns/CampaignsPresenterTest.kt | 17 ++- 7 files changed, 247 insertions(+), 261 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.kt diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java deleted file mode 100644 index d1ee4c8b0..000000000 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java +++ /dev/null @@ -1,118 +0,0 @@ -package fr.free.nrw.commons.campaigns; - -import android.content.Context; -import android.net.Uri; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.campaigns.models.Campaign; -import fr.free.nrw.commons.databinding.LayoutCampaginBinding; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.CommonsDateUtil; - -import fr.free.nrw.commons.utils.DateUtil; -import java.text.ParseException; -import java.util.Date; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.utils.SwipableCardView; -import fr.free.nrw.commons.utils.ViewUtil; - -/** - * A view which represents a single campaign - */ -public class CampaignView extends SwipableCardView { - Campaign campaign; - private LayoutCampaginBinding binding; - private ViewHolder viewHolder; - - public static final String CAMPAIGNS_DEFAULT_PREFERENCE = "displayCampaignsCardView"; - public static final String WLM_CARD_PREFERENCE = "displayWLMCardView"; - - private String campaignPreference = CAMPAIGNS_DEFAULT_PREFERENCE; - - public CampaignView(@NonNull Context context) { - super(context); - init(); - } - - public CampaignView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - init(); - } - - public CampaignView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - - public void setCampaign(final Campaign campaign) { - this.campaign = campaign; - if (campaign != null) { - if (campaign.isWLMCampaign()) { - campaignPreference = WLM_CARD_PREFERENCE; - } - setVisibility(View.VISIBLE); - viewHolder.init(); - } else { - this.setVisibility(View.GONE); - } - } - - @Override public boolean onSwipe(final View view) { - view.setVisibility(View.GONE); - ((BaseActivity) getContext()).defaultKvStore - .putBoolean(CAMPAIGNS_DEFAULT_PREFERENCE, false); - ViewUtil.showLongToast(getContext(), - getResources().getString(R.string.nearby_campaign_dismiss_message)); - return true; - } - - private void init() { - binding = LayoutCampaginBinding.inflate(LayoutInflater.from(getContext()), this, true); - viewHolder = new ViewHolder(); - setOnClickListener(view -> { - if (campaign != null) { - if (campaign.isWLMCampaign()) { - ((MainActivity)(getContext())).showNearby(); - } else { - Utils.handleWebUrl(getContext(), Uri.parse(campaign.getLink())); - } - } - }); - } - - public class ViewHolder { - public void init() { - if (campaign != null) { - binding.ivCampaign.setImageDrawable( - getResources().getDrawable(R.drawable.ic_campaign)); - - binding.tvTitle.setText(campaign.getTitle()); - binding.tvDescription.setText(campaign.getDescription()); - try { - if (campaign.isWLMCampaign()) { - binding.tvDates.setText( - String.format("%1s - %2s", campaign.getStartDate(), - campaign.getEndDate())); - } else { - final Date startDate = CommonsDateUtil.getIso8601DateFormatShort() - .parse(campaign.getStartDate()); - final Date endDate = CommonsDateUtil.getIso8601DateFormatShort() - .parse(campaign.getEndDate()); - binding.tvDates.setText(String.format("%1s - %2s", DateUtil.getExtraShortDateString(startDate), - DateUtil.getExtraShortDateString(endDate))); - } - } catch (final ParseException e) { - e.printStackTrace(); - } - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.kt new file mode 100644 index 000000000..7a4720177 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.kt @@ -0,0 +1,121 @@ +package fr.free.nrw.commons.campaigns + +import android.content.Context +import android.net.Uri +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import androidx.core.content.ContextCompat +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.campaigns.models.Campaign +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.LayoutCampaginBinding +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.CommonsDateUtil.getIso8601DateFormatShort +import fr.free.nrw.commons.utils.DateUtil.getExtraShortDateString +import fr.free.nrw.commons.utils.SwipableCardView +import fr.free.nrw.commons.utils.ViewUtil.showLongToast +import timber.log.Timber +import java.text.ParseException + +/** + * A view which represents a single campaign + */ +class CampaignView : SwipableCardView { + private var campaign: Campaign? = null + private var binding: LayoutCampaginBinding? = null + private var viewHolder: ViewHolder? = null + private var campaignPreference = CAMPAIGNS_DEFAULT_PREFERENCE + + constructor(context: Context) : super(context) { + init() + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, attrs, defStyleAttr) { + init() + } + + fun setCampaign(campaign: Campaign?) { + this.campaign = campaign + if (campaign != null) { + if (campaign.isWLMCampaign) { + campaignPreference = WLM_CARD_PREFERENCE + } + visibility = VISIBLE + viewHolder!!.init() + } else { + visibility = GONE + } + } + + override fun onSwipe(view: View): Boolean { + view.visibility = GONE + (context as BaseActivity).defaultKvStore.putBoolean(CAMPAIGNS_DEFAULT_PREFERENCE, false) + showLongToast( + context, + resources.getString(R.string.nearby_campaign_dismiss_message) + ) + return true + } + + private fun init() { + binding = LayoutCampaginBinding.inflate( + LayoutInflater.from(context), this, true + ) + viewHolder = ViewHolder() + setOnClickListener { + campaign?.let { + if (it.isWLMCampaign) { + ((context) as MainActivity).showNearby() + } else { + Utils.handleWebUrl(context, Uri.parse(it.link)) + } + } + } + } + + inner class ViewHolder { + fun init() { + if (campaign != null) { + binding!!.ivCampaign.setImageDrawable( + ContextCompat.getDrawable(binding!!.root.context, R.drawable.ic_campaign) + ) + binding!!.tvTitle.text = campaign!!.title + binding!!.tvDescription.text = campaign!!.description + try { + if (campaign!!.isWLMCampaign) { + binding!!.tvDates.text = String.format( + "%1s - %2s", campaign!!.startDate, + campaign!!.endDate + ) + } else { + val startDate = getIso8601DateFormatShort().parse( + campaign?.startDate + ) + val endDate = getIso8601DateFormatShort().parse( + campaign?.endDate + ) + binding!!.tvDates.text = String.format( + "%1s - %2s", getExtraShortDateString( + startDate!! + ), getExtraShortDateString(endDate!!) + ) + } + } catch (e: ParseException) { + Timber.e(e) + } + } + } + } + + companion object { + const val CAMPAIGNS_DEFAULT_PREFERENCE: String = "displayCampaignsCardView" + const val WLM_CARD_PREFERENCE: String = "displayWLMCardView" + } +} 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 deleted file mode 100644 index 157047774..000000000 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java +++ /dev/null @@ -1,123 +0,0 @@ -package fr.free.nrw.commons.campaigns; - -import android.annotation.SuppressLint; - -import fr.free.nrw.commons.campaigns.models.Campaign; -import fr.free.nrw.commons.utils.CommonsDateUtil; -import java.text.ParseException; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - -import fr.free.nrw.commons.BasePresenter; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.Scheduler; -import io.reactivex.Single; -import io.reactivex.SingleObserver; -import io.reactivex.disposables.Disposable; -import timber.log.Timber; - -import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; -import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD; - -/** - * The presenter for the campaigns view, fetches the campaigns from the api and informs the view on - * success and error - */ -@Singleton -public class CampaignsPresenter implements BasePresenter { - private final OkHttpJsonApiClient okHttpJsonApiClient; - private final Scheduler mainThreadScheduler; - private final Scheduler ioScheduler; - - private ICampaignsView view; - private Disposable disposable; - private Campaign campaign; - - @Inject - public CampaignsPresenter(OkHttpJsonApiClient okHttpJsonApiClient, @Named(IO_THREAD)Scheduler ioScheduler, @Named(MAIN_THREAD)Scheduler mainThreadScheduler) { - this.okHttpJsonApiClient = okHttpJsonApiClient; - this.mainThreadScheduler=mainThreadScheduler; - this.ioScheduler=ioScheduler; - } - - @Override - public void onAttachView(ICampaignsView view) { - this.view = view; - } - - @Override public void onDetachView() { - this.view = null; - if (disposable != null) { - disposable.dispose(); - } - } - - /** - * make the api call to fetch the campaigns - */ - @SuppressLint("CheckResult") - public void getCampaigns() { - if (view != null && okHttpJsonApiClient != null) { - //If we already have a campaign, lets not make another call - if (this.campaign != null) { - view.showCampaigns(campaign); - return; - } - Single campaigns = okHttpJsonApiClient.getCampaigns(); - campaigns.observeOn(mainThreadScheduler) - .subscribeOn(ioScheduler) - .subscribeWith(new SingleObserver() { - - @Override public void onSubscribe(Disposable d) { - disposable = d; - } - - @Override public void onSuccess(CampaignResponseDTO campaignResponseDTO) { - List campaigns = campaignResponseDTO.getCampaigns(); - if (campaigns == null || campaigns.isEmpty()) { - Timber.e("The campaigns list is empty"); - view.showCampaigns(null); - return; - } - Collections.sort(campaigns, (campaign, t1) -> { - Date date1, date2; - try { - - date1 = CommonsDateUtil.getIso8601DateFormatShort().parse(campaign.getStartDate()); - date2 = CommonsDateUtil.getIso8601DateFormatShort().parse(t1.getStartDate()); - } catch (ParseException e) { - e.printStackTrace(); - return -1; - } - return date1.compareTo(date2); - }); - Date campaignEndDate, campaignStartDate; - Date currentDate = new Date(); - try { - for (Campaign aCampaign : campaigns) { - campaignEndDate = CommonsDateUtil.getIso8601DateFormatShort().parse(aCampaign.getEndDate()); - campaignStartDate = CommonsDateUtil.getIso8601DateFormatShort().parse(aCampaign.getStartDate()); - if (campaignEndDate.compareTo(currentDate) >= 0 - && campaignStartDate.compareTo(currentDate) <= 0) { - campaign = aCampaign; - break; - } - } - } catch (ParseException e) { - e.printStackTrace(); - } - view.showCampaigns(campaign); - } - - @Override public void onError(Throwable e) { - Timber.e(e, "could not fetch campaigns"); - } - }); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt new file mode 100644 index 000000000..3753dfb67 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt @@ -0,0 +1,107 @@ +package fr.free.nrw.commons.campaigns + +import android.annotation.SuppressLint +import fr.free.nrw.commons.BasePresenter +import fr.free.nrw.commons.campaigns.models.Campaign +import fr.free.nrw.commons.di.CommonsApplicationModule +import fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD +import fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.utils.CommonsDateUtil.getIso8601DateFormatShort +import io.reactivex.Scheduler +import io.reactivex.disposables.Disposable +import timber.log.Timber +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +/** + * The presenter for the campaigns view, fetches the campaigns from the api and informs the view on + * success and error + */ +@Singleton +class CampaignsPresenter @Inject constructor( + private val okHttpJsonApiClient: OkHttpJsonApiClient?, + @param:Named(IO_THREAD) private val ioScheduler: Scheduler, + @param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler +) : BasePresenter { + private var view: ICampaignsView? = null + private var disposable: Disposable? = null + private var campaign: Campaign? = null + + override fun onAttachView(view: ICampaignsView) { + this.view = view + } + + override fun onDetachView() { + view = null + disposable?.dispose() + } + + /** + * make the api call to fetch the campaigns + */ + @SuppressLint("CheckResult") + fun getCampaigns() { + if (view != null && okHttpJsonApiClient != null) { + //If we already have a campaign, lets not make another call + if (campaign != null) { + view!!.showCampaigns(campaign) + return + } + + okHttpJsonApiClient.campaigns + .observeOn(mainThreadScheduler) + .subscribeOn(ioScheduler) + .doOnSubscribe { disposable = it } + .subscribe({ campaignResponseDTO -> + val campaigns = campaignResponseDTO.campaigns?.toMutableList() + if (campaigns.isNullOrEmpty()) { + Timber.e("The campaigns list is empty") + view!!.showCampaigns(null) + } else { + sortCampaignsByStartDate(campaigns) + campaign = findActiveCampaign(campaigns) + view!!.showCampaigns(campaign) + } + }, { + Timber.e(it, "could not fetch campaigns") + }) + } + } + + private fun sortCampaignsByStartDate(campaigns: MutableList) { + val dateFormat: SimpleDateFormat = getIso8601DateFormatShort() + campaigns.sortWith(Comparator { campaign: Campaign, other: Campaign -> + val date1: Date? + val date2: Date? + try { + date1 = campaign.startDate?.let { dateFormat.parse(it) } + date2 = other.startDate?.let { dateFormat.parse(it) } + } catch (e: ParseException) { + Timber.e(e) + return@Comparator -1 + } + if (date1 != null && date2 != null) date1.compareTo(date2) else -1 + }) + } + + private fun findActiveCampaign(campaigns: List) : Campaign? { + val dateFormat: SimpleDateFormat = getIso8601DateFormatShort() + val currentDate = Date() + return try { + campaigns.firstOrNull { + val campaignStartDate = it.startDate?.let { s -> dateFormat.parse(s) } + val campaignEndDate = it.endDate?.let { s -> dateFormat.parse(s) } + campaignStartDate != null && campaignEndDate != null && + campaignEndDate >= currentDate && campaignStartDate <= currentDate + } + } catch (e: ParseException) { + Timber.e(e, "could not find active campaign") + null + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java b/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java deleted file mode 100644 index a1e79cca6..000000000 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java +++ /dev/null @@ -1,11 +0,0 @@ -package fr.free.nrw.commons.campaigns; - -import fr.free.nrw.commons.MvpView; -import fr.free.nrw.commons.campaigns.models.Campaign; - -/** - * Interface which defines the view contracts of the campaign view - */ -public interface ICampaignsView extends MvpView { - void showCampaigns(Campaign campaign); -} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.kt new file mode 100644 index 000000000..62a19aaac --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.kt @@ -0,0 +1,11 @@ +package fr.free.nrw.commons.campaigns + +import fr.free.nrw.commons.MvpView +import fr.free.nrw.commons.campaigns.models.Campaign + +/** + * Interface which defines the view contracts of the campaign view + */ +interface ICampaignsView : MvpView { + fun showCampaigns(campaign: Campaign?) +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt index 7efdfd1ad..ec3ad82f1 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt @@ -20,25 +20,24 @@ import kotlin.collections.ArrayList class CampaignsPresenterTest { @Mock - lateinit var okHttpJsonApiClient: OkHttpJsonApiClient - - lateinit var campaignsPresenter: CampaignsPresenter + private lateinit var okHttpJsonApiClient: OkHttpJsonApiClient @Mock - internal lateinit var view: ICampaignsView + private lateinit var view: ICampaignsView @Mock - internal lateinit var campaignResponseDTO: CampaignResponseDTO - lateinit var campaignsSingle: Single + private lateinit var campaignResponseDTO: CampaignResponseDTO @Mock - lateinit var campaign: Campaign - - lateinit var testScheduler: TestScheduler + private lateinit var campaign: Campaign @Mock private lateinit var disposable: Disposable + private lateinit var campaignsPresenter: CampaignsPresenter + private lateinit var campaignsSingle: Single + private lateinit var testScheduler: TestScheduler + /** * initial setup, test environment */ From d6c4cab207fd93104faef67f85c2f84f781e275f Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Fri, 29 Nov 2024 10:20:33 +0530 Subject: [PATCH 46/74] Migrated logging module from Java to Kotlin (#5972) * Migrated logging module from Java to Kotlin * Rename .java to .kt --------- Co-authored-by: Nicolas Raoul --- .../nrw/commons/logging/CommonsLogSender.java | 105 --------- .../nrw/commons/logging/CommonsLogSender.kt | 107 ++++++++++ .../nrw/commons/logging/FileLoggingTree.java | 145 ------------- .../nrw/commons/logging/FileLoggingTree.kt | 133 ++++++++++++ .../commons/logging/LogLevelSettableTree.java | 8 - .../commons/logging/LogLevelSettableTree.kt | 8 + .../fr/free/nrw/commons/logging/LogUtils.java | 48 ----- .../fr/free/nrw/commons/logging/LogUtils.kt | 57 +++++ .../free/nrw/commons/logging/LogsSender.java | 201 ------------------ .../fr/free/nrw/commons/logging/LogsSender.kt | 193 +++++++++++++++++ .../nrw/commons/settings/SettingsFragment.kt | 3 +- 11 files changed, 500 insertions(+), 508 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt diff --git a/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java b/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java deleted file mode 100644 index 29c2c732e..000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java +++ /dev/null @@ -1,105 +0,0 @@ -package fr.free.nrw.commons.logging; - -import android.content.Context; - -import android.os.Bundle; -import javax.inject.Inject; -import javax.inject.Singleton; - -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.utils.DeviceInfoUtil; -import org.acra.data.CrashReportData; -import org.acra.sender.ReportSenderException; -import org.jetbrains.annotations.NotNull; - -/** - * Class responsible for sending logs to developers - */ -@Singleton -public class CommonsLogSender extends LogsSender { - private static final String LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com"; - private static final String LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Android App (%s) Logs"; - private static final String BETA_LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Beta Android App (%s) Logs"; - - private SessionManager sessionManager; - private Context context; - - @Inject - public CommonsLogSender(SessionManager sessionManager, - Context context) { - super(sessionManager); - - this.sessionManager = sessionManager; - this.context = context; - boolean isBeta = ConfigUtils.isBetaFlavour(); - this.logFileName = isBeta ? "CommonsBetaAppLogs.zip" : "CommonsAppLogs.zip"; - String emailSubjectFormat = isBeta ? BETA_LOGS_PRIVATE_EMAIL_SUBJECT : LOGS_PRIVATE_EMAIL_SUBJECT; - this.emailSubject = String.format(emailSubjectFormat, sessionManager.getUserName()); - this.emailBody = getExtraInfo(); - this.mailTo = LOGS_PRIVATE_EMAIL; - } - - /** - * Attach any extra meta information about user or device that might help in debugging - * @return String with extra meta information useful for debugging - */ - @Override - public String getExtraInfo() { - StringBuilder builder = new StringBuilder(); - - // Getting API Level - builder.append("API level: ") - .append(DeviceInfoUtil.getAPILevel()) - .append("\n"); - - // Getting Android Version - builder.append("Android version: ") - .append(DeviceInfoUtil.getAndroidVersion()) - .append("\n"); - - // Getting Device Manufacturer - builder.append("Device manufacturer: ") - .append(DeviceInfoUtil.getDeviceManufacturer()) - .append("\n"); - - // Getting Device Model - builder.append("Device model: ") - .append(DeviceInfoUtil.getDeviceModel()) - .append("\n"); - - // Getting Device Name - builder.append("Device: ") - .append(DeviceInfoUtil.getDevice()) - .append("\n"); - - // Getting Network Type - builder.append("Network type: ") - .append(DeviceInfoUtil.getConnectionType(context)) - .append("\n"); - - // Getting App Version - builder.append("App version name: ") - .append(ConfigUtils.getVersionNameWithSha(context)) - .append("\n"); - - // Getting Username - builder.append("User name: ") - .append(sessionManager.getUserName()) - .append("\n"); - - - return builder.toString(); - } - - @Override - public boolean requiresForeground() { - return false; - } - - @Override - public void send(@NotNull Context context, @NotNull CrashReportData crashReportData, - @NotNull Bundle bundle) throws ReportSenderException { - - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.kt b/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.kt new file mode 100644 index 000000000..7c6b988a6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.kt @@ -0,0 +1,107 @@ +package fr.free.nrw.commons.logging + +import android.content.Context + +import android.os.Bundle +import javax.inject.Inject +import javax.inject.Singleton + +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.utils.ConfigUtils +import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha +import fr.free.nrw.commons.utils.DeviceInfoUtil +import org.acra.data.CrashReportData + + +/** + * Class responsible for sending logs to developers + */ +@Singleton +class CommonsLogSender @Inject constructor( + private val sessionManager: SessionManager, + private val context: Context +) : LogsSender(sessionManager) { + + + companion object { + private const val LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com" + private const val LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Android App (%s) Logs" + private const val BETA_LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Beta Android App (%s) Logs" + } + + init { + val isBeta = ConfigUtils.isBetaFlavour + logFileName = if (isBeta) "CommonsBetaAppLogs.zip" else "CommonsAppLogs.zip" + val emailSubjectFormat = if (isBeta) + BETA_LOGS_PRIVATE_EMAIL_SUBJECT + else + LOGS_PRIVATE_EMAIL_SUBJECT + emailSubject = emailSubjectFormat.format(sessionManager.userName) + emailBody = getExtraInfo() + mailTo = LOGS_PRIVATE_EMAIL + } + + /** + * Attach any extra meta information about the user or device that might help in debugging. + * @return String with extra meta information useful for debugging. + */ + public override fun getExtraInfo(): String { + return buildString { + // Getting API Level + append("API level: ") + .append(DeviceInfoUtil.getAPILevel()) + .append("\n") + + // Getting Android Version + append("Android version: ") + .append(DeviceInfoUtil.getAndroidVersion()) + .append("\n") + + // Getting Device Manufacturer + append("Device manufacturer: ") + .append(DeviceInfoUtil.getDeviceManufacturer()) + .append("\n") + + // Getting Device Model + append("Device model: ") + .append(DeviceInfoUtil.getDeviceModel()) + .append("\n") + + // Getting Device Name + append("Device: ") + .append(DeviceInfoUtil.getDevice()) + .append("\n") + + // Getting Network Type + append("Network type: ") + .append(DeviceInfoUtil.getConnectionType(context)) + .append("\n") + + // Getting App Version + append("App version name: ") + .append(context.getVersionNameWithSha()) + .append("\n") + + // Getting Username + append("User name: ") + .append(sessionManager.userName) + .append("\n") + } + } + + /** + * Determines if the log sending process requires the app to be in the foreground. + * @return False as it does not require foreground execution. + */ + override fun requiresForeground(): Boolean = false + + /** + * Sends logs to developers. Implementation can be extended. + */ + override fun send( + context: Context, + errorContent: CrashReportData, + extras: Bundle) { + // Add logic here if needed. + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java b/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java deleted file mode 100644 index a2ebeec68..000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java +++ /dev/null @@ -1,145 +0,0 @@ -package fr.free.nrw.commons.logging; - -import android.util.Log; - -import androidx.annotation.NonNull; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Locale; -import java.util.concurrent.Executor; - -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.encoder.PatternLayoutEncoder; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.rolling.FixedWindowRollingPolicy; -import ch.qos.logback.core.rolling.RollingFileAppender; -import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy; -import timber.log.Timber; - -/** - * Extends Timber's debug tree to write logs to a file - */ -public class FileLoggingTree extends Timber.DebugTree implements LogLevelSettableTree { - private final Logger logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); - private int logLevel; - private final String logFileName; - private int fileSize; - private FixedWindowRollingPolicy rollingPolicy; - private final Executor executor; - - public FileLoggingTree(int logLevel, - String logFileName, - String logDirectory, - int fileSizeInKb, - Executor executor) { - this.logLevel = logLevel; - this.logFileName = logFileName; - this.fileSize = fileSizeInKb; - configureLogger(logDirectory); - this.executor = executor; - } - - /** - * Can be overridden to change file's log level - * @param logLevel - */ - @Override - public void setLogLevel(int logLevel) { - this.logLevel = logLevel; - } - - /** - * Check and log any message - * @param priority - * @param tag - * @param message - * @param t - */ - @Override - protected void log(final int priority, final String tag, @NonNull final String message, Throwable t) { - executor.execute(() -> logMessage(priority, tag, message)); - - } - - /** - * Log any message based on the priority - * @param priority - * @param tag - * @param message - */ - private void logMessage(int priority, String tag, String message) { - String messageWithTag = String.format("[%s] : %s", tag, message); - switch (priority) { - case Log.VERBOSE: - logger.trace(messageWithTag); - break; - case Log.DEBUG: - logger.debug(messageWithTag); - break; - case Log.INFO: - logger.info(messageWithTag); - break; - case Log.WARN: - logger.warn(messageWithTag); - break; - case Log.ERROR: - logger.error(messageWithTag); - break; - case Log.ASSERT: - logger.error(messageWithTag); - break; - } - } - - /** - * Checks if a particular log line should be logged in the file or not - * @param priority - * @return - */ - @Override - protected boolean isLoggable(int priority) { - return priority >= logLevel; - } - - /** - * Configures the logger with a file size rolling policy (SizeBasedTriggeringPolicy) - * https://github.com/tony19/logback-android/wiki - * @param logDir - */ - private void configureLogger(String logDir) { - LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); - loggerContext.reset(); - - RollingFileAppender rollingFileAppender = new RollingFileAppender<>(); - rollingFileAppender.setContext(loggerContext); - rollingFileAppender.setFile(logDir + "/" + logFileName + ".0.log"); - - rollingPolicy = new FixedWindowRollingPolicy(); - rollingPolicy.setContext(loggerContext); - rollingPolicy.setMinIndex(1); - rollingPolicy.setMaxIndex(4); - rollingPolicy.setParent(rollingFileAppender); - rollingPolicy.setFileNamePattern(logDir + "/" + logFileName + ".%i.log"); - rollingPolicy.start(); - - SizeBasedTriggeringPolicy triggeringPolicy = new SizeBasedTriggeringPolicy<>(); - triggeringPolicy.setContext(loggerContext); - triggeringPolicy.setMaxFileSize(String.format(Locale.ENGLISH, "%dKB", fileSize)); - triggeringPolicy.start(); - - PatternLayoutEncoder encoder = new PatternLayoutEncoder(); - encoder.setContext(loggerContext); - encoder.setPattern("%-27(%date{ISO8601}) [%-5level] [%thread] %msg%n"); - encoder.start(); - - rollingFileAppender.setEncoder(encoder); - rollingFileAppender.setRollingPolicy(rollingPolicy); - rollingFileAppender.setTriggeringPolicy(triggeringPolicy); - rollingFileAppender.start(); - ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) - LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); - logger.addAppender(rollingFileAppender); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt b/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt new file mode 100644 index 000000000..5c6c55f1a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt @@ -0,0 +1,133 @@ +package fr.free.nrw.commons.logging + +import android.util.Log + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.Locale +import java.util.concurrent.Executor + +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.rolling.FixedWindowRollingPolicy +import ch.qos.logback.core.rolling.RollingFileAppender +import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy +import timber.log.Timber + + +/** + * Extends Timber's debug tree to write logs to a file. + */ +class FileLoggingTree( + private var logLevel: Int, + private val logFileName: String, + logDirectory: String, + private val fileSizeInKb: Int, + private val executor: Executor +) : Timber.DebugTree(), LogLevelSettableTree { + + private val logger: Logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) + private lateinit var rollingPolicy: FixedWindowRollingPolicy + + init { + configureLogger(logDirectory) + } + + /** + * Can be overridden to change the file's log level. + * @param logLevel The new log level. + */ + override fun setLogLevel(logLevel: Int) { + this.logLevel = logLevel + } + + /** + * Checks and logs any message. + * @param priority The priority of the log message. + * @param tag The tag associated with the log message. + * @param message The log message. + * @param t An optional throwable. + */ + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + executor.execute { + logMessage(priority, tag.orEmpty(), message) + } + } + + /** + * Logs a message based on the priority. + * @param priority The priority of the log message. + * @param tag The tag associated with the log message. + * @param message The log message. + */ + private fun logMessage(priority: Int, tag: String, message: String) { + val messageWithTag = "[$tag] : $message" + when (priority) { + Log.VERBOSE -> logger.trace(messageWithTag) + Log.DEBUG -> logger.debug(messageWithTag) + Log.INFO -> logger.info(messageWithTag) + Log.WARN -> logger.warn(messageWithTag) + Log.ERROR, Log.ASSERT -> logger.error(messageWithTag) + } + } + + /** + * Checks if a particular log line should be logged in the file or not. + * @param priority The priority of the log message. + * @return True if the log message should be logged, false otherwise. + */ + @Deprecated("Deprecated in Java") + override fun isLoggable(priority: Int): Boolean { + return priority >= logLevel + } + + /** + * Configures the logger with a file size rolling policy (SizeBasedTriggeringPolicy). + * https://github.com/tony19/logback-android/wiki + * @param logDir The directory where logs should be stored. + */ + private fun configureLogger(logDir: String) { + val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext + loggerContext.reset() + + val rollingFileAppender = RollingFileAppender().apply { + context = loggerContext + file = "$logDir/$logFileName.0.log" + } + + rollingPolicy = FixedWindowRollingPolicy().apply { + context = loggerContext + minIndex = 1 + maxIndex = 4 + setParent(rollingFileAppender) + fileNamePattern = "$logDir/$logFileName.%i.log" + start() + } + + val triggeringPolicy = SizeBasedTriggeringPolicy().apply { + context = loggerContext + maxFileSize = "$fileSizeInKb" + start() + } + + val encoder = PatternLayoutEncoder().apply { + context = loggerContext + pattern = "%-27(%date{ISO8601}) [%-5level] [%thread] %msg%n" + start() + } + + rollingFileAppender.apply { + this.encoder = encoder + rollingPolicy = rollingPolicy + this.triggeringPolicy = triggeringPolicy + start() + } + + val rootLogger = LoggerFactory.getLogger( + Logger.ROOT_LOGGER_NAME + ) as ch.qos.logback.classic.Logger + rootLogger.addAppender(rollingFileAppender) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java b/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java deleted file mode 100644 index 5eeca6d3e..000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java +++ /dev/null @@ -1,8 +0,0 @@ -package fr.free.nrw.commons.logging; - -/** - * Can be implemented to set the log level for file tree - */ -public interface LogLevelSettableTree { - void setLogLevel(int logLevel); -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.kt b/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.kt new file mode 100644 index 000000000..babe78121 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.kt @@ -0,0 +1,8 @@ +package fr.free.nrw.commons.logging + +/** + * Can be implemented to set the log level for file tree + */ +interface LogLevelSettableTree { + fun setLogLevel(logLevel: Int) +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java b/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java deleted file mode 100644 index c28b2145b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java +++ /dev/null @@ -1,48 +0,0 @@ -package fr.free.nrw.commons.logging; - -import android.os.Environment; - -import fr.free.nrw.commons.upload.FileUtils; -import fr.free.nrw.commons.utils.ConfigUtils; - -/** - * Returns the log directory - */ -public final class LogUtils { - private LogUtils() { - } - - /** - * Returns the directory for saving logs on the device - * - * @return - */ - public static String getLogDirectory() { - String dirPath; - if (ConfigUtils.isBetaFlavour()) { - dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/beta"; - } else { - dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/prod"; - } - - FileUtils.recursivelyCreateDirs(dirPath); - return dirPath; - } - - /** - * Returns the directory for saving logs on the device - * - * @return - */ - public static String getLogZipDirectory() { - String dirPath; - if (ConfigUtils.isBetaFlavour()) { - dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/beta/zip"; - } else { - dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/prod/zip"; - } - - FileUtils.recursivelyCreateDirs(dirPath); - return dirPath; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt b/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt new file mode 100644 index 000000000..6c91d92dd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt @@ -0,0 +1,57 @@ +package fr.free.nrw.commons.logging + +import android.os.Environment + +import fr.free.nrw.commons.upload.FileUtils +import fr.free.nrw.commons.utils.ConfigUtils + + +/** + * Returns the log directory + */ +object LogUtils { + + /** + * Returns the directory for saving logs on the device. + * + * @return The path to the log directory. + */ + fun getLogDirectory(): String { + val dirPath = if (ConfigUtils.isBetaFlavour) { + "${Environment + .getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + )}/logs/beta" + } else { + "${Environment + .getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + )}/logs/prod" + } + + FileUtils.recursivelyCreateDirs(dirPath) + return dirPath + } + + /** + * Returns the directory for saving zipped logs on the device. + * + * @return The path to the zipped log directory. + */ + fun getLogZipDirectory(): String { + val dirPath = if (ConfigUtils.isBetaFlavour) { + "${Environment + .getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + )}/logs/beta/zip" + } else { + "${Environment + .getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + )}/logs/prod/zip" + } + + FileUtils.recursivelyCreateDirs(dirPath) + return dirPath + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java b/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java deleted file mode 100644 index 68f7bd78c..000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java +++ /dev/null @@ -1,201 +0,0 @@ -package fr.free.nrw.commons.logging; - -import static org.acra.ACRA.getErrorReporter; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.FileProvider; - -import org.acra.data.CrashReportData; -import org.acra.sender.ReportSender; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; -import timber.log.Timber; - -/** - * Abstract class that implements Acra's log sender - */ -public abstract class LogsSender implements ReportSender { - - String mailTo; - String logFileName; - String emailSubject; - String emailBody; - - private final SessionManager sessionManager; - - LogsSender(SessionManager sessionManager) { - this.sessionManager = sessionManager; - } - - /** - * Overrides send method of ACRA's ReportSender to send logs - * - * @param context - * @param report - */ - @Override - public void send(@NonNull final Context context, @Nullable CrashReportData report) { - sendLogs(context, report); - } - - /** - * Gets zipped log files and sends it via email. Can be modified to change the send log mechanism - * - * @param context - * @param report - */ - private void sendLogs(Context context, CrashReportData report) { - final Uri logFileUri = getZippedLogFileUri(context, report); - if (logFileUri != null) { - sendEmail(context, logFileUri); - } else { - getErrorReporter().handleSilentException(null); - } - } - - /*** - * Provides any extra information that you want to send. The return value will be - * delivered inside the report verbatim - * - * @return - */ - protected abstract String getExtraInfo(); - - /** - * Fires an intent to send email with logs - * - * @param context - * @param logFileUri - */ - private void sendEmail(Context context, Uri logFileUri) { - String subject = emailSubject; - String body = emailBody; - - Intent emailIntent = new Intent(Intent.ACTION_SEND); - emailIntent.setType("message/rfc822"); - emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{mailTo}); - emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject); - emailIntent.putExtra(Intent.EXTRA_TEXT, body); - emailIntent.putExtra(Intent.EXTRA_STREAM, logFileUri); - emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - context.startActivity(Intent.createChooser(emailIntent, context.getString(R.string.share_logs_using))); - } - - /** - * Returns the URI for the zipped log file - * - * @param report - * @return - */ - private Uri getZippedLogFileUri(Context context, CrashReportData report) { - try { - StringBuilder builder = new StringBuilder(); - if (report != null) { - attachCrashInfo(report, builder); - } - attachUserInfo(builder); - attachExtraInfo(builder); - byte[] metaData = builder.toString().getBytes(Charset.forName("UTF-8")); - File zipFile = new File(LogUtils.getLogZipDirectory(), logFileName); - writeLogToZipFile(metaData, zipFile); - return FileProvider - .getUriForFile(context, - context.getApplicationContext().getPackageName() + ".provider", zipFile); - } catch (IOException e) { - Timber.w(e, "Error in generating log file"); - } - return null; - } - - /** - * Checks if there are any pending crash reports and attaches them to the logs - * - * @param report - * @param builder - */ - private void attachCrashInfo(CrashReportData report, StringBuilder builder) { - if (report == null) { - return; - } - builder.append(report); - } - - /** - * Attaches username to the the meta_data file - * - * @param builder - */ - private void attachUserInfo(StringBuilder builder) { - builder.append("MediaWiki Username = ").append(sessionManager.getUserName()).append("\n"); - } - - /** - * Gets any extra meta information to be attached with the log files - * - * @param builder - */ - private void attachExtraInfo(StringBuilder builder) { - String infoToBeAttached = getExtraInfo(); - builder.append(infoToBeAttached); - builder.append("\n"); - } - - /** - * Zips the logs and meta information - * - * @param metaData - * @param zipFile - * @throws IOException - */ - private void writeLogToZipFile(byte[] metaData, File zipFile) throws IOException { - FileOutputStream fos = new FileOutputStream(zipFile); - BufferedOutputStream bos = new BufferedOutputStream(fos); - ZipOutputStream zos = new ZipOutputStream(bos); - File logDir = new File(LogUtils.getLogDirectory()); - - if (!logDir.exists() || logDir.listFiles().length == 0) { - return; - } - - byte[] buffer = new byte[1024]; - for (File file : logDir.listFiles()) { - if (file.isDirectory()) { - continue; - } - FileInputStream fis = new FileInputStream(file); - BufferedInputStream bis = new BufferedInputStream(fis); - zos.putNextEntry(new ZipEntry(file.getName())); - int length; - while ((length = bis.read(buffer)) > 0) { - zos.write(buffer, 0, length); - } - zos.closeEntry(); - bis.close(); - } - - //attach metadata as a separate file - zos.putNextEntry(new ZipEntry("meta_data.txt")); - zos.write(metaData); - zos.closeEntry(); - - zos.flush(); - zos.close(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt b/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt new file mode 100644 index 000000000..cd6bb7d70 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt @@ -0,0 +1,193 @@ +package fr.free.nrw.commons.logging + +import android.content.Context +import android.content.Intent +import android.net.Uri + +import androidx.core.content.FileProvider + +import org.acra.data.CrashReportData +import org.acra.sender.ReportSender + +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.SessionManager +import org.acra.ACRA.errorReporter +import timber.log.Timber + + +/** + * Abstract class that implements Acra's log sender. + */ +abstract class LogsSender( + private val sessionManager: SessionManager +): ReportSender { + + var mailTo: String? = null + var logFileName: String? = null + var emailSubject: String? = null + var emailBody: String? = null + + /** + * Overrides the send method of ACRA's ReportSender to send logs. + * + * @param context The context in which to send the logs. + * @param report The crash report data, if any. + */ + fun sendWithNullable(context: Context, report: CrashReportData?) { + if (report == null) { + errorReporter.handleSilentException(null) + return + } + send(context, report) + } + + override fun send(context: Context, report: CrashReportData) { + sendLogs(context, report) + } + + /** + * Gets zipped log files and sends them via email. Can be modified to change the send + * log mechanism. + * + * @param context The context in which to send the logs. + * @param report The crash report data, if any. + */ + private fun sendLogs(context: Context, report: CrashReportData?) { + val logFileUri = getZippedLogFileUri(context, report) + if (logFileUri != null) { + sendEmail(context, logFileUri) + } else { + errorReporter.handleSilentException(null) + + } + } + + /** + * Provides any extra information that you want to send. The return value will be + * delivered inside the report verbatim. + * + * @return A string containing the extra information. + */ + protected abstract fun getExtraInfo(): String + + /** + * Fires an intent to send an email with logs. + * + * @param context The context in which to send the email. + * @param logFileUri The URI of the zipped log file. + */ + private fun sendEmail(context: Context, logFileUri: Uri) { + val emailIntent = Intent(Intent.ACTION_SEND).apply { + type = "message/rfc822" + putExtra(Intent.EXTRA_EMAIL, arrayOf(mailTo)) + putExtra(Intent.EXTRA_SUBJECT, emailSubject) + putExtra(Intent.EXTRA_TEXT, emailBody) + putExtra(Intent.EXTRA_STREAM, logFileUri) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + context.startActivity(Intent.createChooser(emailIntent, context.getString(R.string.share_logs_using))) + } + + /** + * Returns the URI for the zipped log file. + * + * @param context The context for file URI generation. + * @param report The crash report data, if any. + * @return The URI of the zipped log file or null if an error occurs. + */ + private fun getZippedLogFileUri(context: Context, report: CrashReportData?): Uri? { + return try { + val builder = StringBuilder().apply { + report?.let { attachCrashInfo(it, this) } + attachUserInfo(this) + attachExtraInfo(this) + } + val metaData = builder.toString().toByteArray(Charsets.UTF_8) + val zipFile = File(LogUtils.getLogZipDirectory(), logFileName ?: "logs.zip") + writeLogToZipFile(metaData, zipFile) + FileProvider.getUriForFile( + context, + "${context.applicationContext.packageName}.provider", + zipFile + ) + } catch (e: IOException) { + Timber.w(e, "Error in generating log file") + null + } + } + + /** + * Checks if there are any pending crash reports and attaches them to the logs. + * + * @param report The crash report data, if any. + * @param builder The string builder to append crash info. + */ + private fun attachCrashInfo(report: CrashReportData?, builder: StringBuilder) { + if(report != null) { + builder.append(report) + } + } + + /** + * Attaches the username to the metadata file. + * + * @param builder The string builder to append user info. + */ + private fun attachUserInfo(builder: StringBuilder) { + builder.append("MediaWiki Username = ").append(sessionManager.userName).append("\n") + } + + /** + * Gets any extra metadata information to be attached with the log files. + * + * @param builder The string builder to append extra info. + */ + private fun attachExtraInfo(builder: StringBuilder) { + builder.append(getExtraInfo()).append("\n") + } + + /** + * Zips the logs and metadata information. + * + * @param metaData The metadata to be added to the zip file. + * @param zipFile The zip file to write to. + * @throws IOException If an I/O error occurs. + */ + @Throws(IOException::class) + private fun writeLogToZipFile(metaData: ByteArray, zipFile: File) { + val logDir = File(LogUtils.getLogDirectory()) + if (!logDir.exists() || logDir.listFiles().isNullOrEmpty()) return + + ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { zos -> + val buffer = ByteArray(1024) + logDir.listFiles()?.forEach { file -> + if (file.isDirectory) return@forEach + FileInputStream(file).use { fis -> + BufferedInputStream(fis).use { bis -> + zos.putNextEntry(ZipEntry(file.name)) + var length: Int + while (bis.read(buffer).also { length = it } > 0) { + zos.write(buffer, 0, length) + } + zos.closeEntry() + } + } + } + + // Attach metadata as a separate file. + zos.putNextEntry(ZipEntry("meta_data.txt")) + zos.write(metaData) + zos.closeEntry() + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt index b55ac6009..86ee5c4fe 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt @@ -1,6 +1,7 @@ package fr.free.nrw.commons.settings import android.Manifest.permission +import android.annotation.SuppressLint import android.app.Activity import android.app.Dialog import android.content.Context.MODE_PRIVATE @@ -527,7 +528,7 @@ class SettingsFragment : PreferenceFragmentCompat() { PermissionUtils.PERMISSIONS_STORAGE ) ) { - commonsLogSender.send(requireActivity(), null) + commonsLogSender.sendWithNullable(requireActivity(), null) } else { requestExternalStoragePermissions() } From dac3657536b0a258b29056e6022a399956dfc939 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Thu, 28 Nov 2024 23:01:29 -0600 Subject: [PATCH 47/74] Migrate kvstore to kotlin (#5973) * Get good test around the basic KvStore before starting conversion * Converted BasicKvStore to kotlin and removed dead code * Converted JsonKvStore to kotlin --------- Co-authored-by: Nicolas Raoul --- .../free/nrw/commons/auth/SessionManager.kt | 2 +- .../nrw/commons/kvstore/BasicKvStore.java | 215 ---------------- .../free/nrw/commons/kvstore/BasicKvStore.kt | 152 +++++++++++ .../free/nrw/commons/kvstore/JsonKvStore.java | 68 ----- .../free/nrw/commons/kvstore/JsonKvStore.kt | 52 ++++ .../nrw/commons/kvstore/KeyValueStore.java | 35 --- .../free/nrw/commons/kvstore/KeyValueStore.kt | 33 +++ .../nrw/commons/utils/SystemThemeUtils.kt | 2 +- .../nrw/commons/kvstore/BasicKvStoreTest.kt | 238 ++++++++++++++++++ .../nrw/commons/kvstore/JsonKvStoreTest.kt | 85 +++++++ 10 files changed, 562 insertions(+), 320 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.java create mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.java create mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.java create mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/kvstore/BasicKvStoreTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/kvstore/JsonKvStoreTest.kt diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt index eba4a55f4..c9eb7d2f1 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt @@ -62,7 +62,7 @@ class SessionManager @Inject constructor( fun forceLogin(context: Context?) = context?.let { LoginActivity.startYourself(it) } - fun getPreference(key: String?): Boolean = + fun getPreference(key: String): Boolean = defaultKvStore.getBoolean(key) fun logout(): Completable = Completable.fromObservable( diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.java b/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.java deleted file mode 100644 index 032898896..000000000 --- a/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.java +++ /dev/null @@ -1,215 +0,0 @@ -package fr.free.nrw.commons.kvstore; - -import android.content.Context; -import android.content.SharedPreferences; - -import androidx.annotation.Nullable; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import timber.log.Timber; - -public class BasicKvStore implements KeyValueStore { - private static final String KEY_VERSION = "__version__"; - /* - This class only performs puts, sets and clears. - A commit returns a boolean indicating whether it has succeeded, we are not throwing an exception as it will - require the dev to handle it in every usage - instead we will pass on this boolean so it can be evaluated if needed. - */ - private final SharedPreferences _store; - - public BasicKvStore(Context context, String storeName) { - _store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE); - } - - /** - * If you don't want onVersionUpdate to be called on a fresh creation, the first version supplied for the kvstore should be set to 0. - */ - public BasicKvStore(Context context, String storeName, int version) { - this(context,storeName,version,false); - } - - public BasicKvStore(Context context, String storeName, int version, boolean clearAllOnUpgrade) { - _store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE); - int oldVersion = getInt(KEY_VERSION); - - if (version > oldVersion) { - Timber.i("version updated from %s to %s, with clearFlag %b", oldVersion, version, clearAllOnUpgrade); - onVersionUpdate(oldVersion, version, clearAllOnUpgrade); - } - - if (version < oldVersion) { - throw new IllegalArgumentException( - "kvstore downgrade not allowed, old version:" + oldVersion + ", new version: " + - version); - } - //Keep this statement at the end so that clearing of store does not cause version also to get removed. - putIntInternal(KEY_VERSION, version); - } - - public void onVersionUpdate(int oldVersion, int version, boolean clearAllFlag) { - if(clearAllFlag) { - clearAll(); - } - } - - public Set getKeySet() { - Map allContents = new HashMap<>(_store.getAll()); - allContents.remove(KEY_VERSION); - return allContents.keySet(); - } - - @Nullable - public Map getAll() { - Map allContents = _store.getAll(); - if (allContents == null || allContents.size() == 0) { - return null; - } - allContents.remove(KEY_VERSION); - return new HashMap<>(allContents); - } - - @Override - public String getString(String key) { - return getString(key, null); - } - - @Override - public boolean getBoolean(String key) { - return getBoolean(key, false); - } - - @Override - public long getLong(String key) { - return getLong(key, 0); - } - - @Override - public int getInt(String key) { - return getInt(key, 0); - } - - @Override - public String getString(String key, String defaultValue) { - return _store.getString(key, defaultValue); - } - - @Override - public boolean getBoolean(String key, boolean defaultValue) { - return _store.getBoolean(key, defaultValue); - } - - @Override - public long getLong(String key, long defaultValue) { - return _store.getLong(key, defaultValue); - } - - @Override - public int getInt(String key, int defaultValue) { - return _store.getInt(key, defaultValue); - } - - public void putAllStrings(Map keyValuePairs) { - SharedPreferences.Editor editor = _store.edit(); - for (Map.Entry keyValuePair : keyValuePairs.entrySet()) { - putString(editor, keyValuePair.getKey(), keyValuePair.getValue(), false); - } - editor.apply(); - } - - @Override - public void putString(String key, String value) { - SharedPreferences.Editor editor = _store.edit(); - putString(editor, key, value, true); - } - - private void putString(SharedPreferences.Editor editor, String key, String value, - boolean commit) { - assertKeyNotReserved(key); - editor.putString(key, value); - if(commit) { - editor.apply(); - } - } - - @Override - public void putBoolean(String key, boolean value) { - assertKeyNotReserved(key); - SharedPreferences.Editor editor = _store.edit(); - editor.putBoolean(key, value); - editor.apply(); - } - - @Override - public void putLong(String key, long value) { - assertKeyNotReserved(key); - SharedPreferences.Editor editor = _store.edit(); - editor.putLong(key, value); - editor.apply(); - } - - @Override - public void putInt(String key, int value) { - assertKeyNotReserved(key); - putIntInternal(key, value); - } - - @Override - public boolean contains(String key) { - return _store.contains(key); - } - - @Override - public void remove(String key) { - SharedPreferences.Editor editor = _store.edit(); - editor.remove(key); - editor.apply(); - } - - @Override - public void clearAll() { - int version = getInt(KEY_VERSION); - SharedPreferences.Editor editor = _store.edit(); - editor.clear(); - editor.apply(); - putIntInternal(KEY_VERSION, version); - } - - @Override - public void clearAllWithVersion() { - SharedPreferences.Editor editor = _store.edit(); - editor.clear(); - editor.apply(); - } - - private void putIntInternal(String key, int value) { - SharedPreferences.Editor editor = _store.edit(); - editor.putInt(key, value); - editor.apply(); - } - - private void assertKeyNotReserved(String key) { - if (key.equals(KEY_VERSION)) { - throw new IllegalArgumentException(key + "is a reserved key"); - } - } - - public void registerChangeListener(SharedPreferences.OnSharedPreferenceChangeListener l) { - _store.registerOnSharedPreferenceChangeListener(l); - } - - public void unregisterChangeListener(SharedPreferences.OnSharedPreferenceChangeListener l) { - _store.unregisterOnSharedPreferenceChangeListener(l); - } - - public Set getStringSet(String key){ - return _store.getStringSet(key, new HashSet<>()); - } - - public void putStringSet(String key,Set value){ - _store.edit().putStringSet(key,value).apply(); - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt b/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt new file mode 100644 index 000000000..e0b860164 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt @@ -0,0 +1,152 @@ +package fr.free.nrw.commons.kvstore + +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.VisibleForTesting +import androidx.core.content.edit +import timber.log.Timber + +open class BasicKvStore : KeyValueStore { + /* + This class only performs puts, sets and clears. + A commit returns a boolean indicating whether it has succeeded, we are not throwing an exception as it will + require the dev to handle it in every usage - instead we will pass on this boolean so it can be evaluated if needed. + */ + private val _store: SharedPreferences + + constructor(context: Context, storeName: String?) { + _store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE) + } + + /** + * If you don't want onVersionUpdate to be called on a fresh creation, the first version supplied for the kvstore should be set to 0. + */ + @JvmOverloads + constructor( + context: Context, + storeName: String?, + version: Int, + clearAllOnUpgrade: Boolean = false + ) { + _store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE) + val oldVersion = _store.getInt(KEY_VERSION, 0) + + require(version >= oldVersion) { + "kvstore downgrade not allowed, old version:" + oldVersion + ", new version: " + + version + } + + if (version > oldVersion) { + Timber.i( + "version updated from %s to %s, with clearFlag %b", + oldVersion, + version, + clearAllOnUpgrade + ) + onVersionUpdate(oldVersion, version, clearAllOnUpgrade) + } + + //Keep this statement at the end so that clearing of store does not cause version also to get removed. + _store.edit { putInt(KEY_VERSION, version) } + } + + val all: Map? + get() { + val allContents = _store.all + if (allContents == null || allContents.isEmpty()) { + return null + } + allContents.remove(KEY_VERSION) + return HashMap(allContents) + } + + override fun getString(key: String): String? = + getString(key, null) + + override fun getBoolean(key: String): Boolean = + getBoolean(key, false) + + override fun getLong(key: String): Long = + getLong(key, 0) + + override fun getInt(key: String): Int = + getInt(key, 0) + + fun getStringSet(key: String?): MutableSet = + _store.getStringSet(key, HashSet())!! + + override fun getString(key: String, defaultValue: String?): String? = + _store.getString(key, defaultValue) + + override fun getBoolean(key: String, defaultValue: Boolean): Boolean = + _store.getBoolean(key, defaultValue) + + override fun getLong(key: String, defaultValue: Long): Long = + _store.getLong(key, defaultValue) + + override fun getInt(key: String, defaultValue: Int): Int = + _store.getInt(key, defaultValue) + + fun putAllStrings(kvData: Map) = assertKeyNotReserved(kvData.keys) { + for ((key, value) in kvData) { + putString(key, value) + } + } + + override fun putString(key: String, value: String) = assertKeyNotReserved(key) { + putString(key, value) + } + + override fun putBoolean(key: String, value: Boolean) = assertKeyNotReserved(key) { + putBoolean(key, value) + } + + override fun putLong(key: String, value: Long) = assertKeyNotReserved(key) { + putLong(key, value) + } + + override fun putInt(key: String, value: Int) = assertKeyNotReserved(key) { + putInt(key, value) + } + + fun putStringSet(key: String?, value: Set?) = + _store.edit{ putStringSet(key, value) } + + override fun remove(key: String) = assertKeyNotReserved(key) { + remove(key) + } + + override fun contains(key: String): Boolean { + if (key == KEY_VERSION) return false + return _store.contains(key) + } + + override fun clearAll() { + val version = _store.getInt(KEY_VERSION, 0) + _store.edit { + clear() + putInt(KEY_VERSION, version) + } + } + + private fun onVersionUpdate(oldVersion: Int, version: Int, clearAllFlag: Boolean) { + if (clearAllFlag) { + clearAll() + } + } + + protected fun assertKeyNotReserved(key: Set, block: SharedPreferences.Editor.() -> Unit) { + key.forEach { require(it != KEY_VERSION) { "$it is a reserved key" } } + _store.edit { block(this) } + } + + protected fun assertKeyNotReserved(key: String, block: SharedPreferences.Editor.() -> Unit) { + require(key != KEY_VERSION) { "$key is a reserved key" } + _store.edit { block(this) } + } + + companion object { + @VisibleForTesting + const val KEY_VERSION: String = "__version__" + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.java b/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.java deleted file mode 100644 index d612880d9..000000000 --- a/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.java +++ /dev/null @@ -1,68 +0,0 @@ -package fr.free.nrw.commons.kvstore; - -import android.content.Context; - -import androidx.annotation.Nullable; - -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - -import java.lang.reflect.Type; -import java.util.HashMap; -import java.util.Map; - -public class JsonKvStore extends BasicKvStore { - private final Gson gson; - - public JsonKvStore(Context context, String storeName, Gson gson) { - super(context, storeName); - this.gson = gson; - } - - public JsonKvStore(Context context, String storeName, int version, Gson gson) { - super(context, storeName, version); - this.gson = gson; - } - - public JsonKvStore(Context context, String storeName, int version, boolean clearAllOnUpgrade, Gson gson) { - super(context, storeName, version, clearAllOnUpgrade); - this.gson = gson; - } - - public void putAllJsons(Map jsonMap) { - Map stringsMap = new HashMap<>(jsonMap.size()); - for (Map.Entry keyValuePair : jsonMap.entrySet()) { - String jsonString = gson.toJson(keyValuePair.getValue()); - stringsMap.put(keyValuePair.getKey(), jsonString); - } - putAllStrings(stringsMap); - } - - public void putJson(String key, T object) { - putString(key, gson.toJson(object)); - } - - public void putJsonWithTypeInfo(String key, T object, Type type) { - putString(key, gson.toJson(object, type)); - } - - @Nullable - public T getJson(String key, Class clazz) { - String jsonString = getString(key); - try { - return gson.fromJson(jsonString, clazz); - } catch (JsonSyntaxException e) { - return null; - } - } - - @Nullable - public T getJson(String key, Type type) { - String jsonString = getString(key); - try { - return gson.fromJson(jsonString, type); - } catch (JsonSyntaxException e) { - return null; - } - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt b/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt new file mode 100644 index 000000000..0f46222a4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt @@ -0,0 +1,52 @@ +package fr.free.nrw.commons.kvstore + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException + +class JsonKvStore : BasicKvStore { + val gson: Gson + + constructor(context: Context, storeName: String?, gson: Gson) : super(context, storeName) { + this.gson = gson + } + + constructor(context: Context, storeName: String?, version: Int, gson: Gson) : super( + context, storeName, version + ) { + this.gson = gson + } + + constructor( + context: Context, + storeName: String?, + version: Int, + clearAllOnUpgrade: Boolean, + gson: Gson + ) : super(context, storeName, version, clearAllOnUpgrade) { + this.gson = gson + } + + fun putJson(key: String, value: T) = assertKeyNotReserved(key) { + putString(key, gson.toJson(value)) + } + + @Deprecated( + message = "Migrate to newer Kotlin syntax", + replaceWith = ReplaceWith("getJson(key)") + ) + fun getJson(key: String, clazz: Class?): T? = try { + gson.fromJson(getString(key), clazz) + } catch (e: JsonSyntaxException) { + null + } + + // Later, when the calls are coming from Kotlin, this will allow us to + // drop the "clazz" parameter, and just pick up the type at the call site. + // The deprecation warning should help migration! + inline fun getJson(key: String): T? = try { + gson.fromJson(getString(key), T::class.java) + } catch (e: JsonSyntaxException) { + null + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.java b/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.java deleted file mode 100644 index 46d6d8f81..000000000 --- a/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.java +++ /dev/null @@ -1,35 +0,0 @@ -package fr.free.nrw.commons.kvstore; - -public interface KeyValueStore { - String getString(String key); - - boolean getBoolean(String key); - - long getLong(String key); - - int getInt(String key); - - String getString(String key, String defaultValue); - - boolean getBoolean(String key, boolean defaultValue); - - long getLong(String key, long defaultValue); - - int getInt(String key, int defaultValue); - - void putString(String key, String value); - - void putBoolean(String key, boolean value); - - void putLong(String key, long value); - - void putInt(String key, int value); - - boolean contains(String key); - - void remove(String key); - - void clearAll(); - - void clearAllWithVersion(); -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.kt b/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.kt new file mode 100644 index 000000000..6e19901cb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.kt @@ -0,0 +1,33 @@ +package fr.free.nrw.commons.kvstore + +interface KeyValueStore { + fun getString(key: String): String? + + fun getBoolean(key: String): Boolean + + fun getLong(key: String): Long + + fun getInt(key: String): Int + + fun getString(key: String, defaultValue: String?): String? + + fun getBoolean(key: String, defaultValue: Boolean): Boolean + + fun getLong(key: String, defaultValue: Long): Long + + fun getInt(key: String, defaultValue: Int): Int + + fun putString(key: String, value: String) + + fun putBoolean(key: String, value: Boolean) + + fun putLong(key: String, value: Long) + + fun putInt(key: String, value: Int) + + fun contains(key: String): Boolean + + fun remove(key: String) + + fun clearAll() +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt index f4b1f2625..87a710424 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt @@ -46,7 +46,7 @@ class SystemThemeUtils @Inject constructor( // Returns true if the device is in night mode or false otherwise fun isDeviceInNightMode(): Boolean { return getSystemDefaultThemeBool( - applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme()) + applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme())!! ) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/kvstore/BasicKvStoreTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/kvstore/BasicKvStoreTest.kt new file mode 100644 index 000000000..99fdf915b --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/kvstore/BasicKvStoreTest.kt @@ -0,0 +1,238 @@ +package fr.free.nrw.commons.kvstore + +import android.content.Context +import android.content.SharedPreferences +import com.nhaarman.mockitokotlin2.atLeast +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import fr.free.nrw.commons.kvstore.BasicKvStore.Companion.KEY_VERSION +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.mock + +class BasicKvStoreTest { + private val context = mock() + private val prefs = mock() + private val editor = mock() + private lateinit var store: BasicKvStore + + @Before + fun setUp() { + whenever(context.getSharedPreferences(anyString(), anyInt())).thenReturn(prefs) + whenever(prefs.edit()).thenReturn(editor) + store = BasicKvStore(context, "name") + } + + @Test + fun versionUpdate() { + whenever(prefs.getInt(KEY_VERSION, 0)).thenReturn(99) + BasicKvStore(context, "name", 100, true) + + // It should clear itself and automatically put the new version number + verify(prefs, atLeast(2)).edit() + verify(editor).clear() + verify(editor).putInt(KEY_VERSION, 100) + verify(editor, atLeast(2)).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun versionDowngradeNotAllowed() { + whenever(prefs.getInt(KEY_VERSION, 0)).thenReturn(100) + BasicKvStore(context, "name", 99, true) + } + + @Test + fun versionRedactedFromGetAll() { + val all = mutableMapOf("key" to "value", KEY_VERSION to 100) + whenever(prefs.all).thenReturn(all) + + val result = store.all + Assert.assertEquals(mapOf("key" to "value"), result) + } + + @Test + fun getAllHandlesNull() { + whenever(prefs.all).thenReturn(null) + Assert.assertNull(store.all) + } + + @Test + fun getAllHandlesEmpty() { + whenever(prefs.all).thenReturn(emptyMap()) + Assert.assertNull(store.all) + } + + @Test + fun getString() { + whenever(prefs.getString("key", null)).thenReturn("value") + Assert.assertEquals("value", store.getString("key")) + } + + @Test + fun getBoolean() { + whenever(prefs.getBoolean("key", false)).thenReturn(true) + Assert.assertTrue(store.getBoolean("key")) + } + + @Test + fun getLong() { + whenever(prefs.getLong("key", 0L)).thenReturn(100) + Assert.assertEquals(100L, store.getLong("key")) + } + + @Test + fun getInt() { + whenever(prefs.getInt("key", 0)).thenReturn(100) + Assert.assertEquals(100, store.getInt("key")) + } + + @Test + fun getStringWithDefault() { + whenever(prefs.getString("key", "junk")).thenReturn("value") + Assert.assertEquals("value", store.getString("key", "junk")) + } + + @Test + fun getBooleanWithDefault() { + whenever(prefs.getBoolean("key", true)).thenReturn(true) + Assert.assertTrue(store.getBoolean("key", true)) + } + + @Test + fun getLongWithDefault() { + whenever(prefs.getLong("key", 22L)).thenReturn(100) + Assert.assertEquals(100L, store.getLong("key", 22L)) + } + + @Test + fun getIntWithDefault() { + whenever(prefs.getInt("key", 22)).thenReturn(100) + Assert.assertEquals(100, store.getInt("key", 22)) + } + + @Test + fun putAllStrings() { + store.putAllStrings( + mapOf( + "one" to "fish", + "two" to "fish", + "red" to "fish", + "blue" to "fish" + ) + ) + + verify(prefs).edit() + verify(editor).putString("one", "fish") + verify(editor).putString("two", "fish") + verify(editor).putString("red", "fish") + verify(editor).putString("blue", "fish") + verify(editor).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun putAllStringsWithReservedKey() { + store.putAllStrings( + mapOf( + "this" to "that", + KEY_VERSION to "something" + ) + ) + } + + @Test + fun putString() { + store.putString("this" , "that") + + verify(prefs).edit() + verify(editor).putString("this", "that") + verify(editor).apply() + } + + @Test + fun putBoolean() { + store.putBoolean("this" , true) + + verify(prefs).edit() + verify(editor).putBoolean("this", true) + verify(editor).apply() + } + + @Test + fun putLong() { + store.putLong("this" , 123L) + + verify(prefs).edit() + verify(editor).putLong("this", 123L) + verify(editor).apply() + } + + @Test + fun putInt() { + store.putInt("this" , 16) + + verify(prefs).edit() + verify(editor).putInt("this", 16) + verify(editor).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun putStringWithReservedKey() { + store.putString(KEY_VERSION, "that") + } + + @Test(expected = IllegalArgumentException::class) + fun putBooleanWithReservedKey() { + store.putBoolean(KEY_VERSION, true) + } + + @Test(expected = IllegalArgumentException::class) + fun putLongWithReservedKey() { + store.putLong(KEY_VERSION, 33L) + } + + @Test(expected = IllegalArgumentException::class) + fun putIntWithReservedKey() { + store.putInt(KEY_VERSION, 12) + } + + @Test + fun testContains() { + whenever(prefs.contains("key")).thenReturn(true) + Assert.assertTrue(store.contains("key")) + } + + @Test + fun containsRedactsVersion() { + whenever(prefs.contains(KEY_VERSION)).thenReturn(true) + Assert.assertFalse(store.contains(KEY_VERSION)) + } + + @Test + fun remove() { + store.remove("key") + + verify(prefs).edit() + verify(editor).remove("key") + verify(editor).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun removeWithReservedKey() { + store.remove(KEY_VERSION) + } + + @Test + fun clearAllPreservesVersion() { + whenever(prefs.getInt(KEY_VERSION, 0)).thenReturn(99) + + store.clearAll() + + verify(prefs).edit() + verify(editor).clear() + verify(editor).putInt(KEY_VERSION, 99) + verify(editor).apply() + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/kvstore/JsonKvStoreTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/kvstore/JsonKvStoreTest.kt new file mode 100644 index 000000000..0a0bdfc47 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/kvstore/JsonKvStoreTest.kt @@ -0,0 +1,85 @@ +package fr.free.nrw.commons.kvstore + +import android.content.Context +import android.content.SharedPreferences +import com.google.gson.Gson +import com.nhaarman.mockitokotlin2.atLeast +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import fr.free.nrw.commons.kvstore.BasicKvStore.Companion.KEY_VERSION +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.mock + +class JsonKvStoreTest { + private val context = mock() + private val prefs = mock() + private val editor = mock() + + private val gson = Gson() + private val testData = Person(16, "Bob", true, Pet("Poodle", 2)) + private val expected = gson.toJson(testData) + + private lateinit var store: JsonKvStore + + @Before + fun setUp() { + whenever(context.getSharedPreferences(anyString(), anyInt())).thenReturn(prefs) + whenever(prefs.edit()).thenReturn(editor) + store = JsonKvStore(context, "name", gson) + } + + @Test + fun putJson() { + store.putJson("person", testData) + + verify(prefs).edit() + verify(editor).putString("person", expected) + verify(editor).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun putJsonWithReservedKey() { + store.putJson(KEY_VERSION, testData) + } + + @Test + fun getJson() { + whenever(prefs.getString("key", null)).thenReturn(expected) + + val result = store.getJson("key", Person::class.java) + + Assert.assertEquals(testData, result) + } + + @Test + fun getJsonInTheFuture() { + whenever(prefs.getString("key", null)).thenReturn(expected) + + val resultOne: Person? = store.getJson("key") + Assert.assertEquals(testData, resultOne) + + val resultTwo = store.getJson("key") + Assert.assertEquals(testData, resultTwo) + } + + @Test + fun getJsonHandlesMalformedJson() { + whenever(prefs.getString("key", null)).thenReturn("junk") + + val result = store.getJson("key", Person::class.java) + + Assert.assertNull(result) + } + + data class Person( + val age: Int, val name: String, val hasPets: Boolean, val pet: Pet? + ) + + data class Pet( + val breed: String, val age: Int + ) +} \ No newline at end of file From 1e5521b434c5798440e115320fefc72e708d741a Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Fri, 29 Nov 2024 19:50:42 -0600 Subject: [PATCH 48/74] Convert dependency inject ("di") package to kotlin (#5976) * Convert a batch of easier modules * Convert the NetworkingModule to kotlin * Converted the ApplicationlessInjection to kotlin * Convert CommonsDaggerAppCompatActivity to kotlin * Convert CommonsDaggerContentProvider to kotlin * Convert CommonsDaggerIntentService to kotlin * Convert CommonsDaggerService to kotlin * Convert CommonsDaggerSupportFragment to kotlin * Convert CommonsDaggerBroadcastReceiver to kotlin * Convert CommonsApplicationModule to kotlin * Fix imports and make them consistent --- .../free/nrw/commons/actions/ThanksClient.kt | 2 +- .../commons/campaigns/CampaignsPresenter.kt | 5 +- .../ContributionBoundaryCallback.kt | 3 +- .../ContributionsListPresenter.java | 4 +- .../contributions/ContributionsPresenter.java | 3 +- .../ContributionsRemoteDataSource.kt | 4 +- .../fr/free/nrw/commons/db/Converters.java | 5 +- .../nrw/commons/di/ActivityBuilderModule.java | 90 ----- .../nrw/commons/di/ActivityBuilderModule.kt | 89 +++++ .../commons/di/ApplicationlessInjection.java | 105 ------ .../commons/di/ApplicationlessInjection.kt | 98 +++++ .../di/CommonsApplicationComponent.java | 85 ----- .../commons/di/CommonsApplicationComponent.kt | 80 ++++ .../commons/di/CommonsApplicationModule.java | 314 ---------------- .../commons/di/CommonsApplicationModule.kt | 239 ++++++++++++ .../di/CommonsDaggerAppCompatActivity.java | 48 --- .../di/CommonsDaggerAppCompatActivity.kt | 37 ++ .../di/CommonsDaggerBroadcastReceiver.java | 35 -- .../di/CommonsDaggerBroadcastReceiver.kt | 25 ++ .../di/CommonsDaggerContentProvider.java | 32 -- .../di/CommonsDaggerContentProvider.kt | 20 + .../di/CommonsDaggerIntentService.java | 32 -- .../commons/di/CommonsDaggerIntentService.kt | 20 + .../nrw/commons/di/CommonsDaggerService.java | 31 -- .../nrw/commons/di/CommonsDaggerService.kt | 20 + .../di/CommonsDaggerSupportFragment.java | 75 ---- .../di/CommonsDaggerSupportFragment.kt | 66 ++++ .../di/ContentProviderBuilderModule.java | 38 -- .../di/ContentProviderBuilderModule.kt | 37 ++ .../nrw/commons/di/FragmentBuilderModule.java | 166 --------- .../nrw/commons/di/FragmentBuilderModule.kt | 165 +++++++++ .../free/nrw/commons/di/NetworkingModule.java | 350 ------------------ .../free/nrw/commons/di/NetworkingModule.kt | 316 ++++++++++++++++ .../nrw/commons/di/ServiceBuilderModule.java | 19 - .../nrw/commons/di/ServiceBuilderModule.kt | 17 + .../free/nrw/commons/mwapi/CategoryApi.java | 15 +- .../upload/PendingUploadsPresenter.java | 3 +- .../upload/categories/CategoriesPresenter.kt | 6 +- .../upload/depicts/DepictsPresenter.kt | 6 +- .../commons/wikidata/CommonsServiceFactory.kt | 18 +- .../nrw/commons/TestCommonsApplication.kt | 16 +- 41 files changed, 1274 insertions(+), 1465 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.kt diff --git a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt index af305c9c6..1dcf93edf 100644 --- a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt @@ -3,7 +3,7 @@ package fr.free.nrw.commons.actions import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.auth.csrf.CsrfTokenClient import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException -import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF +import fr.free.nrw.commons.di.NetworkingModule.Companion.NAMED_COMMONS_CSRF import io.reactivex.Observable import javax.inject.Inject import javax.inject.Named diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt index 3753dfb67..ffbf92540 100644 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt @@ -3,9 +3,8 @@ package fr.free.nrw.commons.campaigns import android.annotation.SuppressLint import fr.free.nrw.commons.BasePresenter import fr.free.nrw.commons.campaigns.models.Campaign -import fr.free.nrw.commons.di.CommonsApplicationModule -import fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD -import fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient import fr.free.nrw.commons.utils.CommonsDateUtil.getIso8601DateFormatShort import io.reactivex.Scheduler diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt index 286abb97a..3f7bffe91 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt @@ -3,6 +3,7 @@ package fr.free.nrw.commons.contributions import androidx.paging.PagedList.BoundaryCallback import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.di.CommonsApplicationModule +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD import fr.free.nrw.commons.media.MediaClient import io.reactivex.Scheduler import io.reactivex.disposables.CompositeDisposable @@ -20,7 +21,7 @@ class ContributionBoundaryCallback private val repository: ContributionsRepository, private val sessionManager: SessionManager, private val mediaClient: MediaClient, - @param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler, + @param:Named(IO_THREAD) private val ioThreadScheduler: Scheduler, ) : BoundaryCallback() { private val compositeDisposable: CompositeDisposable = CompositeDisposable() var userName: String? = null diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java index 42495889d..735ff63d4 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java @@ -1,5 +1,7 @@ package fr.free.nrw.commons.contributions; +import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; + import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.paging.DataSource; @@ -34,7 +36,7 @@ public class ContributionsListPresenter implements UserActionListener { final ContributionBoundaryCallback contributionBoundaryCallback, final ContributionsRemoteDataSource contributionsRemoteDataSource, final ContributionsRepository repository, - @Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) { + @Named(IO_THREAD) final Scheduler ioThreadScheduler) { this.contributionBoundaryCallback = contributionBoundaryCallback; this.repository = repository; this.ioThreadScheduler = ioThreadScheduler; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java index 297a66616..495a4bc64 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.contributions; +import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; import androidx.work.ExistingWorkPolicy; @@ -31,7 +32,7 @@ public class ContributionsPresenter implements UserActionListener { @Inject ContributionsPresenter(ContributionsRepository repository, UploadRepository uploadRepository, - @Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) { + @Named(IO_THREAD) Scheduler ioThreadScheduler) { this.contributionsRepository = repository; this.uploadRepository = uploadRepository; this.ioThreadScheduler = ioThreadScheduler; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRemoteDataSource.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRemoteDataSource.kt index 346c83b34..e8ff01b3e 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRemoteDataSource.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRemoteDataSource.kt @@ -1,7 +1,7 @@ package fr.free.nrw.commons.contributions import androidx.paging.ItemKeyedDataSource -import fr.free.nrw.commons.di.CommonsApplicationModule +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD import fr.free.nrw.commons.media.MediaClient import io.reactivex.Scheduler import io.reactivex.disposables.CompositeDisposable @@ -16,7 +16,7 @@ class ContributionsRemoteDataSource @Inject constructor( private val mediaClient: MediaClient, - @param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler, + @param:Named(IO_THREAD) private val ioThreadScheduler: Scheduler, ) : ItemKeyedDataSource() { private val compositeDisposable: CompositeDisposable = CompositeDisposable() var userName: String? = null diff --git a/app/src/main/java/fr/free/nrw/commons/db/Converters.java b/app/src/main/java/fr/free/nrw/commons/db/Converters.java index a70cdc815..c0f85420f 100644 --- a/app/src/main/java/fr/free/nrw/commons/db/Converters.java +++ b/app/src/main/java/fr/free/nrw/commons/db/Converters.java @@ -22,7 +22,10 @@ import java.util.Map; public class Converters { public static Gson getGson() { - return ApplicationlessInjection.getInstance(CommonsApplication.getInstance()).getCommonsApplicationComponent().gson(); + return ApplicationlessInjection + .getInstance(CommonsApplication.getInstance()) + .getCommonsApplicationComponent() + .gson(); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java deleted file mode 100644 index 4516d806f..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ /dev/null @@ -1,90 +0,0 @@ -package fr.free.nrw.commons.di; - -import dagger.Module; -import dagger.android.ContributesAndroidInjector; -import fr.free.nrw.commons.AboutActivity; -import fr.free.nrw.commons.LocationPicker.LocationPickerActivity; -import fr.free.nrw.commons.WelcomeActivity; -import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.auth.SignupActivity; -import fr.free.nrw.commons.category.CategoryDetailsActivity; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity; -import fr.free.nrw.commons.description.DescriptionEditActivity; -import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity; -import fr.free.nrw.commons.explore.SearchActivity; -import fr.free.nrw.commons.media.ZoomableActivity; -import fr.free.nrw.commons.nearby.WikidataFeedback; -import fr.free.nrw.commons.notification.NotificationActivity; -import fr.free.nrw.commons.profile.ProfileActivity; -import fr.free.nrw.commons.review.ReviewActivity; -import fr.free.nrw.commons.settings.SettingsActivity; -import fr.free.nrw.commons.upload.UploadActivity; -import fr.free.nrw.commons.upload.UploadProgressActivity; - -/** - * This Class handles the dependency injection (using dagger) - * so, if a developer needs to add a new activity to the commons app - * then that must be mentioned here to inject the dependencies - */ -@Module -@SuppressWarnings({"WeakerAccess", "unused"}) -public abstract class ActivityBuilderModule { - - @ContributesAndroidInjector - abstract LoginActivity bindLoginActivity(); - - @ContributesAndroidInjector - abstract WelcomeActivity bindWelcomeActivity(); - - @ContributesAndroidInjector - abstract MainActivity bindContributionsActivity(); - - @ContributesAndroidInjector - abstract CustomSelectorActivity bindCustomSelectorActivity(); - - @ContributesAndroidInjector - abstract SettingsActivity bindSettingsActivity(); - - @ContributesAndroidInjector - abstract AboutActivity bindAboutActivity(); - - @ContributesAndroidInjector - abstract LocationPickerActivity bindLocationPickerActivity(); - - @ContributesAndroidInjector - abstract SignupActivity bindSignupActivity(); - - @ContributesAndroidInjector - abstract NotificationActivity bindNotificationActivity(); - - @ContributesAndroidInjector - abstract UploadActivity bindUploadActivity(); - - @ContributesAndroidInjector - abstract SearchActivity bindSearchActivity(); - - @ContributesAndroidInjector - abstract CategoryDetailsActivity bindCategoryDetailsActivity(); - - @ContributesAndroidInjector - abstract WikidataItemDetailsActivity bindDepictionDetailsActivity(); - - @ContributesAndroidInjector - abstract ProfileActivity bindAchievementsActivity(); - - @ContributesAndroidInjector - abstract ReviewActivity bindReviewActivity(); - - @ContributesAndroidInjector - abstract DescriptionEditActivity bindDescriptionEditActivity(); - - @ContributesAndroidInjector - abstract ZoomableActivity bindZoomableActivity(); - - @ContributesAndroidInjector - abstract UploadProgressActivity bindUploadProgressActivity(); - - @ContributesAndroidInjector - abstract WikidataFeedback bindWikiFeedback(); -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.kt b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.kt new file mode 100644 index 000000000..86750a553 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.kt @@ -0,0 +1,89 @@ +package fr.free.nrw.commons.di + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import fr.free.nrw.commons.AboutActivity +import fr.free.nrw.commons.LocationPicker.LocationPickerActivity +import fr.free.nrw.commons.WelcomeActivity +import fr.free.nrw.commons.auth.LoginActivity +import fr.free.nrw.commons.auth.SignupActivity +import fr.free.nrw.commons.category.CategoryDetailsActivity +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity +import fr.free.nrw.commons.description.DescriptionEditActivity +import fr.free.nrw.commons.explore.SearchActivity +import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity +import fr.free.nrw.commons.media.ZoomableActivity +import fr.free.nrw.commons.nearby.WikidataFeedback +import fr.free.nrw.commons.notification.NotificationActivity +import fr.free.nrw.commons.profile.ProfileActivity +import fr.free.nrw.commons.review.ReviewActivity +import fr.free.nrw.commons.settings.SettingsActivity +import fr.free.nrw.commons.upload.UploadActivity +import fr.free.nrw.commons.upload.UploadProgressActivity + +/** + * This Class handles the dependency injection (using dagger) + * so, if a developer needs to add a new activity to the commons app + * then that must be mentioned here to inject the dependencies + */ +@Module +@Suppress("unused") +abstract class ActivityBuilderModule { + @ContributesAndroidInjector + abstract fun bindLoginActivity(): LoginActivity + + @ContributesAndroidInjector + abstract fun bindWelcomeActivity(): WelcomeActivity + + @ContributesAndroidInjector + abstract fun bindContributionsActivity(): MainActivity + + @ContributesAndroidInjector + abstract fun bindCustomSelectorActivity(): CustomSelectorActivity + + @ContributesAndroidInjector + abstract fun bindSettingsActivity(): SettingsActivity + + @ContributesAndroidInjector + abstract fun bindAboutActivity(): AboutActivity + + @ContributesAndroidInjector + abstract fun bindLocationPickerActivity(): LocationPickerActivity + + @ContributesAndroidInjector + abstract fun bindSignupActivity(): SignupActivity + + @ContributesAndroidInjector + abstract fun bindNotificationActivity(): NotificationActivity + + @ContributesAndroidInjector + abstract fun bindUploadActivity(): UploadActivity + + @ContributesAndroidInjector + abstract fun bindSearchActivity(): SearchActivity + + @ContributesAndroidInjector + abstract fun bindCategoryDetailsActivity(): CategoryDetailsActivity + + @ContributesAndroidInjector + abstract fun bindDepictionDetailsActivity(): WikidataItemDetailsActivity + + @ContributesAndroidInjector + abstract fun bindAchievementsActivity(): ProfileActivity + + @ContributesAndroidInjector + abstract fun bindReviewActivity(): ReviewActivity + + @ContributesAndroidInjector + abstract fun bindDescriptionEditActivity(): DescriptionEditActivity + + @ContributesAndroidInjector + abstract fun bindZoomableActivity(): ZoomableActivity + + @ContributesAndroidInjector + abstract fun bindUploadProgressActivity(): UploadProgressActivity + + @ContributesAndroidInjector + abstract fun bindWikiFeedback(): WikidataFeedback +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java b/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java deleted file mode 100644 index f2bff5db7..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java +++ /dev/null @@ -1,105 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.app.Activity; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.ContentProvider; -import android.content.Context; - -import androidx.fragment.app.Fragment; - -import dagger.android.HasAndroidInjector; -import javax.inject.Inject; - -import dagger.android.AndroidInjector; -import dagger.android.DispatchingAndroidInjector; -import dagger.android.HasActivityInjector; -import dagger.android.HasBroadcastReceiverInjector; -import dagger.android.HasContentProviderInjector; -import dagger.android.HasFragmentInjector; -import dagger.android.HasServiceInjector; -import dagger.android.support.HasSupportFragmentInjector; - -/** - * Provides injectors for all sorts of components - * Ex: Activities, Fragments, Services, ContentProviders - */ -public class ApplicationlessInjection - implements - HasAndroidInjector, - HasActivityInjector, - HasFragmentInjector, - HasSupportFragmentInjector, - HasServiceInjector, - HasBroadcastReceiverInjector, - HasContentProviderInjector { - - private static ApplicationlessInjection instance = null; - - @Inject DispatchingAndroidInjector androidInjector; - @Inject DispatchingAndroidInjector activityInjector; - @Inject DispatchingAndroidInjector broadcastReceiverInjector; - @Inject DispatchingAndroidInjector fragmentInjector; - @Inject DispatchingAndroidInjector supportFragmentInjector; - @Inject DispatchingAndroidInjector serviceInjector; - @Inject DispatchingAndroidInjector contentProviderInjector; - - private CommonsApplicationComponent commonsApplicationComponent; - - public ApplicationlessInjection(Context applicationContext) { - commonsApplicationComponent = DaggerCommonsApplicationComponent.builder() - .appModule(new CommonsApplicationModule(applicationContext)).build(); - commonsApplicationComponent.inject(this); - } - - @Override - public AndroidInjector androidInjector() { - return androidInjector; - } - - @Override - public DispatchingAndroidInjector activityInjector() { - return activityInjector; - } - - @Override - public DispatchingAndroidInjector fragmentInjector() { - return fragmentInjector; - } - - @Override - public DispatchingAndroidInjector supportFragmentInjector() { - return supportFragmentInjector; - } - - @Override - public DispatchingAndroidInjector broadcastReceiverInjector() { - return broadcastReceiverInjector; - } - - @Override - public DispatchingAndroidInjector serviceInjector() { - return serviceInjector; - } - - @Override - public AndroidInjector contentProviderInjector() { - return contentProviderInjector; - } - - public CommonsApplicationComponent getCommonsApplicationComponent() { - return commonsApplicationComponent; - } - - public static ApplicationlessInjection getInstance(Context applicationContext) { - if (instance == null) { - synchronized (ApplicationlessInjection.class) { - if (instance == null) { - instance = new ApplicationlessInjection(applicationContext); - } - } - } - - return instance; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.kt b/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.kt new file mode 100644 index 000000000..1a88bd809 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.kt @@ -0,0 +1,98 @@ +package fr.free.nrw.commons.di + +import android.app.Activity +import android.app.Fragment +import android.app.Service +import android.content.BroadcastReceiver +import android.content.ContentProvider +import android.content.Context +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasActivityInjector +import dagger.android.HasAndroidInjector +import dagger.android.HasBroadcastReceiverInjector +import dagger.android.HasContentProviderInjector +import dagger.android.HasFragmentInjector +import dagger.android.HasServiceInjector +import dagger.android.support.HasSupportFragmentInjector +import javax.inject.Inject +import androidx.fragment.app.Fragment as AndroidXFragmen + +/** + * Provides injectors for all sorts of components + * Ex: Activities, Fragments, Services, ContentProviders + */ +class ApplicationlessInjection(applicationContext: Context) : HasAndroidInjector, + HasActivityInjector, HasFragmentInjector, HasSupportFragmentInjector, HasServiceInjector, + HasBroadcastReceiverInjector, HasContentProviderInjector { + @Inject @JvmField + var androidInjector: DispatchingAndroidInjector? = null + + @Inject @JvmField + var activityInjector: DispatchingAndroidInjector? = null + + @Inject @JvmField + var broadcastReceiverInjector: DispatchingAndroidInjector? = null + + @Inject @JvmField + var fragmentInjector: DispatchingAndroidInjector? = null + + @Inject @JvmField + var supportFragmentInjector: DispatchingAndroidInjector? = null + + @Inject @JvmField + var serviceInjector: DispatchingAndroidInjector? = null + + @Inject @JvmField + var contentProviderInjector: DispatchingAndroidInjector? = null + + val instance: ApplicationlessInjection get() = _instance!! + + val commonsApplicationComponent: CommonsApplicationComponent = + DaggerCommonsApplicationComponent + .builder() + .appModule(CommonsApplicationModule(applicationContext)) + .build() + + init { + commonsApplicationComponent.inject(this) + } + + override fun androidInjector(): AndroidInjector? = + androidInjector + + override fun activityInjector(): DispatchingAndroidInjector? = + activityInjector + + override fun fragmentInjector(): DispatchingAndroidInjector? = + fragmentInjector + + override fun supportFragmentInjector(): DispatchingAndroidInjector? = + supportFragmentInjector + + override fun broadcastReceiverInjector(): DispatchingAndroidInjector? = + broadcastReceiverInjector + + override fun serviceInjector(): DispatchingAndroidInjector? = + serviceInjector + + override fun contentProviderInjector(): AndroidInjector? = + contentProviderInjector + + companion object { + private var _instance: ApplicationlessInjection? = null + + @JvmStatic + fun getInstance(applicationContext: Context): ApplicationlessInjection { + if (_instance == null) { + synchronized(ApplicationlessInjection::class.java) { + if (_instance == null) { + _instance = ApplicationlessInjection(applicationContext) + } + } + } + + return _instance!! + } + } +} 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 deleted file mode 100644 index 0d847b649..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ /dev/null @@ -1,85 +0,0 @@ -package fr.free.nrw.commons.di; - -import com.google.gson.Gson; - -import fr.free.nrw.commons.explore.categories.CategoriesModule; -import fr.free.nrw.commons.navtab.MoreBottomSheetFragment; -import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment; -import fr.free.nrw.commons.nearby.NearbyController; -import fr.free.nrw.commons.upload.worker.UploadWorker; -import javax.inject.Singleton; - -import dagger.Component; -import dagger.android.AndroidInjectionModule; -import dagger.android.AndroidInjector; -import dagger.android.support.AndroidSupportInjectionModule; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.contributions.ContributionsModule; -import fr.free.nrw.commons.explore.depictions.DepictionModule; -import fr.free.nrw.commons.explore.SearchModule; -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; - - -/** - * Facilitates Injection from CommonsApplicationModule to all the - * classes seeking a dependency to be injected - */ -@Singleton -@Component(modules = { - CommonsApplicationModule.class, - NetworkingModule.class, - AndroidInjectionModule.class, - AndroidSupportInjectionModule.class, - ActivityBuilderModule.class, - FragmentBuilderModule.class, - ServiceBuilderModule.class, - ContentProviderBuilderModule.class, - UploadModule.class, - ContributionsModule.class, - SearchModule.class, - DepictionModule.class, - CategoriesModule.class -}) -public interface CommonsApplicationComponent extends AndroidInjector { - void inject(CommonsApplication application); - - void inject(UploadWorker worker); - - void inject(LoginActivity activity); - - void inject(SettingsFragment fragment); - - void inject(MoreBottomSheetFragment fragment); - - void inject(MoreBottomSheetLoggedOutFragment fragment); - - void inject(ReviewController reviewController); - - //void inject(NavTabLayout view); - - @Override - void inject(ApplicationlessInjection instance); - - void inject(FileProcessor fileProcessor); - - void inject(PicOfDayAppWidget picOfDayAppWidget); - - @Singleton - void inject(NearbyController nearbyController); - - Gson gson(); - - @Component.Builder - @SuppressWarnings({"WeakerAccess", "unused"}) - interface Builder { - - Builder appModule(CommonsApplicationModule applicationModule); - - CommonsApplicationComponent build(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt new file mode 100644 index 000000000..b0c0c4d37 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt @@ -0,0 +1,80 @@ +package fr.free.nrw.commons.di + +import com.google.gson.Gson +import dagger.Component +import dagger.android.AndroidInjectionModule +import dagger.android.AndroidInjector +import dagger.android.support.AndroidSupportInjectionModule +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.auth.LoginActivity +import fr.free.nrw.commons.contributions.ContributionsModule +import fr.free.nrw.commons.explore.SearchModule +import fr.free.nrw.commons.explore.categories.CategoriesModule +import fr.free.nrw.commons.explore.depictions.DepictionModule +import fr.free.nrw.commons.navtab.MoreBottomSheetFragment +import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment +import fr.free.nrw.commons.nearby.NearbyController +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.upload.worker.UploadWorker +import fr.free.nrw.commons.widget.PicOfDayAppWidget +import javax.inject.Singleton + +/** + * Facilitates Injection from CommonsApplicationModule to all the + * classes seeking a dependency to be injected + */ +@Singleton +@Component( + modules = [ + CommonsApplicationModule::class, + NetworkingModule::class, + AndroidInjectionModule::class, + AndroidSupportInjectionModule::class, + ActivityBuilderModule::class, + FragmentBuilderModule::class, + ServiceBuilderModule::class, + ContentProviderBuilderModule::class, + UploadModule::class, + ContributionsModule::class, + SearchModule::class, + DepictionModule::class, + CategoriesModule::class + ] +) +interface CommonsApplicationComponent : AndroidInjector { + fun inject(application: CommonsApplication) + + fun inject(worker: UploadWorker) + + fun inject(activity: LoginActivity) + + fun inject(fragment: SettingsFragment) + + fun inject(fragment: MoreBottomSheetFragment) + + fun inject(fragment: MoreBottomSheetLoggedOutFragment) + + fun inject(reviewController: ReviewController) + + override fun inject(instance: ApplicationlessInjection) + + fun inject(fileProcessor: FileProcessor) + + fun inject(picOfDayAppWidget: PicOfDayAppWidget) + + @Singleton + fun inject(nearbyController: NearbyController) + + fun gson(): Gson + + @Component.Builder + @Suppress("unused") + interface Builder { + fun appModule(applicationModule: CommonsApplicationModule): Builder + + fun build(): CommonsApplicationComponent + } +} 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 deleted file mode 100644 index 3f9344184..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ /dev/null @@ -1,314 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.app.Activity; -import android.content.ContentProviderClient; -import android.content.ContentResolver; -import android.content.Context; -import android.view.inputmethod.InputMethodManager; -import androidx.collection.LruCache; -import androidx.room.Room; -import androidx.room.migration.Migration; -import androidx.sqlite.db.SupportSQLiteDatabase; -import com.google.gson.Gson; -import dagger.Module; -import dagger.Provides; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.contributions.ContributionDao; -import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao; -import fr.free.nrw.commons.customselector.database.UploadedStatusDao; -import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader; -import fr.free.nrw.commons.data.DBOpenHelper; -import fr.free.nrw.commons.db.AppDatabase; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.nearby.PlaceDao; -import fr.free.nrw.commons.review.ReviewDao; -import fr.free.nrw.commons.settings.Prefs; -import fr.free.nrw.commons.upload.UploadController; -import fr.free.nrw.commons.upload.depicts.DepictsDao; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.wikidata.WikidataEditListener; -import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl; -import io.reactivex.Scheduler; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import javax.inject.Named; -import javax.inject.Singleton; - -/** - * The Dependency Provider class for Commons Android. - * - * Provides all sorts of ContentProviderClients used by the app - * along with the Liscences, AccountUtility, UploadController, Logged User, - * Location manager etc - */ -@Module -@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"; - private AppDatabase appDatabase; - - static final Migration MIGRATION_1_2 = new Migration(1, 2) { - @Override - public void migrate(SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE contribution " - + " ADD COLUMN hasInvalidLocation INTEGER NOT NULL DEFAULT 0"); - } - }; - - public CommonsApplicationModule(Context applicationContext) { - this.applicationContext = applicationContext; - } - - /** - * Provides ImageFileLoader used to fetch device images. - * @param context - * @return - */ - @Provides - public ImageFileLoader providesImageFileLoader(Context context) { - return new ImageFileLoader(context); - } - - @Provides - public Context providesApplicationContext() { - return this.applicationContext; - } - - @Provides - public InputMethodManager provideInputMethodManager() { - return (InputMethodManager) applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE); - } - - @Provides - @Named("licenses") - public List provideLicenses(Context context) { - List licenseItems = new ArrayList<>(); - licenseItems.add(context.getString(R.string.license_name_cc0)); - licenseItems.add(context.getString(R.string.license_name_cc_by)); - licenseItems.add(context.getString(R.string.license_name_cc_by_sa)); - licenseItems.add(context.getString(R.string.license_name_cc_by_four)); - licenseItems.add(context.getString(R.string.license_name_cc_by_sa_four)); - return licenseItems; - } - - @Provides - @Named("licenses_by_name") - public Map provideLicensesByName(Context context) { - Map byName = new HashMap<>(); - byName.put(context.getString(R.string.license_name_cc0), Prefs.Licenses.CC0); - byName.put(context.getString(R.string.license_name_cc_by), Prefs.Licenses.CC_BY_3); - byName.put(context.getString(R.string.license_name_cc_by_sa), Prefs.Licenses.CC_BY_SA_3); - byName.put(context.getString(R.string.license_name_cc_by_four), Prefs.Licenses.CC_BY_4); - byName.put(context.getString(R.string.license_name_cc_by_sa_four), Prefs.Licenses.CC_BY_SA_4); - return byName; - } - - /** - * Provides an instance of CategoryContentProviderClient i.e. the categories - * that are there in local storage - */ - @Provides - @Named("category") - public ContentProviderClient provideCategoryContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.CATEGORY_AUTHORITY); - } - - /** - * This method is used to provide instance of RecentSearchContentProviderClient - * which provides content of Recent Searches from database - * @param context - * @return returns RecentSearchContentProviderClient - */ - @Provides - @Named("recentsearch") - public ContentProviderClient provideRecentSearchContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.RECENT_SEARCH_AUTHORITY); - } - - @Provides - @Named("contribution") - public ContentProviderClient provideContributionContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.CONTRIBUTION_AUTHORITY); - } - - @Provides - @Named("modification") - public ContentProviderClient provideModificationContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.MODIFICATION_AUTHORITY); - } - - @Provides - @Named("bookmarks") - public ContentProviderClient provideBookmarkContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY); - } - - @Provides - @Named("bookmarksLocation") - public ContentProviderClient provideBookmarkLocationContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY); - } - - @Provides - @Named("bookmarksItem") - public ContentProviderClient provideBookmarkItemContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_ITEMS_AUTHORITY); - } - - /** - * This method is used to provide instance of RecentLanguagesContentProvider - * which provides content of recent used languages from database - * @param context Context - * @return returns RecentLanguagesContentProvider - */ - @Provides - @Named("recent_languages") - public ContentProviderClient provideRecentLanguagesContentProviderClient(final Context context) { - return context.getContentResolver() - .acquireContentProviderClient(BuildConfig.RECENT_LANGUAGE_AUTHORITY); - } - - /** - * Provides a Json store instance(JsonKvStore) which keeps - * the provided Gson in it's instance - * @param gson stored inside the store instance - */ - @Provides - @Named("default_preferences") - public JsonKvStore providesDefaultKvStore(Context context, Gson gson) { - String storeName = context.getPackageName() + "_preferences"; - return new JsonKvStore(context, storeName, gson); - } - - @Provides - public UploadController providesUploadController(SessionManager sessionManager, - @Named("default_preferences") JsonKvStore kvStore, - Context context, ContributionDao contributionDao) { - return new UploadController(sessionManager, context, kvStore); - } - - @Provides - @Singleton - public LocationServiceManager provideLocationServiceManager(Context context) { - return new LocationServiceManager(context); - } - - @Provides - @Singleton - public DBOpenHelper provideDBOpenHelper(Context context) { - return new DBOpenHelper(context); - } - - @Provides - @Singleton - @Named("thumbnail-cache") - public LruCache provideLruCache() { - return new LruCache<>(1024); - } - - @Provides - @Singleton - public WikidataEditListener provideWikidataEditListener() { - return new WikidataEditListenerImpl(); - } - - /** - * Provides app flavour. Can be used to alter flows in the app - * @return - */ - @Named("isBeta") - @Provides - @Singleton - public boolean provideIsBetaVariant() { - return ConfigUtils.isBetaFlavour(); - } - - /** - * Provide JavaRx IO scheduler which manages IO operations - * across various Threads - */ - @Named(IO_THREAD) - @Provides - public Scheduler providesIoThread(){ - return Schedulers.io(); - } - - @Named(MAIN_THREAD) - @Provides - public Scheduler providesMainThread() { - return AndroidSchedulers.mainThread(); - } - - @Named("username") - @Provides - public String provideLoggedInUsername(SessionManager sessionManager) { - return Objects.toString(sessionManager.getUserName(), ""); - } - - @Provides - @Singleton - public AppDatabase provideAppDataBase() { - appDatabase = Room.databaseBuilder(applicationContext, AppDatabase.class, "commons_room.db") - .addMigrations(MIGRATION_1_2) - .fallbackToDestructiveMigration() - .build(); - return appDatabase; - } - - @Provides - public ContributionDao providesContributionsDao(AppDatabase appDatabase) { - return appDatabase.contributionDao(); - } - - @Provides - public PlaceDao providesPlaceDao(AppDatabase appDatabase) { - return appDatabase.PlaceDao(); - } - - /** - * Get the reference of DepictsDao class. - */ - @Provides - public DepictsDao providesDepictDao(AppDatabase appDatabase) { - return appDatabase.DepictsDao(); - } - - /** - * Get the reference of UploadedStatus class. - */ - @Provides - public UploadedStatusDao providesUploadedStatusDao(AppDatabase appDatabase) { - return appDatabase.UploadedStatusDao(); - } - - /** - * Get the reference of NotForUploadStatus class. - */ - @Provides - public NotForUploadStatusDao providesNotForUploadStatusDao(AppDatabase appDatabase) { - return appDatabase.NotForUploadStatusDao(); - } - - /** - * Get the reference of ReviewDao class - */ - @Provides - public ReviewDao providesReviewDao(AppDatabase appDatabase){ - return appDatabase.ReviewDao(); - } - - @Provides - public ContentResolver providesContentResolver(Context context){ - return context.getContentResolver(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt new file mode 100644 index 000000000..6f883769f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt @@ -0,0 +1,239 @@ +package fr.free.nrw.commons.di + +import android.app.Activity +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.Context +import android.view.inputmethod.InputMethodManager +import androidx.collection.LruCache +import androidx.room.Room.databaseBuilder +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.google.gson.Gson +import dagger.Module +import dagger.Provides +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.contributions.ContributionDao +import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao +import fr.free.nrw.commons.customselector.database.UploadedStatusDao +import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader +import fr.free.nrw.commons.data.DBOpenHelper +import fr.free.nrw.commons.db.AppDatabase +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LocationServiceManager +import fr.free.nrw.commons.nearby.PlaceDao +import fr.free.nrw.commons.review.ReviewDao +import fr.free.nrw.commons.settings.Prefs +import fr.free.nrw.commons.upload.UploadController +import fr.free.nrw.commons.upload.depicts.DepictsDao +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.wikidata.WikidataEditListener +import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl +import io.reactivex.Scheduler +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import java.util.Objects +import javax.inject.Named +import javax.inject.Singleton + +/** + * The Dependency Provider class for Commons Android. + * Provides all sorts of ContentProviderClients used by the app + * along with the Liscences, AccountUtility, UploadController, Logged User, + * Location manager etc + */ +@Module +@Suppress("unused") +open class CommonsApplicationModule(private val applicationContext: Context) { + @Provides + fun providesImageFileLoader(context: Context): ImageFileLoader = + ImageFileLoader(context) + + @Provides + fun providesApplicationContext(): Context = + applicationContext + + @Provides + fun provideInputMethodManager(): InputMethodManager = + applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + + @Provides + @Named("licenses") + fun provideLicenses(context: Context): List = listOf( + context.getString(R.string.license_name_cc0), + context.getString(R.string.license_name_cc_by), + context.getString(R.string.license_name_cc_by_sa), + context.getString(R.string.license_name_cc_by_four), + context.getString(R.string.license_name_cc_by_sa_four) + ) + + @Provides + @Named("licenses_by_name") + fun provideLicensesByName(context: Context): Map = mapOf( + context.getString(R.string.license_name_cc0) to Prefs.Licenses.CC0, + context.getString(R.string.license_name_cc_by) to Prefs.Licenses.CC_BY_3, + context.getString(R.string.license_name_cc_by_sa) to Prefs.Licenses.CC_BY_SA_3, + context.getString(R.string.license_name_cc_by_four) to Prefs.Licenses.CC_BY_4, + context.getString(R.string.license_name_cc_by_sa_four) to Prefs.Licenses.CC_BY_SA_4 + ) + + /** + * Provides an instance of CategoryContentProviderClient i.e. the categories + * that are there in local storage + */ + @Provides + @Named("category") + open fun provideCategoryContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.CATEGORY_AUTHORITY) + + @Provides + @Named("recentsearch") + fun provideRecentSearchContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.RECENT_SEARCH_AUTHORITY) + + @Provides + @Named("contribution") + open fun provideContributionContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.CONTRIBUTION_AUTHORITY) + + @Provides + @Named("modification") + open fun provideModificationContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.MODIFICATION_AUTHORITY) + + @Provides + @Named("bookmarks") + fun provideBookmarkContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY) + + @Provides + @Named("bookmarksLocation") + fun provideBookmarkLocationContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY) + + @Provides + @Named("bookmarksItem") + fun provideBookmarkItemContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_ITEMS_AUTHORITY) + + /** + * This method is used to provide instance of RecentLanguagesContentProvider + * which provides content of recent used languages from database + * @param context Context + * @return returns RecentLanguagesContentProvider + */ + @Provides + @Named("recent_languages") + fun provideRecentLanguagesContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.RECENT_LANGUAGE_AUTHORITY) + + /** + * Provides a Json store instance(JsonKvStore) which keeps + * the provided Gson in it's instance + * @param gson stored inside the store instance + */ + @Provides + @Named("default_preferences") + open fun providesDefaultKvStore(context: Context, gson: Gson): JsonKvStore = + JsonKvStore(context, "${context.packageName}_preferences", gson) + + @Provides + fun providesUploadController( + sessionManager: SessionManager, + @Named("default_preferences") kvStore: JsonKvStore, + context: Context + ): UploadController = UploadController(sessionManager, context, kvStore) + + @Provides + @Singleton + open fun provideLocationServiceManager(context: Context): LocationServiceManager = + LocationServiceManager(context) + + @Provides + @Singleton + open fun provideDBOpenHelper(context: Context): DBOpenHelper = + DBOpenHelper(context) + + @Provides + @Singleton + @Named("thumbnail-cache") + open fun provideLruCache(): LruCache = + LruCache(1024) + + @Provides + @Singleton + fun provideWikidataEditListener(): WikidataEditListener = + WikidataEditListenerImpl() + + @Named("isBeta") + @Provides + @Singleton + fun provideIsBetaVariant(): Boolean = + isBetaFlavour + + @Named(IO_THREAD) + @Provides + fun providesIoThread(): Scheduler = + Schedulers.io() + + @Named(MAIN_THREAD) + @Provides + fun providesMainThread(): Scheduler = + AndroidSchedulers.mainThread() + + @Named("username") + @Provides + fun provideLoggedInUsername(sessionManager: SessionManager): String = + Objects.toString(sessionManager.userName, "") + + @Provides + @Singleton + fun provideAppDataBase(): AppDatabase = databaseBuilder( + applicationContext, + AppDatabase::class.java, + "commons_room.db" + ).addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build() + + @Provides + fun providesContributionsDao(appDatabase: AppDatabase): ContributionDao = + appDatabase.contributionDao() + + @Provides + fun providesPlaceDao(appDatabase: AppDatabase): PlaceDao = + appDatabase.PlaceDao() + + @Provides + fun providesDepictDao(appDatabase: AppDatabase): DepictsDao = + appDatabase.DepictsDao() + + @Provides + fun providesUploadedStatusDao(appDatabase: AppDatabase): UploadedStatusDao = + appDatabase.UploadedStatusDao() + + @Provides + fun providesNotForUploadStatusDao(appDatabase: AppDatabase): NotForUploadStatusDao = + appDatabase.NotForUploadStatusDao() + + @Provides + fun providesReviewDao(appDatabase: AppDatabase): ReviewDao = + appDatabase.ReviewDao() + + @Provides + fun providesContentResolver(context: Context): ContentResolver = + context.contentResolver + + companion object { + const val IO_THREAD: String = "io_thread" + const val MAIN_THREAD: String = "main_thread" + + val MIGRATION_1_2: Migration = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE contribution " + " ADD COLUMN hasInvalidLocation INTEGER NOT NULL DEFAULT 0" + ) + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java deleted file mode 100644 index 003b3649c..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java +++ /dev/null @@ -1,48 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.app.Activity; -import android.os.Bundle; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.Fragment; - -import javax.inject.Inject; - -import dagger.android.AndroidInjector; -import dagger.android.DispatchingAndroidInjector; -import dagger.android.support.HasSupportFragmentInjector; - -public abstract class CommonsDaggerAppCompatActivity extends AppCompatActivity implements HasSupportFragmentInjector { - - @Inject - DispatchingAndroidInjector supportFragmentInjector; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - inject(); - super.onCreate(savedInstanceState); - } - - @Override - public AndroidInjector supportFragmentInjector() { - return supportFragmentInjector; - } - - /** - * when this Activity is created it injects an instance of this class inside - * activityInjector method of ApplicationlessInjection - */ - private void inject() { - ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext()); - - AndroidInjector activityInjector = injection.activityInjector(); - - if (activityInjector == null) { - throw new NullPointerException("ApplicationlessInjection.activityInjector() returned null"); - } - - activityInjector.inject(this); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.kt new file mode 100644 index 000000000..fe9c7adee --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.kt @@ -0,0 +1,37 @@ +package fr.free.nrw.commons.di + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.support.HasSupportFragmentInjector +import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance +import javax.inject.Inject + +abstract class CommonsDaggerAppCompatActivity : AppCompatActivity(), HasSupportFragmentInjector { + @Inject @JvmField + var supportFragmentInjector: DispatchingAndroidInjector? = null + + override fun onCreate(savedInstanceState: Bundle?) { + inject() + super.onCreate(savedInstanceState) + } + + override fun supportFragmentInjector(): AndroidInjector { + return supportFragmentInjector!! + } + + /** + * when this Activity is created it injects an instance of this class inside + * activityInjector method of ApplicationlessInjection + */ + private fun inject() { + val injection = getInstance(applicationContext) + + val activityInjector = injection.activityInjector() + ?: throw NullPointerException("ApplicationlessInjection.activityInjector() returned null") + + activityInjector.inject(this) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java deleted file mode 100644 index 0b89003b5..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java +++ /dev/null @@ -1,35 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import dagger.android.AndroidInjector; - -/** - * Receives broadcast then injects it's instance to the broadcastReceiverInjector method of - * ApplicationlessInjection class - */ -public abstract class CommonsDaggerBroadcastReceiver extends BroadcastReceiver { - - public CommonsDaggerBroadcastReceiver() { - super(); - } - - @Override - public void onReceive(Context context, Intent intent) { - inject(context); - } - - private void inject(Context context) { - ApplicationlessInjection injection = ApplicationlessInjection.getInstance(context.getApplicationContext()); - - AndroidInjector serviceInjector = injection.broadcastReceiverInjector(); - - if (serviceInjector == null) { - throw new NullPointerException("ApplicationlessInjection.broadcastReceiverInjector() returned null"); - } - serviceInjector.inject(this); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.kt new file mode 100644 index 000000000..4df25889f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.di + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance + +/** + * Receives broadcast then injects it's instance to the broadcastReceiverInjector method of + * ApplicationlessInjection class + */ +abstract class CommonsDaggerBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + inject(context) + } + + private fun inject(context: Context) { + val injection = getInstance(context.applicationContext) + + val serviceInjector = injection.broadcastReceiverInjector() + ?: throw NullPointerException("ApplicationlessInjection.broadcastReceiverInjector() returned null") + + serviceInjector.inject(this) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java deleted file mode 100644 index 06adee489..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java +++ /dev/null @@ -1,32 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.content.ContentProvider; - -import dagger.android.AndroidInjector; - - -public abstract class CommonsDaggerContentProvider extends ContentProvider { - - public CommonsDaggerContentProvider() { - super(); - } - - @Override - public boolean onCreate() { - inject(); - return true; - } - - private void inject() { - ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getContext()); - - AndroidInjector serviceInjector = injection.contentProviderInjector(); - - if (serviceInjector == null) { - throw new NullPointerException("ApplicationlessInjection.contentProviderInjector() returned null"); - } - - serviceInjector.inject(this); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.kt new file mode 100644 index 000000000..c1bda689c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.di + +import android.content.ContentProvider +import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance + +abstract class CommonsDaggerContentProvider : ContentProvider() { + override fun onCreate(): Boolean { + inject() + return true + } + + private fun inject() { + val injection = getInstance(context!!) + + val serviceInjector = injection.contentProviderInjector() + ?: throw NullPointerException("ApplicationlessInjection.contentProviderInjector() returned null") + + serviceInjector.inject(this) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java deleted file mode 100644 index 41f661db4..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java +++ /dev/null @@ -1,32 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.app.IntentService; -import android.app.Service; - -import dagger.android.AndroidInjector; - -public abstract class CommonsDaggerIntentService extends IntentService { - - public CommonsDaggerIntentService(String name) { - super(name); - } - - @Override - public void onCreate() { - inject(); - super.onCreate(); - } - - private void inject() { - ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext()); - - AndroidInjector serviceInjector = injection.serviceInjector(); - - if (serviceInjector == null) { - throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null"); - } - - serviceInjector.inject(this); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.kt new file mode 100644 index 000000000..4aae35f0b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.di + +import android.app.IntentService +import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance + +abstract class CommonsDaggerIntentService(name: String?) : IntentService(name) { + override fun onCreate() { + inject() + super.onCreate() + } + + private fun inject() { + val injection = getInstance(applicationContext) + + val serviceInjector = injection.serviceInjector() + ?: throw NullPointerException("ApplicationlessInjection.serviceInjector() returned null") + + serviceInjector.inject(this) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java deleted file mode 100644 index 0d045d2ce..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java +++ /dev/null @@ -1,31 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.app.Service; - -import dagger.android.AndroidInjector; - -public abstract class CommonsDaggerService extends Service { - - public CommonsDaggerService() { - super(); - } - - @Override - public void onCreate() { - inject(); - super.onCreate(); - } - - private void inject() { - ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext()); - - AndroidInjector serviceInjector = injection.serviceInjector(); - - if (serviceInjector == null) { - throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null"); - } - - serviceInjector.inject(this); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.kt new file mode 100644 index 000000000..3a67e0d2a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.di + +import android.app.Service +import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance + +abstract class CommonsDaggerService : Service() { + override fun onCreate() { + inject() + super.onCreate() + } + + private fun inject() { + val injection = getInstance(applicationContext) + + val serviceInjector = injection.serviceInjector() + ?: throw NullPointerException("ApplicationlessInjection.serviceInjector() returned null") + + serviceInjector.inject(this) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java deleted file mode 100644 index f5ef2dd28..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java +++ /dev/null @@ -1,75 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.app.Activity; -import android.content.Context; - -import androidx.fragment.app.Fragment; - -import javax.inject.Inject; - -import dagger.android.AndroidInjector; -import dagger.android.DispatchingAndroidInjector; -import dagger.android.support.HasSupportFragmentInjector; -import io.reactivex.disposables.CompositeDisposable; - -public abstract class CommonsDaggerSupportFragment extends Fragment implements HasSupportFragmentInjector { - - @Inject - DispatchingAndroidInjector childFragmentInjector; - - protected CompositeDisposable compositeDisposable = new CompositeDisposable(); - - @Override - public void onAttach(Context context) { - inject(); - super.onAttach(context); - } - - @Override - public void onDestroy() { - super.onDestroy(); - compositeDisposable.clear(); - } - - @Override - public AndroidInjector supportFragmentInjector() { - return childFragmentInjector; - } - - - public void inject() { - HasSupportFragmentInjector hasSupportFragmentInjector = findHasFragmentInjector(); - - AndroidInjector fragmentInjector = hasSupportFragmentInjector.supportFragmentInjector(); - - if (fragmentInjector == null) { - throw new NullPointerException(String.format("%s.supportFragmentInjector() returned null", hasSupportFragmentInjector.getClass().getCanonicalName())); - } - - fragmentInjector.inject(this); - } - - private HasSupportFragmentInjector findHasFragmentInjector() { - Fragment parentFragment = this; - - while ((parentFragment = parentFragment.getParentFragment()) != null) { - if (parentFragment instanceof HasSupportFragmentInjector) { - return (HasSupportFragmentInjector) parentFragment; - } - } - - Activity activity = getActivity(); - - if (activity instanceof HasSupportFragmentInjector) { - return (HasSupportFragmentInjector) activity; - } - - ApplicationlessInjection injection = ApplicationlessInjection.getInstance(activity.getApplicationContext()); - if (injection != null) { - return injection; - } - - throw new IllegalArgumentException(String.format("No injector was found for %s", getClass().getCanonicalName())); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.kt new file mode 100644 index 000000000..8204d4415 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.kt @@ -0,0 +1,66 @@ +package fr.free.nrw.commons.di + +import android.app.Activity +import android.content.Context +import androidx.fragment.app.Fragment +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.support.HasSupportFragmentInjector +import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance +import io.reactivex.disposables.CompositeDisposable +import javax.inject.Inject + +abstract class CommonsDaggerSupportFragment : Fragment(), HasSupportFragmentInjector { + + @Inject @JvmField + var childFragmentInjector: DispatchingAndroidInjector? = null + + @JvmField + protected var compositeDisposable: CompositeDisposable = CompositeDisposable() + + override fun onAttach(context: Context) { + inject() + super.onAttach(context) + } + + override fun onDestroy() { + super.onDestroy() + compositeDisposable.clear() + } + + override fun supportFragmentInjector(): AndroidInjector = + childFragmentInjector!! + + + fun inject() { + val hasSupportFragmentInjector = findHasFragmentInjector() + + val fragmentInjector = hasSupportFragmentInjector.supportFragmentInjector() + ?: throw NullPointerException( + String.format( + "%s.supportFragmentInjector() returned null", + hasSupportFragmentInjector.javaClass.canonicalName + ) + ) + + fragmentInjector.inject(this) + } + + private fun findHasFragmentInjector(): HasSupportFragmentInjector { + var parentFragment: Fragment? = this + + while ((parentFragment!!.parentFragment.also { parentFragment = it }) != null) { + if (parentFragment is HasSupportFragmentInjector) { + return parentFragment as HasSupportFragmentInjector + } + } + + val activity: Activity = requireActivity() + + if (activity is HasSupportFragmentInjector) { + return activity + } + + return getInstance(activity.applicationContext) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java deleted file mode 100644 index aca6a2bf9..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java +++ /dev/null @@ -1,38 +0,0 @@ -package fr.free.nrw.commons.di; - -import dagger.Module; -import dagger.android.ContributesAndroidInjector; -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider; -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider; -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider; -import fr.free.nrw.commons.category.CategoryContentProvider; -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider; -import fr.free.nrw.commons.recentlanguages.RecentLanguagesContentProvider; - -/** - * This Class Represents the Module for dependency injection (using dagger) - * so, if a developer needs to add a new ContentProvider to the commons app - * then that must be mentioned here to inject the dependencies - */ -@Module -@SuppressWarnings({ "WeakerAccess", "unused" }) -public abstract class ContentProviderBuilderModule { - - @ContributesAndroidInjector - abstract CategoryContentProvider bindCategoryContentProvider(); - - @ContributesAndroidInjector - abstract RecentSearchesContentProvider bindRecentSearchesContentProvider(); - - @ContributesAndroidInjector - abstract BookmarkPicturesContentProvider bindBookmarkContentProvider(); - - @ContributesAndroidInjector - abstract BookmarkLocationsContentProvider bindBookmarkLocationContentProvider(); - - @ContributesAndroidInjector - abstract BookmarkItemsContentProvider bindBookmarkItemContentProvider(); - - @ContributesAndroidInjector - abstract RecentLanguagesContentProvider bindRecentLanguagesContentProvider(); -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt new file mode 100644 index 000000000..1882f77a9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt @@ -0,0 +1,37 @@ +package fr.free.nrw.commons.di + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider +import fr.free.nrw.commons.category.CategoryContentProvider +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider +import fr.free.nrw.commons.recentlanguages.RecentLanguagesContentProvider + +/** + * This Class Represents the Module for dependency injection (using dagger) + * so, if a developer needs to add a new ContentProvider to the commons app + * then that must be mentioned here to inject the dependencies + */ +@Module +@Suppress("unused") +abstract class ContentProviderBuilderModule { + @ContributesAndroidInjector + abstract fun bindCategoryContentProvider(): CategoryContentProvider + + @ContributesAndroidInjector + abstract fun bindRecentSearchesContentProvider(): RecentSearchesContentProvider + + @ContributesAndroidInjector + abstract fun bindBookmarkContentProvider(): BookmarkPicturesContentProvider + + @ContributesAndroidInjector + abstract fun bindBookmarkLocationContentProvider(): BookmarkLocationsContentProvider + + @ContributesAndroidInjector + abstract fun bindBookmarkItemContentProvider(): BookmarkItemsContentProvider + + @ContributesAndroidInjector + abstract fun bindRecentLanguagesContentProvider(): RecentLanguagesContentProvider +} 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 deleted file mode 100644 index 698ca1500..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ /dev/null @@ -1,166 +0,0 @@ -package fr.free.nrw.commons.di; - -import dagger.Module; -import dagger.android.ContributesAndroidInjector; -import fr.free.nrw.commons.bookmarks.BookmarkFragment; -import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment; -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment; -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; -import fr.free.nrw.commons.contributions.ContributionsFragment; -import fr.free.nrw.commons.contributions.ContributionsListFragment; -import fr.free.nrw.commons.customselector.ui.selector.FolderFragment; -import fr.free.nrw.commons.customselector.ui.selector.ImageFragment; -import fr.free.nrw.commons.explore.ExploreFragment; -import fr.free.nrw.commons.explore.ExploreListRootFragment; -import fr.free.nrw.commons.explore.ExploreMapRootFragment; -import fr.free.nrw.commons.explore.map.ExploreMapFragment; -import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment; -import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment; -import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment; -import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment; -import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment; -import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment; -import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment; -import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment; -import fr.free.nrw.commons.explore.media.SearchMediaFragment; -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; -import fr.free.nrw.commons.media.MediaDetailFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.navtab.MoreBottomSheetFragment; -import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment; -import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment; -import fr.free.nrw.commons.profile.achievements.AchievementsFragment; -import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment; -import fr.free.nrw.commons.review.ReviewImageFragment; -import fr.free.nrw.commons.settings.SettingsFragment; -import fr.free.nrw.commons.upload.FailedUploadsFragment; -import fr.free.nrw.commons.upload.PendingUploadsFragment; -import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment; -import fr.free.nrw.commons.upload.depicts.DepictsFragment; -import fr.free.nrw.commons.upload.license.MediaLicenseFragment; -import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment; - -/** - * This Class Represents the Module for dependency injection (using dagger) - * so, if a developer needs to add a new Fragment to the commons app - * then that must be mentioned here to inject the dependencies - */ -@Module -@SuppressWarnings({"WeakerAccess", "unused"}) -public abstract class FragmentBuilderModule { - - @ContributesAndroidInjector - abstract ContributionsListFragment bindContributionsListFragment(); - - @ContributesAndroidInjector - abstract MediaDetailFragment bindMediaDetailFragment(); - - @ContributesAndroidInjector - abstract FolderFragment bindFolderFragment(); - - @ContributesAndroidInjector - abstract ImageFragment bindImageFragment(); - - @ContributesAndroidInjector - abstract MediaDetailPagerFragment bindMediaDetailPagerFragment(); - - @ContributesAndroidInjector - abstract SettingsFragment bindSettingsFragment(); - - @ContributesAndroidInjector - abstract DepictedImagesFragment bindDepictedImagesFragment(); - - @ContributesAndroidInjector - abstract SearchMediaFragment bindBrowseImagesListFragment(); - - @ContributesAndroidInjector - abstract SearchCategoryFragment bindSearchCategoryListFragment(); - - @ContributesAndroidInjector - abstract SearchDepictionsFragment bindSearchDepictionListFragment(); - - @ContributesAndroidInjector - abstract RecentSearchesFragment bindRecentSearchesFragment(); - - @ContributesAndroidInjector - abstract ContributionsFragment bindContributionsFragment(); - - @ContributesAndroidInjector(modules = NearbyParentFragmentModule.class) - abstract NearbyParentFragment bindNearbyParentFragment(); - - @ContributesAndroidInjector - abstract BookmarkPicturesFragment bindBookmarkPictureListFragment(); - - @ContributesAndroidInjector(modules = BookmarkLocationsFragmentModule.class) - abstract BookmarkLocationsFragment bindBookmarkLocationListFragment(); - - @ContributesAndroidInjector(modules = BookmarkItemsFragmentModule.class) - abstract BookmarkItemsFragment bindBookmarkItemListFragment(); - - @ContributesAndroidInjector - abstract ReviewImageFragment bindReviewOutOfContextFragment(); - - @ContributesAndroidInjector - abstract UploadMediaDetailFragment bindUploadMediaDetailFragment(); - - @ContributesAndroidInjector - abstract UploadCategoriesFragment bindUploadCategoriesFragment(); - - @ContributesAndroidInjector - abstract DepictsFragment bindDepictsFragment(); - - @ContributesAndroidInjector - abstract MediaLicenseFragment bindMediaLicenseFragment(); - - @ContributesAndroidInjector - abstract ParentDepictionsFragment bindParentDepictionsFragment(); - - @ContributesAndroidInjector - abstract ChildDepictionsFragment bindChildDepictionsFragment(); - - @ContributesAndroidInjector - abstract CategoriesMediaFragment bindCategoriesMediaFragment(); - - @ContributesAndroidInjector - abstract SubCategoriesFragment bindSubCategoriesFragment(); - - @ContributesAndroidInjector - abstract ParentCategoriesFragment bindParentCategoriesFragment(); - - @ContributesAndroidInjector - abstract ExploreFragment bindExploreFragmentFragment(); - - @ContributesAndroidInjector - abstract ExploreListRootFragment bindExploreFeaturedRootFragment(); - - @ContributesAndroidInjector(modules = ExploreMapFragmentModule.class) - abstract ExploreMapFragment bindExploreNearbyUploadsFragment(); - - @ContributesAndroidInjector - abstract ExploreMapRootFragment bindExploreNearbyUploadsRootFragment(); - - @ContributesAndroidInjector - abstract BookmarkListRootFragment bindBookmarkListRootFragment(); - - @ContributesAndroidInjector - abstract BookmarkFragment bindBookmarkFragmentFragment(); - - @ContributesAndroidInjector - abstract MoreBottomSheetFragment bindMoreBottomSheetFragment(); - - @ContributesAndroidInjector - abstract MoreBottomSheetLoggedOutFragment bindMoreBottomSheetLoggedOutFragment(); - - @ContributesAndroidInjector - abstract AchievementsFragment bindAchievementsFragment(); - - @ContributesAndroidInjector - abstract LeaderboardFragment bindLeaderboardFragment(); - - @ContributesAndroidInjector - abstract PendingUploadsFragment bindPendingUploadsFragment(); - - @ContributesAndroidInjector - abstract FailedUploadsFragment bindFailedUploadsFragment(); -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.kt b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.kt new file mode 100644 index 000000000..bfdb90181 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.kt @@ -0,0 +1,165 @@ +package fr.free.nrw.commons.di + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import fr.free.nrw.commons.bookmarks.BookmarkFragment +import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment +import fr.free.nrw.commons.contributions.ContributionsFragment +import fr.free.nrw.commons.contributions.ContributionsListFragment +import fr.free.nrw.commons.customselector.ui.selector.FolderFragment +import fr.free.nrw.commons.customselector.ui.selector.ImageFragment +import fr.free.nrw.commons.explore.ExploreFragment +import fr.free.nrw.commons.explore.ExploreListRootFragment +import fr.free.nrw.commons.explore.ExploreMapRootFragment +import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment +import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment +import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment +import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment +import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment +import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment +import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment +import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment +import fr.free.nrw.commons.explore.map.ExploreMapFragment +import fr.free.nrw.commons.explore.media.SearchMediaFragment +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment +import fr.free.nrw.commons.media.MediaDetailFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.navtab.MoreBottomSheetFragment +import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment +import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment +import fr.free.nrw.commons.profile.achievements.AchievementsFragment +import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment +import fr.free.nrw.commons.review.ReviewImageFragment +import fr.free.nrw.commons.settings.SettingsFragment +import fr.free.nrw.commons.upload.FailedUploadsFragment +import fr.free.nrw.commons.upload.PendingUploadsFragment +import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment +import fr.free.nrw.commons.upload.depicts.DepictsFragment +import fr.free.nrw.commons.upload.license.MediaLicenseFragment +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment + +/** + * This Class Represents the Module for dependency injection (using dagger) + * so, if a developer needs to add a new Fragment to the commons app + * then that must be mentioned here to inject the dependencies + */ +@Module +@Suppress("unused") +abstract class FragmentBuilderModule { + @ContributesAndroidInjector + abstract fun bindContributionsListFragment(): ContributionsListFragment + + @ContributesAndroidInjector + abstract fun bindMediaDetailFragment(): MediaDetailFragment + + @ContributesAndroidInjector + abstract fun bindFolderFragment(): FolderFragment + + @ContributesAndroidInjector + abstract fun bindImageFragment(): ImageFragment + + @ContributesAndroidInjector + abstract fun bindMediaDetailPagerFragment(): MediaDetailPagerFragment + + @ContributesAndroidInjector + abstract fun bindSettingsFragment(): SettingsFragment + + @ContributesAndroidInjector + abstract fun bindDepictedImagesFragment(): DepictedImagesFragment + + @ContributesAndroidInjector + abstract fun bindBrowseImagesListFragment(): SearchMediaFragment + + @ContributesAndroidInjector + abstract fun bindSearchCategoryListFragment(): SearchCategoryFragment + + @ContributesAndroidInjector + abstract fun bindSearchDepictionListFragment(): SearchDepictionsFragment + + @ContributesAndroidInjector + abstract fun bindRecentSearchesFragment(): RecentSearchesFragment + + @ContributesAndroidInjector + abstract fun bindContributionsFragment(): ContributionsFragment + + @ContributesAndroidInjector(modules = [NearbyParentFragmentModule::class]) + abstract fun bindNearbyParentFragment(): NearbyParentFragment + + @ContributesAndroidInjector + abstract fun bindBookmarkPictureListFragment(): BookmarkPicturesFragment + + @ContributesAndroidInjector(modules = [BookmarkLocationsFragmentModule::class]) + abstract fun bindBookmarkLocationListFragment(): BookmarkLocationsFragment + + @ContributesAndroidInjector(modules = [BookmarkItemsFragmentModule::class]) + abstract fun bindBookmarkItemListFragment(): BookmarkItemsFragment + + @ContributesAndroidInjector + abstract fun bindReviewOutOfContextFragment(): ReviewImageFragment + + @ContributesAndroidInjector + abstract fun bindUploadMediaDetailFragment(): UploadMediaDetailFragment + + @ContributesAndroidInjector + abstract fun bindUploadCategoriesFragment(): UploadCategoriesFragment + + @ContributesAndroidInjector + abstract fun bindDepictsFragment(): DepictsFragment + + @ContributesAndroidInjector + abstract fun bindMediaLicenseFragment(): MediaLicenseFragment + + @ContributesAndroidInjector + abstract fun bindParentDepictionsFragment(): ParentDepictionsFragment + + @ContributesAndroidInjector + abstract fun bindChildDepictionsFragment(): ChildDepictionsFragment + + @ContributesAndroidInjector + abstract fun bindCategoriesMediaFragment(): CategoriesMediaFragment + + @ContributesAndroidInjector + abstract fun bindSubCategoriesFragment(): SubCategoriesFragment + + @ContributesAndroidInjector + abstract fun bindParentCategoriesFragment(): ParentCategoriesFragment + + @ContributesAndroidInjector + abstract fun bindExploreFragmentFragment(): ExploreFragment + + @ContributesAndroidInjector + abstract fun bindExploreFeaturedRootFragment(): ExploreListRootFragment + + @ContributesAndroidInjector(modules = [ExploreMapFragmentModule::class]) + abstract fun bindExploreNearbyUploadsFragment(): ExploreMapFragment + + @ContributesAndroidInjector + abstract fun bindExploreNearbyUploadsRootFragment(): ExploreMapRootFragment + + @ContributesAndroidInjector + abstract fun bindBookmarkListRootFragment(): BookmarkListRootFragment + + @ContributesAndroidInjector + abstract fun bindBookmarkFragmentFragment(): BookmarkFragment + + @ContributesAndroidInjector + abstract fun bindMoreBottomSheetFragment(): MoreBottomSheetFragment + + @ContributesAndroidInjector + abstract fun bindMoreBottomSheetLoggedOutFragment(): MoreBottomSheetLoggedOutFragment + + @ContributesAndroidInjector + abstract fun bindAchievementsFragment(): AchievementsFragment + + @ContributesAndroidInjector + abstract fun bindLeaderboardFragment(): LeaderboardFragment + + @ContributesAndroidInjector + abstract fun bindPendingUploadsFragment(): PendingUploadsFragment + + @ContributesAndroidInjector + abstract fun bindFailedUploadsFragment(): FailedUploadsFragment +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java deleted file mode 100644 index 6aef8d323..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java +++ /dev/null @@ -1,350 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.content.Context; -import androidx.annotation.NonNull; -import com.google.gson.Gson; -import dagger.Module; -import dagger.Provides; -import fr.free.nrw.commons.BetaConstants; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.OkHttpConnectionFactory; -import fr.free.nrw.commons.actions.PageEditClient; -import fr.free.nrw.commons.actions.PageEditInterface; -import fr.free.nrw.commons.actions.ThanksInterface; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.auth.csrf.CsrfTokenClient; -import fr.free.nrw.commons.auth.csrf.CsrfTokenInterface; -import fr.free.nrw.commons.auth.csrf.LogoutClient; -import fr.free.nrw.commons.auth.login.LoginClient; -import fr.free.nrw.commons.auth.login.LoginInterface; -import fr.free.nrw.commons.category.CategoryInterface; -import fr.free.nrw.commons.explore.depictions.DepictsClient; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.media.MediaDetailInterface; -import fr.free.nrw.commons.media.MediaInterface; -import fr.free.nrw.commons.media.PageMediaInterface; -import fr.free.nrw.commons.media.WikidataMediaInterface; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import fr.free.nrw.commons.mwapi.UserInterface; -import fr.free.nrw.commons.notification.NotificationInterface; -import fr.free.nrw.commons.review.ReviewInterface; -import fr.free.nrw.commons.upload.UploadInterface; -import fr.free.nrw.commons.upload.WikiBaseInterface; -import fr.free.nrw.commons.upload.depicts.DepictsInterface; -import fr.free.nrw.commons.wikidata.CommonsServiceFactory; -import fr.free.nrw.commons.wikidata.WikidataInterface; -import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar; -import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage; -import java.io.File; -import java.util.Locale; -import java.util.concurrent.TimeUnit; -import javax.inject.Named; -import javax.inject.Singleton; -import okhttp3.Cache; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.logging.HttpLoggingInterceptor; -import okhttp3.logging.HttpLoggingInterceptor.Level; -import fr.free.nrw.commons.wikidata.model.WikiSite; -import fr.free.nrw.commons.wikidata.GsonUtil; -import timber.log.Timber; - -@Module -@SuppressWarnings({"WeakerAccess", "unused"}) -public class NetworkingModule { - private static final String WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql"; - private static final String TOOLS_FORGE_URL = "https://tools.wmflabs.org/commons-android-app/tool-commons-android-app"; - - public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024; - - private static final String NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite"; - private static final String NAMED_WIKI_PEDIA_WIKI_SITE = "wikipedia-wikisite"; - - public static final String NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE = "language-wikipedia-wikisite"; - - public static final String NAMED_COMMONS_CSRF = "commons-csrf"; - public static final String NAMED_WIKI_CSRF = "wiki-csrf"; - - @Provides - @Singleton - public OkHttpClient provideOkHttpClient(Context context, - HttpLoggingInterceptor httpLoggingInterceptor) { - File dir = new File(context.getCacheDir(), "okHttpCache"); - return new OkHttpClient.Builder() - .connectTimeout(120, TimeUnit.SECONDS) - .writeTimeout(120, TimeUnit.SECONDS) - .addInterceptor(httpLoggingInterceptor) - .readTimeout(120, TimeUnit.SECONDS) - .cache(new Cache(dir, OK_HTTP_CACHE_SIZE)) - .build(); - } - - @Provides - @Singleton - public CommonsServiceFactory serviceFactory(CommonsCookieJar cookieJar) { - return new CommonsServiceFactory(OkHttpConnectionFactory.getClient(cookieJar)); - } - - @Provides - @Singleton - public HttpLoggingInterceptor provideHttpLoggingInterceptor() { - HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(message -> { - Timber.tag("OkHttp").v(message); - }); - httpLoggingInterceptor.setLevel(BuildConfig.DEBUG ? Level.BODY: Level.BASIC); - return httpLoggingInterceptor; - } - - @Provides - @Singleton - public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient, - DepictsClient depictsClient, - @Named("tools_forge") HttpUrl toolsForgeUrl, - @Named("default_preferences") JsonKvStore defaultKvStore, - Gson gson) { - return new OkHttpJsonApiClient(okHttpClient, - depictsClient, - toolsForgeUrl, - WIKIDATA_SPARQL_QUERY_URL, - BuildConfig.WIKIMEDIA_CAMPAIGNS_URL, - gson); - } - - @Provides - @Singleton - public CommonsCookieStorage provideCookieStorage( - @Named("default_preferences") JsonKvStore preferences) { - CommonsCookieStorage cookieStorage = new CommonsCookieStorage(preferences); - cookieStorage.load(); - return cookieStorage; - } - - @Provides - @Singleton - public CommonsCookieJar provideCookieJar(CommonsCookieStorage storage) { - return new CommonsCookieJar(storage); - } - - @Named(NAMED_COMMONS_CSRF) - @Provides - @Singleton - public CsrfTokenClient provideCommonsCsrfTokenClient(SessionManager sessionManager, - @Named("commons-csrf-interface") CsrfTokenInterface tokenInterface, LoginClient loginClient, LogoutClient logoutClient) { - return new CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient); - } - - /** - * Provides a singleton instance of CsrfTokenClient for Wikidata. - * - * @param sessionManager The session manager to manage user sessions. - * @param tokenInterface The interface for obtaining CSRF tokens. - * @param loginClient The client for handling login operations. - * @param logoutClient The client for handling logout operations. - * @return A singleton instance of CsrfTokenClient. - */ - @Named(NAMED_WIKI_CSRF) - @Provides - @Singleton - public CsrfTokenClient provideWikiCsrfTokenClient(SessionManager sessionManager, - @Named("wikidata-csrf-interface") CsrfTokenInterface tokenInterface, LoginClient loginClient, LogoutClient logoutClient) { - return new CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient); - } - - /** - * Provides a singleton instance of CsrfTokenInterface for Wikidata. - * - * @param serviceFactory The factory used to create service interfaces. - * @return A singleton instance of CsrfTokenInterface for Wikidata. - */ - @Named("wikidata-csrf-interface") - @Provides - @Singleton - public CsrfTokenInterface provideWikidataCsrfTokenInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.WIKIDATA_URL, CsrfTokenInterface.class); - } - - @Named("commons-csrf-interface") - @Provides - @Singleton - public CsrfTokenInterface provideCsrfTokenInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, CsrfTokenInterface.class); - } - - @Provides - @Singleton - public LoginInterface provideLoginInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, LoginInterface.class); - } - - @Provides - @Singleton - public LoginClient provideLoginClient(LoginInterface loginInterface) { - return new LoginClient(loginInterface); - } - - @Provides - @Named("wikimedia_api_host") - @NonNull - @SuppressWarnings("ConstantConditions") - public String provideMwApiUrl() { - return BuildConfig.WIKIMEDIA_API_HOST; - } - - @Provides - @Named("tools_forge") - @NonNull - @SuppressWarnings("ConstantConditions") - public HttpUrl provideToolsForgeUrl() { - return HttpUrl.parse(TOOLS_FORGE_URL); - } - - @Provides - @Singleton - @Named(NAMED_WIKI_DATA_WIKI_SITE) - public WikiSite provideWikidataWikiSite() { - return new WikiSite(BuildConfig.WIKIDATA_URL); - } - - - /** - * Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere. - * @return returns a singleton Gson instance - */ - @Provides - @Singleton - public Gson provideGson() { - return GsonUtil.getDefaultGson(); - } - - @Provides - @Singleton - public ReviewInterface provideReviewInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, ReviewInterface.class); - } - - @Provides - @Singleton - public DepictsInterface provideDepictsInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.WIKIDATA_URL, DepictsInterface.class); - } - - @Provides - @Singleton - public WikiBaseInterface provideWikiBaseInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, WikiBaseInterface.class); - } - - @Provides - @Singleton - public UploadInterface provideUploadInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, UploadInterface.class); - } - - @Named("commons-page-edit-service") - @Provides - @Singleton - public PageEditInterface providePageEditService(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, PageEditInterface.class); - } - - @Named("wikidata-page-edit-service") - @Provides - @Singleton - public PageEditInterface provideWikiDataPageEditService(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.WIKIDATA_URL, PageEditInterface.class); - } - - @Named("commons-page-edit") - @Provides - @Singleton - public PageEditClient provideCommonsPageEditClient(@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient, - @Named("commons-page-edit-service") PageEditInterface pageEditInterface) { - return new PageEditClient(csrfTokenClient, pageEditInterface); - } - - /** - * Provides a singleton instance of PageEditClient for Wikidata. - * - * @param csrfTokenClient The client used to manage CSRF tokens. - * @param pageEditInterface The interface for page edit operations. - * @return A singleton instance of PageEditClient for Wikidata. - */ - @Named("wikidata-page-edit") - @Provides - @Singleton - public PageEditClient provideWikidataPageEditClient(@Named(NAMED_WIKI_CSRF) CsrfTokenClient csrfTokenClient, - @Named("wikidata-page-edit-service") PageEditInterface pageEditInterface) { - return new PageEditClient(csrfTokenClient, pageEditInterface); - } - - @Provides - @Singleton - public MediaInterface provideMediaInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, MediaInterface.class); - } - - /** - * Add provider for WikidataMediaInterface - * It creates a retrofit service for the commons wiki site - * @param commonsWikiSite commonsWikiSite - * @return WikidataMediaInterface - */ - @Provides - @Singleton - public WikidataMediaInterface provideWikidataMediaInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BetaConstants.COMMONS_URL, WikidataMediaInterface.class); - } - - @Provides - @Singleton - public MediaDetailInterface providesMediaDetailInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, MediaDetailInterface.class); - } - - @Provides - @Singleton - public CategoryInterface provideCategoryInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, CategoryInterface.class); - } - - @Provides - @Singleton - public ThanksInterface provideThanksInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, ThanksInterface.class); - } - - @Provides - @Singleton - public NotificationInterface provideNotificationInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, NotificationInterface.class); - } - - @Provides - @Singleton - public UserInterface provideUserInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, UserInterface.class); - } - - @Provides - @Singleton - public WikidataInterface provideWikidataInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.WIKIDATA_URL, WikidataInterface.class); - } - - /** - * Add provider for PageMediaInterface - * It creates a retrofit service for the wiki site using device's current language - */ - @Provides - @Singleton - public PageMediaInterface providePageMediaInterface(@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) WikiSite wikiSite, CommonsServiceFactory serviceFactory) { - return serviceFactory.create(wikiSite.url(), PageMediaInterface.class); - } - - @Provides - @Singleton - @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) - public WikiSite provideLanguageWikipediaSite() { - return WikiSite.forLanguageCode(Locale.getDefault().getLanguage()); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt new file mode 100644 index 000000000..5ecc04120 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt @@ -0,0 +1,316 @@ +package fr.free.nrw.commons.di + +import android.content.Context +import com.google.gson.Gson +import dagger.Module +import dagger.Provides +import fr.free.nrw.commons.BetaConstants +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.OkHttpConnectionFactory +import fr.free.nrw.commons.actions.PageEditClient +import fr.free.nrw.commons.actions.PageEditInterface +import fr.free.nrw.commons.actions.ThanksInterface +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient +import fr.free.nrw.commons.auth.csrf.CsrfTokenInterface +import fr.free.nrw.commons.auth.csrf.LogoutClient +import fr.free.nrw.commons.auth.login.LoginClient +import fr.free.nrw.commons.auth.login.LoginInterface +import fr.free.nrw.commons.category.CategoryInterface +import fr.free.nrw.commons.explore.depictions.DepictsClient +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.media.MediaDetailInterface +import fr.free.nrw.commons.media.MediaInterface +import fr.free.nrw.commons.media.PageMediaInterface +import fr.free.nrw.commons.media.WikidataMediaInterface +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.mwapi.UserInterface +import fr.free.nrw.commons.notification.NotificationInterface +import fr.free.nrw.commons.review.ReviewInterface +import fr.free.nrw.commons.upload.UploadInterface +import fr.free.nrw.commons.upload.WikiBaseInterface +import fr.free.nrw.commons.upload.depicts.DepictsInterface +import fr.free.nrw.commons.wikidata.CommonsServiceFactory +import fr.free.nrw.commons.wikidata.GsonUtil +import fr.free.nrw.commons.wikidata.WikidataInterface +import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar +import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage +import fr.free.nrw.commons.wikidata.model.WikiSite +import okhttp3.Cache +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import okhttp3.logging.HttpLoggingInterceptor.Level +import timber.log.Timber +import java.io.File +import java.util.Locale +import java.util.concurrent.TimeUnit +import javax.inject.Named +import javax.inject.Singleton + +@Module +@Suppress("unused") +class NetworkingModule { + @Provides + @Singleton + fun provideOkHttpClient( + context: Context, + httpLoggingInterceptor: HttpLoggingInterceptor + ): OkHttpClient = OkHttpClient.Builder() + .connectTimeout(120, TimeUnit.SECONDS) + .writeTimeout(120, TimeUnit.SECONDS) + .addInterceptor(httpLoggingInterceptor) + .readTimeout(120, TimeUnit.SECONDS) + .cache(Cache(File(context.cacheDir, "okHttpCache"), OK_HTTP_CACHE_SIZE)) + .build() + + @Provides + @Singleton + fun serviceFactory(cookieJar: CommonsCookieJar): CommonsServiceFactory = + CommonsServiceFactory(OkHttpConnectionFactory.getClient(cookieJar)) + + @Provides + @Singleton + fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor = + HttpLoggingInterceptor { message: String? -> + Timber.tag("OkHttp").v(message) + }.apply { + level = if (BuildConfig.DEBUG) Level.BODY else Level.BASIC + } + + @Provides + @Singleton + fun provideOkHttpJsonApiClient( + okHttpClient: OkHttpClient, + depictsClient: DepictsClient, + @Named("tools_forge") toolsForgeUrl: HttpUrl, + gson: Gson + ): OkHttpJsonApiClient = OkHttpJsonApiClient( + okHttpClient, depictsClient, toolsForgeUrl, WIKIDATA_SPARQL_QUERY_URL, + BuildConfig.WIKIMEDIA_CAMPAIGNS_URL, gson + ) + + @Provides + @Singleton + fun provideCookieStorage( + @Named("default_preferences") preferences: JsonKvStore + ): CommonsCookieStorage = CommonsCookieStorage(preferences).also { + it.load() + } + + @Provides + @Singleton + fun provideCookieJar(storage: CommonsCookieStorage): CommonsCookieJar = + CommonsCookieJar(storage) + + @Named(NAMED_COMMONS_CSRF) + @Provides + @Singleton + fun provideCommonsCsrfTokenClient( + sessionManager: SessionManager, + @Named("commons-csrf-interface") tokenInterface: CsrfTokenInterface, + loginClient: LoginClient, + logoutClient: LogoutClient + ): CsrfTokenClient = CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient) + + /** + * Provides a singleton instance of CsrfTokenClient for Wikidata. + * + * @param sessionManager The session manager to manage user sessions. + * @param tokenInterface The interface for obtaining CSRF tokens. + * @param loginClient The client for handling login operations. + * @param logoutClient The client for handling logout operations. + * @return A singleton instance of CsrfTokenClient. + */ + @Named(NAMED_WIKI_CSRF) + @Provides + @Singleton + fun provideWikiCsrfTokenClient( + sessionManager: SessionManager, + @Named("wikidata-csrf-interface") tokenInterface: CsrfTokenInterface, + loginClient: LoginClient, + logoutClient: LogoutClient + ): CsrfTokenClient = CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient) + + /** + * Provides a singleton instance of CsrfTokenInterface for Wikidata. + * + * @param factory The factory used to create service interfaces. + * @return A singleton instance of CsrfTokenInterface for Wikidata. + */ + @Named("wikidata-csrf-interface") + @Provides + @Singleton + fun provideWikidataCsrfTokenInterface(factory: CommonsServiceFactory): CsrfTokenInterface = + factory.create(BuildConfig.WIKIDATA_URL) + + @Named("commons-csrf-interface") + @Provides + @Singleton + fun provideCsrfTokenInterface(factory: CommonsServiceFactory): CsrfTokenInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideLoginInterface(factory: CommonsServiceFactory): LoginInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideLoginClient(loginInterface: LoginInterface): LoginClient = + LoginClient(loginInterface) + + @Provides + @Named("tools_forge") + fun provideToolsForgeUrl(): HttpUrl = TOOLS_FORGE_URL.toHttpUrlOrNull()!! + + @Provides + @Singleton + @Named(NAMED_WIKI_DATA_WIKI_SITE) + fun provideWikidataWikiSite(): WikiSite = WikiSite(BuildConfig.WIKIDATA_URL) + + + /** + * Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere. + * @return returns a singleton Gson instance + */ + @Provides + @Singleton + fun provideGson(): Gson = GsonUtil.getDefaultGson() + + @Provides + @Singleton + fun provideReviewInterface(factory: CommonsServiceFactory): ReviewInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideDepictsInterface(factory: CommonsServiceFactory): DepictsInterface = + factory.create(BuildConfig.WIKIDATA_URL) + + @Provides + @Singleton + fun provideWikiBaseInterface(factory: CommonsServiceFactory): WikiBaseInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideUploadInterface(factory: CommonsServiceFactory): UploadInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Named("commons-page-edit-service") + @Provides + @Singleton + fun providePageEditService(factory: CommonsServiceFactory): PageEditInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Named("wikidata-page-edit-service") + @Provides + @Singleton + fun provideWikiDataPageEditService(factory: CommonsServiceFactory): PageEditInterface = + factory.create(BuildConfig.WIKIDATA_URL) + + @Named("commons-page-edit") + @Provides + @Singleton + fun provideCommonsPageEditClient( + @Named(NAMED_COMMONS_CSRF) csrfTokenClient: CsrfTokenClient, + @Named("commons-page-edit-service") pageEditInterface: PageEditInterface + ): PageEditClient = PageEditClient(csrfTokenClient, pageEditInterface) + + /** + * Provides a singleton instance of PageEditClient for Wikidata. + * + * @param csrfTokenClient The client used to manage CSRF tokens. + * @param pageEditInterface The interface for page edit operations. + * @return A singleton instance of PageEditClient for Wikidata. + */ + @Named("wikidata-page-edit") + @Provides + @Singleton + fun provideWikidataPageEditClient( + @Named(NAMED_WIKI_CSRF) csrfTokenClient: CsrfTokenClient, + @Named("wikidata-page-edit-service") pageEditInterface: PageEditInterface + ): PageEditClient = PageEditClient(csrfTokenClient, pageEditInterface) + + @Provides + @Singleton + fun provideMediaInterface(factory: CommonsServiceFactory): MediaInterface = + factory.create(BuildConfig.COMMONS_URL) + + /** + * Add provider for WikidataMediaInterface + * It creates a retrofit service for the commons wiki site + * @param commonsWikiSite commonsWikiSite + * @return WikidataMediaInterface + */ + @Provides + @Singleton + fun provideWikidataMediaInterface(factory: CommonsServiceFactory): WikidataMediaInterface = + factory.create(BetaConstants.COMMONS_URL) + + @Provides + @Singleton + fun providesMediaDetailInterface(factory: CommonsServiceFactory): MediaDetailInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideCategoryInterface(factory: CommonsServiceFactory): CategoryInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideThanksInterface(factory: CommonsServiceFactory): ThanksInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideNotificationInterface(factory: CommonsServiceFactory): NotificationInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideUserInterface(factory: CommonsServiceFactory): UserInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideWikidataInterface(factory: CommonsServiceFactory): WikidataInterface = + factory.create(BuildConfig.WIKIDATA_URL) + + /** + * Add provider for PageMediaInterface + * It creates a retrofit service for the wiki site using device's current language + */ + @Provides + @Singleton + fun providePageMediaInterface( + @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) wikiSite: WikiSite, + factory: CommonsServiceFactory + ): PageMediaInterface = factory.create(wikiSite.url()) + + @Provides + @Singleton + @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) + fun provideLanguageWikipediaSite(): WikiSite { + return WikiSite.forLanguageCode(Locale.getDefault().language) + } + + companion object { + private const val WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql" + private const val TOOLS_FORGE_URL = + "https://tools.wmflabs.org/commons-android-app/tool-commons-android-app" + + const val OK_HTTP_CACHE_SIZE: Long = (10 * 1024 * 1024).toLong() + + private const val NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite" + private const val NAMED_WIKI_PEDIA_WIKI_SITE = "wikipedia-wikisite" + + const val NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE: String = "language-wikipedia-wikisite" + + const val NAMED_COMMONS_CSRF: String = "commons-csrf" + const val NAMED_WIKI_CSRF: String = "wiki-csrf" + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java deleted file mode 100644 index 1fb52c937..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java +++ /dev/null @@ -1,19 +0,0 @@ -package fr.free.nrw.commons.di; - -import dagger.Module; -import dagger.android.ContributesAndroidInjector; -import fr.free.nrw.commons.auth.WikiAccountAuthenticatorService; - -/** - * This Class Represents the Module for dependency injection (using dagger) - * so, if a developer needs to add a new Service to the commons app - * then that must be mentioned here to inject the dependencies - */ -@Module -@SuppressWarnings({"WeakerAccess", "unused"}) -public abstract class ServiceBuilderModule { - - @ContributesAndroidInjector - abstract WikiAccountAuthenticatorService bindWikiAccountAuthenticatorService(); - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.kt b/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.kt new file mode 100644 index 000000000..45dbd5721 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.kt @@ -0,0 +1,17 @@ +package fr.free.nrw.commons.di + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import fr.free.nrw.commons.auth.WikiAccountAuthenticatorService + +/** + * This Class Represents the Module for dependency injection (using dagger) + * so, if a developer needs to add a new Service to the commons app + * then that must be mentioned here to inject the dependencies + */ +@Module +@Suppress("unused") +abstract class ServiceBuilderModule { + @ContributesAndroidInjector + abstract fun bindWikiAccountAuthenticatorService(): WikiAccountAuthenticatorService +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java index f810d0480..f587893c5 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java @@ -3,7 +3,10 @@ package fr.free.nrw.commons.mwapi; import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_PREFIX; import com.google.gson.Gson; +import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.category.CategoryItem; +import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage; +import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse; import io.reactivex.Single; import java.util.ArrayList; import java.util.Collections; @@ -11,14 +14,11 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import javax.inject.Inject; -import javax.inject.Named; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage; -import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse; import timber.log.Timber; /** @@ -30,14 +30,11 @@ import timber.log.Timber; public class CategoryApi { private final OkHttpClient okHttpClient; - private final String commonsBaseUrl; private final Gson gson; @Inject - public CategoryApi(OkHttpClient okHttpClient, Gson gson, - @Named("wikimedia_api_host") String commonsBaseUrl) { + public CategoryApi(final OkHttpClient okHttpClient, final Gson gson) { this.okHttpClient = okHttpClient; - this.commonsBaseUrl = commonsBaseUrl; this.gson = gson; } @@ -75,9 +72,9 @@ public class CategoryApi { * @param coords Coordinates to build query with * @return URL for API query */ - private HttpUrl buildUrl(String coords) { + private HttpUrl buildUrl(final String coords) { return HttpUrl - .parse(commonsBaseUrl) + .parse(BuildConfig.WIKIMEDIA_API_HOST) .newBuilder() .addQueryParameter("action", "query") .addQueryParameter("prop", "categories|coordinates|pageprops") diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.java index 36c558519..ecc9c19b5 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.upload; +import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; import android.content.Context; @@ -53,7 +54,7 @@ public class PendingUploadsPresenter implements UserActionListener { final ContributionsRemoteDataSource contributionsRemoteDataSource, final ContributionsRepository contributionsRepository, final UploadRepository uploadRepository, - @Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) { + @Named(IO_THREAD) final Scheduler ioThreadScheduler) { this.contributionBoundaryCallback = contributionBoundaryCallback; this.contributionsRepository = contributionsRepository; this.uploadRepository = uploadRepository; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt index 712f6fc3e..210754bf4 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt @@ -9,6 +9,8 @@ import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException import fr.free.nrw.commons.category.CategoryEditHelper import fr.free.nrw.commons.category.CategoryItem import fr.free.nrw.commons.di.CommonsApplicationModule +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.upload.depicts.proxy import io.reactivex.Observable @@ -30,8 +32,8 @@ class CategoriesPresenter @Inject constructor( private val repository: UploadRepository, - @param:Named(CommonsApplicationModule.IO_THREAD) private val ioScheduler: Scheduler, - @param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler, + @param:Named(IO_THREAD) private val ioScheduler: Scheduler, + @param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler, ) : CategoriesContract.UserActionListener { companion object { private val DUMMY: CategoriesContract.View = proxy() diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt index fa3eb354e..4502e3434 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt @@ -7,6 +7,8 @@ import fr.free.nrw.commons.Media import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException import fr.free.nrw.commons.bookmarks.items.BookmarkItemsController import fr.free.nrw.commons.di.CommonsApplicationModule +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import fr.free.nrw.commons.wikidata.WikidataDisambiguationItems @@ -31,8 +33,8 @@ class DepictsPresenter @Inject constructor( private val repository: UploadRepository, - @param:Named(CommonsApplicationModule.IO_THREAD) private val ioScheduler: Scheduler, - @param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler, + @param:Named(IO_THREAD) private val ioScheduler: Scheduler, + @param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler, ) : DepictsContract.UserActionListener { companion object { private val DUMMY = proxy() diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt index 39dbf0cad..ca523a21f 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt @@ -8,7 +8,7 @@ import retrofit2.converter.gson.GsonConverterFactory class CommonsServiceFactory( private val okHttpClient: OkHttpClient, ) { - private val builder: Retrofit.Builder by lazy { + val builder: Retrofit.Builder by lazy { // All instances of retrofit share this configuration, but create it lazily Retrofit .Builder() @@ -17,15 +17,11 @@ class CommonsServiceFactory( .addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson())) } - private val retrofitCache: MutableMap = mutableMapOf() + val retrofitCache: MutableMap = mutableMapOf() - fun create( - baseUrl: String, - service: Class, - ): T = - retrofitCache - .getOrPut(baseUrl) { - // Cache instances of retrofit based on API backend - builder.baseUrl(baseUrl).build() - }.create(service) + inline fun create(baseUrl: String): T = + retrofitCache.getOrPut(baseUrl) { + // Cache instances of retrofit based on API backend + builder.baseUrl(baseUrl).build() + }.create(T::class.java) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt index 4c38a30ff..c0e3bda08 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt @@ -37,9 +37,8 @@ class TestCommonsApplication : Application() { } @Suppress("MemberVisibilityCanBePrivate") -class MockCommonsApplicationModule( - appContext: Context, -) : CommonsApplicationModule(appContext) { +class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModule(appContext) { + val defaultSharedPreferences: JsonKvStore = mock() val locationServiceManager: LocationServiceManager = mock() val mockDbOpenHelper: DBOpenHelper = mock() @@ -50,16 +49,13 @@ class MockCommonsApplicationModule( val modificationClient: ContentProviderClient = mock() val uploadPrefs: JsonKvStore = mock() - override fun provideCategoryContentProviderClient(context: Context?): ContentProviderClient = categoryClient + override fun provideCategoryContentProviderClient(context: Context): ContentProviderClient = categoryClient - override fun provideContributionContentProviderClient(context: Context?): ContentProviderClient = contributionClient + override fun provideContributionContentProviderClient(context: Context): ContentProviderClient = contributionClient - override fun provideModificationContentProviderClient(context: Context?): ContentProviderClient = modificationClient + override fun provideModificationContentProviderClient(context: Context): ContentProviderClient = modificationClient - override fun providesDefaultKvStore( - context: Context, - gson: Gson, - ): JsonKvStore = defaultSharedPreferences + override fun providesDefaultKvStore(context: Context, gson: Gson): JsonKvStore = defaultSharedPreferences override fun provideLocationServiceManager(context: Context): LocationServiceManager = locationServiceManager From fb1ef3212daecdfa0334f29bcc1a18cd409a64cd Mon Sep 17 00:00:00 2001 From: Neel Doshi <60827173+neeldoshii@users.noreply.github.com> Date: Sat, 30 Nov 2024 07:21:53 +0530 Subject: [PATCH 49/74] Migrated Bookmark from `Java` to `Kotlin` (#5960) * Rename Bookmark Pages from `.java` to `.kt` * Migrated Bookmark Pages to kotlin --- .../nrw/commons/bookmarks/BookmarkPages.java | 32 ------------------- .../nrw/commons/bookmarks/BookmarkPages.kt | 8 +++++ 2 files changed, 8 insertions(+), 32 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.kt diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java deleted file mode 100644 index 71690c5e2..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java +++ /dev/null @@ -1,32 +0,0 @@ -package fr.free.nrw.commons.bookmarks; - -import androidx.fragment.app.Fragment; - -/** - * Data class for handling a bookmark fragment and it title - */ -public class BookmarkPages { - private Fragment page; - private String title; - - BookmarkPages(Fragment fragment, String title) { - this.title = title; - this.page = fragment; - } - - /** - * Return the fragment - * @return fragment object - */ - public Fragment getPage() { - return page; - } - - /** - * Return the fragment title - * @return title - */ - public String getTitle() { - return title; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.kt new file mode 100644 index 000000000..e0ade52fe --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.kt @@ -0,0 +1,8 @@ +package fr.free.nrw.commons.bookmarks + +import androidx.fragment.app.Fragment + +data class BookmarkPages ( + val page: Fragment? = null, + val title: String? = null +) \ No newline at end of file From 771f370f9a3e34d17d29c2b0d39e89d380d4e311 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Mon, 2 Dec 2024 13:24:26 +0530 Subject: [PATCH 50/74] Migration of locationpicker module from Java to Kotlin (#5981) * Rename .java to .kt * Migrated location picker module from Java to Kotlin --- .../LocationPicker/LocationPicker.java | 77 -- .../commons/LocationPicker/LocationPicker.kt | 72 ++ .../LocationPickerActivity.java | 681 ------------------ .../LocationPicker/LocationPickerActivity.kt | 678 +++++++++++++++++ .../LocationPickerConstants.java | 20 - .../LocationPicker/LocationPickerConstants.kt | 13 + .../LocationPickerViewModel.java | 63 -- .../LocationPicker/LocationPickerViewModel.kt | 44 ++ 8 files changed, 807 insertions(+), 841 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java create mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java create mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java create mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.kt diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java deleted file mode 100644 index 58801c499..000000000 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java +++ /dev/null @@ -1,77 +0,0 @@ -package fr.free.nrw.commons.LocationPicker; - -import android.app.Activity; -import android.content.Intent; -import fr.free.nrw.commons.CameraPosition; -import fr.free.nrw.commons.Media; - -/** - * Helper class for starting the activity - */ -public final class LocationPicker { - - /** - * Getting camera position from the intent using constants - * - * @param data intent - * @return CameraPosition - */ - public static CameraPosition getCameraPosition(final Intent data) { - return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION); - } - - public static class IntentBuilder { - - private final Intent intent; - - /** - * Creates a new builder that creates an intent to launch the place picker activity. - */ - public IntentBuilder() { - intent = new Intent(); - } - - /** - * Gets and puts location in intent - * @param position CameraPosition - * @return LocationPicker.IntentBuilder - */ - public LocationPicker.IntentBuilder defaultLocation( - final CameraPosition position) { - intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position); - return this; - } - - /** - * Gets and puts activity name in intent - * @param activity activity key - * @return LocationPicker.IntentBuilder - */ - public LocationPicker.IntentBuilder activityKey( - final String activity) { - intent.putExtra(LocationPickerConstants.ACTIVITY_KEY, activity); - return this; - } - - /** - * Gets and puts media in intent - * @param media Media - * @return LocationPicker.IntentBuilder - */ - public LocationPicker.IntentBuilder media( - final Media media) { - intent.putExtra(LocationPickerConstants.MEDIA, media); - return this; - } - - /** - * Gets and sets the activity - * @param activity Activity - * @return Intent - */ - public Intent build(final Activity activity) { - intent.setClass(activity, LocationPickerActivity.class); - return intent; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.kt b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.kt new file mode 100644 index 000000000..0bab50201 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.kt @@ -0,0 +1,72 @@ +package fr.free.nrw.commons.LocationPicker + +import android.app.Activity +import android.content.Intent +import fr.free.nrw.commons.CameraPosition +import fr.free.nrw.commons.Media + + +/** + * Helper class for starting the activity + */ +object LocationPicker { + + /** + * Getting camera position from the intent using constants + * + * @param data intent + * @return CameraPosition + */ + @JvmStatic + fun getCameraPosition(data: Intent): CameraPosition? { + return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION) + } + + class IntentBuilder + /** + * Creates a new builder that creates an intent to launch the place picker activity. + */() { + + private val intent: Intent = Intent() + + /** + * Gets and puts location in intent + * @param position CameraPosition + * @return LocationPicker.IntentBuilder + */ + fun defaultLocation(position: CameraPosition): IntentBuilder { + intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position) + return this + } + + /** + * Gets and puts activity name in intent + * @param activity activity key + * @return LocationPicker.IntentBuilder + */ + fun activityKey(activity: String): IntentBuilder { + intent.putExtra(LocationPickerConstants.ACTIVITY_KEY, activity) + return this + } + + /** + * Gets and puts media in intent + * @param media Media + * @return LocationPicker.IntentBuilder + */ + fun media(media: Media): IntentBuilder { + intent.putExtra(LocationPickerConstants.MEDIA, media) + return this + } + + /** + * Gets and sets the activity + * @param activity Activity + * @return Intent + */ + fun build(activity: Activity): Intent { + intent.setClass(activity, LocationPickerActivity::class.java) + return intent + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java deleted file mode 100644 index 40f360a24..000000000 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java +++ /dev/null @@ -1,681 +0,0 @@ -package fr.free.nrw.commons.LocationPicker; - -import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION; -import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM; -import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL; - -import android.Manifest.permission; -import android.annotation.SuppressLint; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.drawable.Drawable; -import android.location.LocationManager; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.text.Html; -import android.text.method.LinkMovementMethod; -import android.view.MotionEvent; -import android.view.View; -import android.view.Window; -import android.view.animation.OvershootInterpolator; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.AppCompatTextView; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import fr.free.nrw.commons.CameraPosition; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.auth.csrf.CsrfTokenClient; -import fr.free.nrw.commons.coordinates.CoordinateEditHelper; -import fr.free.nrw.commons.filepicker.Constants; -import fr.free.nrw.commons.kvstore.BasicKvStore; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LocationPermissionsHelper; -import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback; -import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.SystemThemeUtils; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.util.List; -import java.util.Locale; -import javax.inject.Inject; -import javax.inject.Named; -import org.osmdroid.tileprovider.tilesource.TileSourceFactory; -import org.osmdroid.util.GeoPoint; -import org.osmdroid.util.constants.GeoConstants; -import org.osmdroid.views.CustomZoomButtonsController; -import org.osmdroid.views.overlay.Marker; -import org.osmdroid.views.overlay.Overlay; -import org.osmdroid.views.overlay.ScaleDiskOverlay; -import org.osmdroid.views.overlay.TilesOverlay; -import timber.log.Timber; - -/** - * Helps to pick location and return the result with an intent - */ -public class LocationPickerActivity extends BaseActivity implements - LocationPermissionCallback { - /** - * coordinateEditHelper: helps to edit coordinates - */ - @Inject - CoordinateEditHelper coordinateEditHelper; - /** - * media : Media object - */ - private Media media; - /** - * cameraPosition : position of picker - */ - private CameraPosition cameraPosition; - /** - * markerImage : picker image - */ - private ImageView markerImage; - /** - * mapView : OSM Map - */ - private org.osmdroid.views.MapView mapView; - /** - * tvAttribution : credit - */ - private AppCompatTextView tvAttribution; - /** - * activity : activity key - */ - private String activity; - /** - * modifyLocationButton : button for start editing location - */ - Button modifyLocationButton; - /** - * removeLocationButton : button to remove location metadata - */ - Button removeLocationButton; - /** - * showInMapButton : button for showing in map - */ - TextView showInMapButton; - /** - * placeSelectedButton : fab for selecting location - */ - FloatingActionButton placeSelectedButton; - /** - * fabCenterOnLocation: button for center on location; - */ - FloatingActionButton fabCenterOnLocation; - /** - * shadow : imageview of shadow - */ - private ImageView shadow; - /** - * largeToolbarText : textView of shadow - */ - private TextView largeToolbarText; - /** - * smallToolbarText : textView of shadow - */ - private TextView smallToolbarText; - /** - * applicationKvStore : for storing values - */ - @Inject - @Named("default_preferences") - public - JsonKvStore applicationKvStore; - BasicKvStore store; - /** - * isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly - */ - @Inject - SystemThemeUtils systemThemeUtils; - private boolean isDarkTheme; - private boolean moveToCurrentLocation; - - @Inject - LocationServiceManager locationManager; - LocationPermissionsHelper locationPermissionsHelper; - - @Inject - SessionManager sessionManager; - - /** - * Constants - */ - private static final String CAMERA_POS = "cameraPosition"; - private static final String ACTIVITY = "activity"; - - - @SuppressLint("ClickableViewAccessibility") - @Override - protected void onCreate(@Nullable final Bundle savedInstanceState) { - getWindow().requestFeature(Window.FEATURE_ACTION_BAR); - super.onCreate(savedInstanceState); - - isDarkTheme = systemThemeUtils.isDeviceInNightMode(); - moveToCurrentLocation = false; - store = new BasicKvStore(this, "LocationPermissions"); - - getWindow().requestFeature(Window.FEATURE_ACTION_BAR); - final ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.hide(); - } - setContentView(R.layout.activity_location_picker); - - if (savedInstanceState == null) { - cameraPosition = getIntent() - .getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION); - activity = getIntent().getStringExtra(LocationPickerConstants.ACTIVITY_KEY); - media = getIntent().getParcelableExtra(LocationPickerConstants.MEDIA); - }else{ - cameraPosition = savedInstanceState.getParcelable(CAMERA_POS); - activity = savedInstanceState.getString(ACTIVITY); - media = savedInstanceState.getParcelable("sMedia"); - } - bindViews(); - addBackButtonListener(); - addPlaceSelectedButton(); - addCredits(); - getToolbarUI(); - addCenterOnGPSButton(); - - org.osmdroid.config.Configuration.getInstance().load(getApplicationContext(), - PreferenceManager.getDefaultSharedPreferences(getApplicationContext())); - - mapView.setTileSource(TileSourceFactory.WIKIMEDIA); - mapView.setTilesScaledToDpi(true); - mapView.setMultiTouchControls(true); - - org.osmdroid.config.Configuration.getInstance().getAdditionalHttpRequestProperties().put( - "Referer", "http://maps.wikimedia.org/" - ); - mapView.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER); - mapView.getController().setZoom(ZOOM_LEVEL); - mapView.setOnTouchListener((v, event) -> { - if (event.getAction() == MotionEvent.ACTION_MOVE) { - if (markerImage.getTranslationY() == 0) { - markerImage.animate().translationY(-75) - .setInterpolator(new OvershootInterpolator()).setDuration(250).start(); - } - } else if (event.getAction() == MotionEvent.ACTION_UP) { - markerImage.animate().translationY(0) - .setInterpolator(new OvershootInterpolator()).setDuration(250).start(); - } - return false; - }); - - if ("UploadActivity".equals(activity)) { - placeSelectedButton.setVisibility(View.GONE); - modifyLocationButton.setVisibility(View.VISIBLE); - removeLocationButton.setVisibility(View.VISIBLE); - showInMapButton.setVisibility(View.VISIBLE); - largeToolbarText.setText(getResources().getString(R.string.image_location)); - smallToolbarText.setText(getResources(). - getString(R.string.check_whether_location_is_correct)); - fabCenterOnLocation.setVisibility(View.GONE); - markerImage.setVisibility(View.GONE); - shadow.setVisibility(View.GONE); - assert cameraPosition != null; - showSelectedLocationMarker(new GeoPoint(cameraPosition.getLatitude(), - cameraPosition.getLongitude())); - } - setupMapView(); - } - - /** - * Moves the center of the map to the specified coordinates - * - */ - private void moveMapTo(double latitude, double longitude){ - if(mapView != null && mapView.getController() != null){ - GeoPoint point = new GeoPoint(latitude, longitude); - - mapView.getController().setCenter(point); - mapView.getController().animateTo(point); - } - } - - /** - * Moves the center of the map to the specified coordinates - * @param point The GeoPoint object which contains the coordinates to move to - */ - private void moveMapTo(GeoPoint point){ - if(point != null){ - moveMapTo(point.getLatitude(), point.getLongitude()); - } - } - - /** - * For showing credits - */ - private void addCredits() { - tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution))); - tvAttribution.setMovementMethod(LinkMovementMethod.getInstance()); - } - - /** - * For setting up Dark Theme - */ - private void darkThemeSetup() { - if (isDarkTheme) { - shadow.setColorFilter(Color.argb(255, 255, 255, 255)); - mapView.getOverlayManager().getTilesOverlay() - .setColorFilter(TilesOverlay.INVERT_COLORS); - } - } - - /** - * Clicking back button destroy locationPickerActivity - */ - private void addBackButtonListener() { - final ImageView backButton = findViewById(R.id.maplibre_place_picker_toolbar_back_button); - backButton.setOnClickListener(v -> { - finish(); - }); - - } - - /** - * Binds mapView and location picker icon - */ - private void bindViews() { - mapView = findViewById(R.id.map_view); - markerImage = findViewById(R.id.location_picker_image_view_marker); - tvAttribution = findViewById(R.id.tv_attribution); - modifyLocationButton = findViewById(R.id.modify_location); - removeLocationButton = findViewById(R.id.remove_location); - showInMapButton = findViewById(R.id.show_in_map); - showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase( - Locale.ROOT)); - shadow = findViewById(R.id.location_picker_image_view_shadow); - } - - /** - * Gets toolbar color - */ - private void getToolbarUI() { - final ConstraintLayout toolbar = findViewById(R.id.location_picker_toolbar); - largeToolbarText = findViewById(R.id.location_picker_toolbar_primary_text_view); - smallToolbarText = findViewById(R.id.location_picker_toolbar_secondary_text_view); - toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor)); - } - - private void setupMapView() { - requestLocationPermissions(); - - //If location metadata is available, move map to that location. - if(activity.equals("UploadActivity") || activity.equals("MediaActivity")){ - moveMapToMediaLocation(); - } else { - //If location metadata is not available, move map to device GPS location. - moveMapToGPSLocation(); - } - - modifyLocationButton.setOnClickListener(v -> onClickModifyLocation()); - removeLocationButton.setOnClickListener(v -> onClickRemoveLocation()); - showInMapButton.setOnClickListener(v -> showInMapApp()); - darkThemeSetup(); - } - - /** - * Handles onclick event of modifyLocationButton - */ - private void onClickModifyLocation() { - placeSelectedButton.setVisibility(View.VISIBLE); - modifyLocationButton.setVisibility(View.GONE); - removeLocationButton.setVisibility(View.GONE); - showInMapButton.setVisibility(View.GONE); - markerImage.setVisibility(View.VISIBLE); - shadow.setVisibility(View.VISIBLE); - largeToolbarText.setText(getResources().getString(R.string.choose_a_location)); - smallToolbarText.setText(getResources().getString(R.string.pan_and_zoom_to_adjust)); - fabCenterOnLocation.setVisibility(View.VISIBLE); - removeSelectedLocationMarker(); - moveMapToMediaLocation(); - } - - /** - * Handles onclick event of removeLocationButton - */ - private void onClickRemoveLocation() { - DialogUtil.showAlertDialog(this, - getString(R.string.remove_location_warning_title), - getString(R.string.remove_location_warning_desc), - getString(R.string.continue_message), - getString(R.string.cancel), () -> removeLocationFromImage(), null); - } - - /** - * Method to remove the location from the picture - */ - private void removeLocationFromImage() { - if (media != null) { - getCompositeDisposable().add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext() - , media, "0.0", "0.0", "0.0f") - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(s -> { - Timber.d("Coordinates are removed from the image"); - })); - } - final Intent returningIntent = new Intent(); - setResult(AppCompatActivity.RESULT_OK, returningIntent); - finish(); - } - - /** - * Show the location in map app. Map will center on the location metadata, if available. - * If there is no location metadata, the map will center on the commons app map center. - */ - private void showInMapApp() { - fr.free.nrw.commons.location.LatLng position = null; - - if(activity.equals("UploadActivity") && cameraPosition != null){ - //location metadata is available - position = new fr.free.nrw.commons.location.LatLng(cameraPosition.getLatitude(), - cameraPosition.getLongitude(), 0.0f); - } else if(mapView != null){ - //location metadata is not available - position = new fr.free.nrw.commons.location.LatLng(mapView.getMapCenter().getLatitude(), - mapView.getMapCenter().getLongitude(), 0.0f); - } - - if(position != null){ - Utils.handleGeoCoordinates(this, position); - } - } - - /** - * Moves the center of the map to the media's location, if that data - * is available. - */ - private void moveMapToMediaLocation() { - if (cameraPosition != null) { - - GeoPoint point = new GeoPoint(cameraPosition.getLatitude(), - cameraPosition.getLongitude()); - - moveMapTo(point); - } - } - - /** - * Moves the center of the map to the device's GPS location, if that data is available. - */ - private void moveMapToGPSLocation(){ - if(locationManager != null){ - fr.free.nrw.commons.location.LatLng location = locationManager.getLastLocation(); - - if(location != null){ - GeoPoint point = new GeoPoint(location.getLatitude(), location.getLongitude()); - - moveMapTo(point); - } - } - } - - /** - * Select the preferable location - */ - private void addPlaceSelectedButton() { - placeSelectedButton = findViewById(R.id.location_chosen_button); - placeSelectedButton.setOnClickListener(view -> placeSelected()); - } - - /** - * Return the intent with required data - */ - void placeSelected() { - if (activity.equals("NoLocationUploadActivity")) { - applicationKvStore.putString(LAST_LOCATION, - mapView.getMapCenter().getLatitude() - + "," - + mapView.getMapCenter().getLongitude()); - applicationKvStore.putString(LAST_ZOOM, mapView.getZoomLevel() + ""); - } - - if (media == null) { - final Intent returningIntent = new Intent(); - returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, - new CameraPosition(mapView.getMapCenter().getLatitude(), - mapView.getMapCenter().getLongitude(), 14.0)); - setResult(AppCompatActivity.RESULT_OK, returningIntent); - } else { - updateCoordinates(String.valueOf(mapView.getMapCenter().getLatitude()), - String.valueOf(mapView.getMapCenter().getLongitude()), - String.valueOf(0.0f)); - } - - finish(); - } - - /** - * Fetched coordinates are replaced with existing coordinates by a POST API call. - * @param Latitude to be added - * @param Longitude to be added - * @param Accuracy to be added - */ - public void updateCoordinates(final String Latitude, final String Longitude, - final String Accuracy) { - if (media == null) { - return; - } - - try { - getCompositeDisposable().add( - coordinateEditHelper.makeCoordinatesEdit(getApplicationContext(), media, - Latitude, Longitude, Accuracy) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(s -> { - Timber.d("Coordinates are added."); - })); - } catch (Exception e) { - if (e.getLocalizedMessage().equals(CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE)) { - final String username = sessionManager.getUserName(); - final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( - this, - getString(R.string.invalid_login_message), - username - ); - - CommonsApplication.getInstance().clearApplicationData( - this, logoutListener); - } - } - } - - /** - * Center the camera on the last saved location - */ - private void addCenterOnGPSButton() { - fabCenterOnLocation = findViewById(R.id.center_on_gps); - fabCenterOnLocation.setOnClickListener(view -> { - moveToCurrentLocation = true; - requestLocationPermissions(); - }); - } - - /** - * Adds selected location marker on the map - */ - private void showSelectedLocationMarker(GeoPoint point) { - Drawable icon = ContextCompat.getDrawable(this, R.drawable.map_default_map_marker); - Marker marker = new Marker(mapView); - marker.setPosition(point); - marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); - marker.setIcon(icon); - marker.setInfoWindow(null); - mapView.getOverlays().add(marker); - mapView.invalidate(); - } - - /** - * Removes selected location marker from the map - */ - private void removeSelectedLocationMarker() { - List overlays = mapView.getOverlays(); - for (int i = 0; i < overlays.size(); i++) { - if (overlays.get(i) instanceof Marker) { - Marker item = (Marker) overlays.get(i); - if (cameraPosition.getLatitude() == item.getPosition().getLatitude() - && cameraPosition.getLongitude() == item.getPosition().getLongitude()) { - mapView.getOverlays().remove(i); - mapView.invalidate(); - break; - } - } - } - } - - /** - * Center the map at user's current location - */ - private void requestLocationPermissions() { - locationPermissionsHelper = new LocationPermissionsHelper( - this, locationManager, this); - locationPermissionsHelper.requestForLocationAccess(R.string.location_permission_title, - R.string.upload_map_location_access); - } - - @Override - public void onRequestPermissionsResult(final int requestCode, - @NonNull final String[] permissions, - @NonNull final int[] grantResults) { - if (requestCode == Constants.RequestCodes.LOCATION - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - onLocationPermissionGranted(); - } else { - onLocationPermissionDenied(getString(R.string.upload_map_location_access)); - } - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - - @Override - protected void onResume() { - super.onResume(); - mapView.onResume(); - } - - @Override - protected void onPause() { - super.onPause(); - mapView.onPause(); - } - - @Override - public void onLocationPermissionDenied(String toastMessage) { - if (!ActivityCompat.shouldShowRequestPermissionRationale(this, - permission.ACCESS_FINE_LOCATION)) { - if (!locationPermissionsHelper.checkLocationPermission(this)) { - if (store.getBoolean("isPermissionDenied", false)) { - // means user has denied location permission twice or checked the "Don't show again" - locationPermissionsHelper.showAppSettingsDialog(this, - R.string.upload_map_location_access); - } else { - Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show(); - } - store.putBoolean("isPermissionDenied", true); - } - } else { - Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show(); - } - } - - @Override - public void onLocationPermissionGranted() { - if (moveToCurrentLocation || !(activity.equals("MediaActivity"))) { - if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { - locationManager.requestLocationUpdatesFromProvider( - LocationManager.NETWORK_PROVIDER); - locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); - addMarkerAtGPSLocation(); - } else { - addMarkerAtGPSLocation(); - locationPermissionsHelper.showLocationOffDialog(this, - R.string.ask_to_turn_location_on_text); - } - } - } - - /** - * Adds a marker to the map at the most recent GPS location - * (which may be the current GPS location). - */ - private void addMarkerAtGPSLocation() { - fr.free.nrw.commons.location.LatLng currLocation = locationManager.getLastLocation(); - if (currLocation != null) { - GeoPoint currLocationGeopoint = new GeoPoint(currLocation.getLatitude(), - currLocation.getLongitude()); - addLocationMarker(currLocationGeopoint); - markerImage.setTranslationY(0); - } - } - - private void addLocationMarker(GeoPoint geoPoint) { - if (moveToCurrentLocation) { - mapView.getOverlays().clear(); - } - ScaleDiskOverlay diskOverlay = - new ScaleDiskOverlay(this, - geoPoint, 2000, GeoConstants.UnitOfMeasure.foot); - Paint circlePaint = new Paint(); - circlePaint.setColor(Color.rgb(128, 128, 128)); - circlePaint.setStyle(Paint.Style.STROKE); - circlePaint.setStrokeWidth(2f); - diskOverlay.setCirclePaint2(circlePaint); - Paint diskPaint = new Paint(); - diskPaint.setColor(Color.argb(40, 128, 128, 128)); - diskPaint.setStyle(Paint.Style.FILL_AND_STROKE); - diskOverlay.setCirclePaint1(diskPaint); - diskOverlay.setDisplaySizeMin(900); - diskOverlay.setDisplaySizeMax(1700); - mapView.getOverlays().add(diskOverlay); - org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker( - mapView); - startMarker.setPosition(geoPoint); - startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER, - org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM); - startMarker.setIcon( - ContextCompat.getDrawable(this, R.drawable.current_location_marker)); - startMarker.setTitle("Your Location"); - startMarker.setTextLabelFontSize(24); - mapView.getOverlays().add(startMarker); - } - - /** - * Saves the state of the activity - * @param outState Bundle - */ - @Override - public void onSaveInstanceState(@NonNull final Bundle outState) { - super.onSaveInstanceState(outState); - if(cameraPosition!=null){ - outState.putParcelable(CAMERA_POS, cameraPosition); - } - if(activity!=null){ - outState.putString(ACTIVITY, activity); - } - - if(media!=null){ - outState.putParcelable("sMedia", media); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt new file mode 100644 index 000000000..6508c4f25 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt @@ -0,0 +1,678 @@ +package fr.free.nrw.commons.LocationPicker + +import android.Manifest.permission +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Color +import android.graphics.Paint +import android.location.LocationManager +import android.os.Bundle +import android.preference.PreferenceManager +import android.text.Html +import android.text.method.LinkMovementMethod +import android.view.MotionEvent +import android.view.View +import android.view.Window +import android.view.animation.OvershootInterpolator +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.widget.AppCompatTextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.google.android.material.floatingactionbutton.FloatingActionButton +import fr.free.nrw.commons.CameraPosition +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient +import fr.free.nrw.commons.coordinates.CoordinateEditHelper +import fr.free.nrw.commons.filepicker.Constants +import fr.free.nrw.commons.kvstore.BasicKvStore +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LocationPermissionsHelper +import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback +import fr.free.nrw.commons.location.LocationServiceManager +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM +import fr.free.nrw.commons.utils.DialogUtil +import fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.util.constants.GeoConstants +import org.osmdroid.views.CustomZoomButtonsController +import org.osmdroid.views.overlay.Marker +import org.osmdroid.views.overlay.ScaleDiskOverlay +import org.osmdroid.views.overlay.TilesOverlay +import timber.log.Timber +import java.util.Locale +import javax.inject.Inject +import javax.inject.Named + + +/** + * Helps to pick location and return the result with an intent + */ +class LocationPickerActivity : BaseActivity(), LocationPermissionCallback { + /** + * coordinateEditHelper: helps to edit coordinates + */ + @Inject + lateinit var coordinateEditHelper: CoordinateEditHelper + + /** + * media : Media object + */ + private var media: Media? = null + + /** + * cameraPosition : position of picker + */ + private var cameraPosition: CameraPosition? = null + + /** + * markerImage : picker image + */ + private lateinit var markerImage: ImageView + + /** + * mapView : OSM Map + */ + private var mapView: org.osmdroid.views.MapView? = null + + /** + * tvAttribution : credit + */ + private lateinit var tvAttribution: AppCompatTextView + + /** + * activity : activity key + */ + private var activity: String? = null + + /** + * modifyLocationButton : button for start editing location + */ + private lateinit var modifyLocationButton: Button + + /** + * removeLocationButton : button to remove location metadata + */ + private lateinit var removeLocationButton: Button + + /** + * showInMapButton : button for showing in map + */ + private lateinit var showInMapButton: TextView + + /** + * placeSelectedButton : fab for selecting location + */ + private lateinit var placeSelectedButton: FloatingActionButton + + /** + * fabCenterOnLocation: button for center on location; + */ + private lateinit var fabCenterOnLocation: FloatingActionButton + + /** + * shadow : imageview of shadow + */ + private lateinit var shadow: ImageView + + /** + * largeToolbarText : textView of shadow + */ + private lateinit var largeToolbarText: TextView + + /** + * smallToolbarText : textView of shadow + */ + private lateinit var smallToolbarText: TextView + + /** + * applicationKvStore : for storing values + */ + @Inject + @field: Named("default_preferences") + lateinit var applicationKvStore: JsonKvStore + private lateinit var store: BasicKvStore + + /** + * isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly + */ + private var isDarkTheme: Boolean = false + private var moveToCurrentLocation: Boolean = false + + @Inject + lateinit var locationManager: LocationServiceManager + private lateinit var locationPermissionsHelper: LocationPermissionsHelper + + @Inject + lateinit var sessionManager: SessionManager + + /** + * Constants + */ + companion object { + private const val CAMERA_POS = "cameraPosition" + private const val ACTIVITY = "activity" + } + + @SuppressLint("ClickableViewAccessibility") + override fun onCreate(savedInstanceState: Bundle?) { + requestWindowFeature(Window.FEATURE_ACTION_BAR) + super.onCreate(savedInstanceState) + + isDarkTheme = systemThemeUtils.isDeviceInNightMode() + moveToCurrentLocation = false + store = BasicKvStore(this, "LocationPermissions") + + requestWindowFeature(Window.FEATURE_ACTION_BAR) + supportActionBar?.hide() + setContentView(R.layout.activity_location_picker) + + if (savedInstanceState == null) { + cameraPosition = intent.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION) + activity = intent.getStringExtra(LocationPickerConstants.ACTIVITY_KEY) + media = intent.getParcelableExtra(LocationPickerConstants.MEDIA) + } else { + cameraPosition = savedInstanceState.getParcelable(CAMERA_POS) + activity = savedInstanceState.getString(ACTIVITY) + media = savedInstanceState.getParcelable("sMedia") + } + + bindViews() + addBackButtonListener() + addPlaceSelectedButton() + addCredits() + getToolbarUI() + addCenterOnGPSButton() + + org.osmdroid.config.Configuration.getInstance() + .load( + applicationContext, PreferenceManager.getDefaultSharedPreferences( + applicationContext + ) + ) + + mapView?.setTileSource(TileSourceFactory.WIKIMEDIA) + mapView?.setTilesScaledToDpi(true) + mapView?.setMultiTouchControls(true) + + org.osmdroid.config.Configuration.getInstance().additionalHttpRequestProperties["Referer"] = + "http://maps.wikimedia.org/" + mapView?.zoomController?.setVisibility(CustomZoomButtonsController.Visibility.NEVER) + mapView?.controller?.setZoom(ZOOM_LEVEL.toDouble()) + mapView?.setOnTouchListener { _, event -> + when (event.action) { + MotionEvent.ACTION_MOVE -> { + if (markerImage.translationY == 0f) { + markerImage.animate().translationY(-75f) + .setInterpolator(OvershootInterpolator()).duration = 250 + } + } + MotionEvent.ACTION_UP -> { + markerImage.animate().translationY(0f) + .setInterpolator(OvershootInterpolator()).duration = 250 + } + } + false + } + + if (activity == "UploadActivity") { + placeSelectedButton.visibility = View.GONE + modifyLocationButton.visibility = View.VISIBLE + removeLocationButton.visibility = View.VISIBLE + showInMapButton.visibility = View.VISIBLE + largeToolbarText.text = getString(R.string.image_location) + smallToolbarText.text = getString(R.string.check_whether_location_is_correct) + fabCenterOnLocation.visibility = View.GONE + markerImage.visibility = View.GONE + shadow.visibility = View.GONE + cameraPosition?.let { + showSelectedLocationMarker(GeoPoint(it.latitude, it.longitude)) + } + } + setupMapView() + } + + /** + * Moves the center of the map to the specified coordinates + */ + private fun moveMapTo(latitude: Double, longitude: Double) { + mapView?.controller?.let { + val point = GeoPoint(latitude, longitude) + it.setCenter(point) + it.animateTo(point) + } + } + + /** + * Moves the center of the map to the specified coordinates + * @param point The GeoPoint object which contains the coordinates to move to + */ + private fun moveMapTo(point: GeoPoint?) { + point?.let { + moveMapTo(it.latitude, it.longitude) + } + } + + /** + * For showing credits + */ + private fun addCredits() { + tvAttribution.text = Html.fromHtml(getString(R.string.map_attribution)) + tvAttribution.movementMethod = LinkMovementMethod.getInstance() + } + + /** + * For setting up Dark Theme + */ + private fun darkThemeSetup() { + if (isDarkTheme) { + shadow.setColorFilter(Color.argb(255, 255, 255, 255)) + mapView?.overlayManager?.tilesOverlay?.setColorFilter(TilesOverlay.INVERT_COLORS) + } + } + + /** + * Clicking back button destroy locationPickerActivity + */ + private fun addBackButtonListener() { + val backButton = findViewById(R.id.maplibre_place_picker_toolbar_back_button) + backButton.setOnClickListener { + finish() + } + } + + /** + * Binds mapView and location picker icon + */ + private fun bindViews() { + mapView = findViewById(R.id.map_view) + markerImage = findViewById(R.id.location_picker_image_view_marker) + tvAttribution = findViewById(R.id.tv_attribution) + modifyLocationButton = findViewById(R.id.modify_location) + removeLocationButton = findViewById(R.id.remove_location) + showInMapButton = findViewById(R.id.show_in_map) + showInMapButton.text = getString(R.string.show_in_map_app).uppercase(Locale.ROOT) + shadow = findViewById(R.id.location_picker_image_view_shadow) + } + + /** + * Gets toolbar color + */ + private fun getToolbarUI() { + val toolbar: ConstraintLayout = findViewById(R.id.location_picker_toolbar) + largeToolbarText = findViewById(R.id.location_picker_toolbar_primary_text_view) + smallToolbarText = findViewById(R.id.location_picker_toolbar_secondary_text_view) + toolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.primaryColor)) + } + + private fun setupMapView() { + requestLocationPermissions() + + //If location metadata is available, move map to that location. + if (activity == "UploadActivity" || activity == "MediaActivity") { + moveMapToMediaLocation() + } else { + //If location metadata is not available, move map to device GPS location. + moveMapToGPSLocation() + } + + modifyLocationButton.setOnClickListener { onClickModifyLocation() } + removeLocationButton.setOnClickListener { onClickRemoveLocation() } + showInMapButton.setOnClickListener { showInMapApp() } + darkThemeSetup() + } + + /** + * Handles onClick event of modifyLocationButton + */ + private fun onClickModifyLocation() { + placeSelectedButton.visibility = View.VISIBLE + modifyLocationButton.visibility = View.GONE + removeLocationButton.visibility = View.GONE + showInMapButton.visibility = View.GONE + markerImage.visibility = View.VISIBLE + shadow.visibility = View.VISIBLE + largeToolbarText.text = getString(R.string.choose_a_location) + smallToolbarText.text = getString(R.string.pan_and_zoom_to_adjust) + fabCenterOnLocation.visibility = View.VISIBLE + removeSelectedLocationMarker() + moveMapToMediaLocation() + } + + /** + * Handles onClick event of removeLocationButton + */ + private fun onClickRemoveLocation() { + DialogUtil.showAlertDialog( + this, + getString(R.string.remove_location_warning_title), + getString(R.string.remove_location_warning_desc), + getString(R.string.continue_message), + getString(R.string.cancel), + { removeLocationFromImage() }, + null + ) + } + + /** + * Removes location metadata from the image + */ + private fun removeLocationFromImage() { + media?.let { + compositeDisposable.add( + coordinateEditHelper.makeCoordinatesEdit( + applicationContext, it, "0.0", "0.0", "0.0f" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { _ -> + Timber.d("Coordinates removed from the image") + } + ) + } + setResult(RESULT_OK, Intent()) + finish() + } + + /** + * Show location in map app + */ + private fun showInMapApp() { + val position = when { + //location metadata is available + activity == "UploadActivity" && cameraPosition != null -> { + fr.free.nrw.commons.location.LatLng(cameraPosition!!.latitude, cameraPosition!!.longitude, 0.0f) + } + //location metadata is not available + mapView != null -> { + fr.free.nrw.commons.location.LatLng( + mapView?.mapCenter?.latitude!!, + mapView?.mapCenter?.longitude!!, + 0.0f + ) + } + else -> null + } + + position?.let { Utils.handleGeoCoordinates(this, it) } + } + + /** + * Moves map to media's location + */ + private fun moveMapToMediaLocation() { + cameraPosition?.let { + moveMapTo(GeoPoint(it.latitude, it.longitude)) + } + } + + /** + * Moves map to GPS location + */ + private fun moveMapToGPSLocation() { + locationManager.lastLocation?.let { + moveMapTo(GeoPoint(it.latitude, it.longitude)) + } + } + + /** + * Adds "Place Selected" button + */ + private fun addPlaceSelectedButton() { + placeSelectedButton = findViewById(R.id.location_chosen_button) + placeSelectedButton.setOnClickListener { placeSelected() } + } + + /** + * Handles "Place Selected" action + */ + private fun placeSelected() { + if (activity == "NoLocationUploadActivity") { + applicationKvStore.putString( + LAST_LOCATION, + "${mapView?.mapCenter?.latitude},${mapView?.mapCenter?.longitude}" + ) + applicationKvStore.putString(LAST_ZOOM, mapView?.zoomLevel?.toString()!!) + } + + if (media == null) { + val intent = Intent().apply { + putExtra( + LocationPickerConstants.MAP_CAMERA_POSITION, + CameraPosition(mapView?.mapCenter?.latitude!!, mapView?.mapCenter?.longitude!!, 14.0) + ) + } + setResult(RESULT_OK, intent) + } else { + updateCoordinates( + mapView?.mapCenter?.latitude.toString(), + mapView?.mapCenter?.longitude.toString(), + "0.0f" + ) + } + + finish() + } + + /** + * Updates image with new coordinates + */ + fun updateCoordinates(latitude: String, longitude: String, accuracy: String) { + media?.let { + try { + compositeDisposable.add( + coordinateEditHelper.makeCoordinatesEdit( + applicationContext, + it, + latitude, + longitude, + accuracy + ).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { _ -> + Timber.d("Coordinates updated") + } + ) + } catch (e: Exception) { + if (e.localizedMessage == CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE) { + val username = sessionManager.userName + CommonsApplication.BaseLogoutListener( + this, + getString(R.string.invalid_login_message) + , username + ).let { + CommonsApplication.instance.clearApplicationData(this, it) + } + } else { } + } + } + } + + /** + * Adds a button to center the map at user's location + */ + private fun addCenterOnGPSButton() { + fabCenterOnLocation = findViewById(R.id.center_on_gps) + fabCenterOnLocation.setOnClickListener { + moveToCurrentLocation = true + requestLocationPermissions() + } + } + + /** + * Shows a selected location marker + */ + private fun showSelectedLocationMarker(point: GeoPoint) { + val icon = ContextCompat.getDrawable(this, R.drawable.map_default_map_marker) + Marker(mapView).apply { + position = point + setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) + setIcon(icon) + infoWindow = null + mapView?.overlays?.add(this) + } + mapView?.invalidate() + } + + /** + * Removes selected location marker + */ + private fun removeSelectedLocationMarker() { + val overlays = mapView?.overlays + overlays?.filterIsInstance()?.firstOrNull { + it.position.latitude == + cameraPosition?.latitude && it.position.longitude == cameraPosition?.longitude + }?.let { + overlays.remove(it) + mapView?.invalidate() + } + } + + /** + * Centers map at user's location + */ + private fun requestLocationPermissions() { + locationPermissionsHelper = LocationPermissionsHelper(this, locationManager, this) + locationPermissionsHelper.requestForLocationAccess( + R.string.location_permission_title, + R.string.upload_map_location_access + ) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (requestCode == Constants.RequestCodes.LOCATION && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + onLocationPermissionGranted() + } else { + onLocationPermissionDenied(getString(R.string.upload_map_location_access)) + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + + override fun onResume() { + super.onResume() + mapView?.onResume() + } + + override fun onPause() { + super.onPause() + mapView?.onPause() + } + + override fun onLocationPermissionDenied(toastMessage: String) { + val isDeniedBefore = store.getBoolean("isPermissionDenied", false) + val showRationale = ActivityCompat.shouldShowRequestPermissionRationale(this, permission.ACCESS_FINE_LOCATION) + + if (!showRationale) { + if (!locationPermissionsHelper.checkLocationPermission(this)) { + if (isDeniedBefore) { + locationPermissionsHelper.showAppSettingsDialog(this, R.string.upload_map_location_access) + } else { + Toast.makeText(this, toastMessage, Toast.LENGTH_LONG).show() + } + store.putBoolean("isPermissionDenied", true) + } + } else { + Toast.makeText(this, toastMessage, Toast.LENGTH_LONG).show() + } + } + + override fun onLocationPermissionGranted() { + if (moveToCurrentLocation || activity != "MediaActivity") { + if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn) { + locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) + locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER) + addMarkerAtGPSLocation() + } else { + addMarkerAtGPSLocation() + locationPermissionsHelper.showLocationOffDialog(this, R.string.ask_to_turn_location_on_text) + } + } + } + + /** + * Adds a marker at the user's GPS location + */ + private fun addMarkerAtGPSLocation() { + locationManager.lastLocation?.let { + addLocationMarker(GeoPoint(it.latitude, it.longitude)) + markerImage.translationY = 0f + } + } + + private fun addLocationMarker(geoPoint: GeoPoint) { + if (moveToCurrentLocation) { + mapView?.overlays?.clear() + } + + val diskOverlay = ScaleDiskOverlay( + this, + geoPoint, + 2000, + GeoConstants.UnitOfMeasure.foot + ) + + val circlePaint = Paint().apply { + color = Color.rgb(128, 128, 128) + style = Paint.Style.STROKE + strokeWidth = 2f + } + diskOverlay.setCirclePaint2(circlePaint) + + val diskPaint = Paint().apply { + color = Color.argb(40, 128, 128, 128) + style = Paint.Style.FILL_AND_STROKE + } + diskOverlay.setCirclePaint1(diskPaint) + + diskOverlay.setDisplaySizeMin(900) + diskOverlay.setDisplaySizeMax(1700) + + mapView?.overlays?.add(diskOverlay) + + val startMarker = Marker(mapView).apply { + position = geoPoint + setAnchor( + Marker.ANCHOR_CENTER, + Marker.ANCHOR_BOTTOM + ) + icon = ContextCompat.getDrawable(this@LocationPickerActivity, R.drawable.current_location_marker) + title = "Your Location" + textLabelFontSize = 24 + } + + mapView?.overlays?.add(startMarker) + } + + /** + * Saves the state of the activity + * @param outState Bundle + */ + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + cameraPosition?.let { + outState.putParcelable(CAMERA_POS, it) + } + + activity?.let { + outState.putString(ACTIVITY, it) + } + + media?.let { + outState.putParcelable("sMedia", it) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java deleted file mode 100644 index 060a15c88..000000000 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java +++ /dev/null @@ -1,20 +0,0 @@ -package fr.free.nrw.commons.LocationPicker; - -/** - * Constants need for location picking - */ -public final class LocationPickerConstants { - - public static final String ACTIVITY_KEY - = "location.picker.activity"; - - public static final String MAP_CAMERA_POSITION - = "location.picker.cameraPosition"; - - public static final String MEDIA - = "location.picker.media"; - - - private LocationPickerConstants() { - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.kt b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.kt new file mode 100644 index 000000000..a1c9d989a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.kt @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.LocationPicker + +/** + * Constants need for location picking + */ +object LocationPickerConstants { + + const val ACTIVITY_KEY = "location.picker.activity" + + const val MAP_CAMERA_POSITION = "location.picker.cameraPosition" + + const val MEDIA = "location.picker.media" +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java deleted file mode 100644 index 57bb238d2..000000000 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java +++ /dev/null @@ -1,63 +0,0 @@ -package fr.free.nrw.commons.LocationPicker; - -import android.app.Application; -import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.MutableLiveData; -import fr.free.nrw.commons.CameraPosition; -import org.jetbrains.annotations.NotNull; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; -import timber.log.Timber; - -/** - * Observes live camera position data - */ -public class LocationPickerViewModel extends AndroidViewModel implements Callback { - - /** - * Wrapping CameraPosition with MutableLiveData - */ - private final MutableLiveData result = new MutableLiveData<>(); - - /** - * Constructor for this class - * - * @param application Application - */ - public LocationPickerViewModel(@NonNull final Application application) { - super(application); - } - - /** - * Responses on camera position changing - * - * @param call Call - * @param response Response - */ - @Override - public void onResponse(final @NotNull Call call, - final Response response) { - if (response.body() == null) { - result.setValue(null); - return; - } - result.setValue(response.body()); - } - - @Override - public void onFailure(final @NotNull Call call, final @NotNull Throwable t) { - Timber.e(t); - } - - /** - * Gets live CameraPosition - * - * @return MutableLiveData - */ - public MutableLiveData getResult() { - return result; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.kt b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.kt new file mode 100644 index 000000000..b0b2ce6de --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.kt @@ -0,0 +1,44 @@ +package fr.free.nrw.commons.LocationPicker + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import fr.free.nrw.commons.CameraPosition +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import timber.log.Timber + +/** + * Observes live camera position data + */ +class LocationPickerViewModel( + application: Application +): AndroidViewModel(application), Callback { + + /** + * Wrapping CameraPosition with MutableLiveData + */ + val result = MutableLiveData() + + /** + * Responses on camera position changing + * + * @param call Call + * @param response Response + */ + override fun onResponse( + call: Call, + response: Response + ) { + if(response.body() == null) { + result.value = null + return + } + result.value = response.body() + } + + override fun onFailure(call: Call, t: Throwable) { + Timber.e(t) + } +} \ No newline at end of file From 8265cc6306c771ba3dd36abee37684c710700821 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Tue, 3 Dec 2024 11:57:11 +0530 Subject: [PATCH 51/74] Migrate location and language module from Java to Kotlin (#5988) * Rename .java to .kt * Migrated location and language module from Java to Kotlin * Changed lastLocation visibility --- .../LocationPicker/LocationPickerActivity.kt | 6 +- .../language/AppLanguageLookUpTable.java | 141 --------- .../language/AppLanguageLookUpTable.kt | 135 +++++++++ .../fr/free/nrw/commons/location/LatLng.java | 198 ------------- .../fr/free/nrw/commons/location/LatLng.kt | 150 ++++++++++ .../location/LocationPermissionsHelper.java | 186 ------------ .../location/LocationPermissionsHelper.kt | 200 +++++++++++++ .../location/LocationServiceManager.java | 274 ------------------ .../location/LocationServiceManager.kt | 255 ++++++++++++++++ .../location/LocationUpdateListener.java | 7 - .../location/LocationUpdateListener.kt | 12 + .../nrw/commons/upload/LanguagesAdapter.kt | 16 +- .../kotlin/fr/free/nrw/commons/LatLngTests.kt | 2 +- .../media/MediaDetailFragmentUnitTests.kt | 8 +- .../commons/upload/LanguagesAdapterTest.kt | 10 +- 15 files changed, 773 insertions(+), 827 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.java create mode 100644 app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/location/LatLng.java create mode 100644 app/src/main/java/fr/free/nrw/commons/location/LatLng.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java create mode 100644 app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java create mode 100644 app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.kt diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt index 6508c4f25..1a5ec0a34 100644 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt @@ -423,7 +423,7 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback { * Moves map to GPS location */ private fun moveMapToGPSLocation() { - locationManager.lastLocation?.let { + locationManager.getLastLocation()?.let { moveMapTo(GeoPoint(it.latitude, it.longitude)) } } @@ -591,7 +591,7 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback { override fun onLocationPermissionGranted() { if (moveToCurrentLocation || activity != "MediaActivity") { - if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn) { + if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER) addMarkerAtGPSLocation() @@ -606,7 +606,7 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback { * Adds a marker at the user's GPS location */ private fun addMarkerAtGPSLocation() { - locationManager.lastLocation?.let { + locationManager.getLastLocation()?.let { addLocationMarker(GeoPoint(it.latitude, it.longitude)) markerImage.translationY = 0f } diff --git a/app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.java b/app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.java deleted file mode 100644 index a0286a7ef..000000000 --- a/app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.java +++ /dev/null @@ -1,141 +0,0 @@ -package fr.free.nrw.commons.language; - -import android.content.Context; -import android.content.res.Resources; -import android.text.TextUtils; - -import androidx.annotation.ArrayRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.R; -import java.lang.ref.SoftReference; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -/** Immutable look up table for all app supported languages. All article languages may not be - * present in this table as it is statically bundled with the app. */ -public class AppLanguageLookUpTable { - public static final String SIMPLIFIED_CHINESE_LANGUAGE_CODE = "zh-hans"; - public static final String TRADITIONAL_CHINESE_LANGUAGE_CODE = "zh-hant"; - public static final String CHINESE_CN_LANGUAGE_CODE = "zh-cn"; - public static final String CHINESE_HK_LANGUAGE_CODE = "zh-hk"; - public static final String CHINESE_MO_LANGUAGE_CODE = "zh-mo"; - public static final String CHINESE_SG_LANGUAGE_CODE = "zh-sg"; - public static final String CHINESE_TW_LANGUAGE_CODE = "zh-tw"; - public static final String CHINESE_YUE_LANGUAGE_CODE = "zh-yue"; - public static final String CHINESE_LANGUAGE_CODE = "zh"; - public static final String NORWEGIAN_LEGACY_LANGUAGE_CODE = "no"; - public static final String NORWEGIAN_BOKMAL_LANGUAGE_CODE = "nb"; - public static final String TEST_LANGUAGE_CODE = "test"; - public static final String FALLBACK_LANGUAGE_CODE = "en"; // Must exist in preference_language_keys. - - @NonNull private final Resources resources; - - // Language codes for all app supported languages in fixed order. The special code representing - // the dynamic system language is null. - @NonNull private SoftReference> codesRef = new SoftReference<>(null); - - // English names for all app supported languages in fixed order. - @NonNull private SoftReference> canonicalNamesRef = new SoftReference<>(null); - - // Native names for all app supported languages in fixed order. - @NonNull private SoftReference> localizedNamesRef = new SoftReference<>(null); - - public AppLanguageLookUpTable(@NonNull Context context) { - resources = context.getResources(); - } - - /** - * @return Nonnull immutable list. The special code representing the dynamic system language is - * null. - */ - @NonNull - public List getCodes() { - List codes = codesRef.get(); - if (codes == null) { - codes = getStringList(R.array.preference_language_keys); - codesRef = new SoftReference<>(codes); - } - return codes; - } - - @Nullable - public String getCanonicalName(@Nullable String code) { - String name = defaultIndex(getCanonicalNames(), indexOfCode(code), null); - if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(code)) { - if (code.equals(Locale.CHINESE.getLanguage())) { - name = Locale.CHINESE.getDisplayName(Locale.ENGLISH); - } else if (code.equals(NORWEGIAN_LEGACY_LANGUAGE_CODE)) { - name = defaultIndex(getCanonicalNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null); - } - } - return name; - } - - @Nullable - public String getLocalizedName(@Nullable String code) { - String name = defaultIndex(getLocalizedNames(), indexOfCode(code), null); - if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(code)) { - if (code.equals(Locale.CHINESE.getLanguage())) { - name = Locale.CHINESE.getDisplayName(Locale.CHINESE); - } else if (code.equals(NORWEGIAN_LEGACY_LANGUAGE_CODE)) { - name = defaultIndex(getLocalizedNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null); - } - } - return name; - } - - public List getCanonicalNames() { - List names = canonicalNamesRef.get(); - if (names == null) { - names = getStringList(R.array.preference_language_canonical_names); - canonicalNamesRef = new SoftReference<>(names); - } - return names; - } - - public List getLocalizedNames() { - List names = localizedNamesRef.get(); - if (names == null) { - names = getStringList(R.array.preference_language_local_names); - localizedNamesRef = new SoftReference<>(names); - } - return names; - } - - public boolean isSupportedCode(@Nullable String code) { - return getCodes().contains(code); - } - - private T defaultIndex(List list, int index, T defaultValue) { - return inBounds(list, index) ? list.get(index) : defaultValue; - } - - /** - * Searches #codes for the specified language code and returns the index for use in - * #canonicalNames and #localizedNames. - * - * @param code The language code to search for. The special code representing the dynamic system - * language is null. - * @return The index of the language code or -1 if the code is not supported. - */ - private int indexOfCode(@Nullable String code) { - return getCodes().indexOf(code); - } - - /** @return Nonnull immutable list. */ - @NonNull - private List getStringList(int id) { - return Arrays.asList(getStringArray(id)); - } - - private boolean inBounds(List list, int index) { - return index >= 0 && index < list.size(); - } - - public String[] getStringArray(@ArrayRes int id) { - return resources.getStringArray(id); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.kt b/app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.kt new file mode 100644 index 000000000..6809fd79c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.kt @@ -0,0 +1,135 @@ +package fr.free.nrw.commons.language + +import android.content.Context +import android.content.res.Resources +import android.text.TextUtils + +import androidx.annotation.ArrayRes +import fr.free.nrw.commons.R +import java.lang.ref.SoftReference +import java.util.Arrays +import java.util.Locale + + +/** Immutable look up table for all app supported languages. All article languages may not be + * present in this table as it is statically bundled with the app. */ +class AppLanguageLookUpTable(context: Context) { + + companion object { + const val SIMPLIFIED_CHINESE_LANGUAGE_CODE = "zh-hans" + const val TRADITIONAL_CHINESE_LANGUAGE_CODE = "zh-hant" + const val CHINESE_CN_LANGUAGE_CODE = "zh-cn" + const val CHINESE_HK_LANGUAGE_CODE = "zh-hk" + const val CHINESE_MO_LANGUAGE_CODE = "zh-mo" + const val CHINESE_SG_LANGUAGE_CODE = "zh-sg" + const val CHINESE_TW_LANGUAGE_CODE = "zh-tw" + const val CHINESE_YUE_LANGUAGE_CODE = "zh-yue" + const val CHINESE_LANGUAGE_CODE = "zh" + const val NORWEGIAN_LEGACY_LANGUAGE_CODE = "no" + const val NORWEGIAN_BOKMAL_LANGUAGE_CODE = "nb" + const val TEST_LANGUAGE_CODE = "test" + const val FALLBACK_LANGUAGE_CODE = "en" // Must exist in preference_language_keys. + } + + private val resources: Resources = context.resources + + // Language codes for all app supported languages in fixed order. The special code representing + // the dynamic system language is null. + private var codesRef = SoftReference>(null) + + // English names for all app supported languages in fixed order. + private var canonicalNamesRef = SoftReference>(null) + + // Native names for all app supported languages in fixed order. + private var localizedNamesRef = SoftReference>(null) + + /** + * @return Nonnull immutable list. The special code representing the dynamic system language is + * null. + */ + fun getCodes(): List { + var codes = codesRef.get() + if (codes == null) { + codes = getStringList(R.array.preference_language_keys) + codesRef = SoftReference(codes) + } + return codes + } + + fun getCanonicalName(code: String?): String? { + var name = defaultIndex(getCanonicalNames(), indexOfCode(code), null) + if (name.isNullOrEmpty() && !code.isNullOrEmpty()) { + name = when (code) { + Locale.CHINESE.language -> Locale.CHINESE.getDisplayName(Locale.ENGLISH) + NORWEGIAN_LEGACY_LANGUAGE_CODE -> + defaultIndex(getCanonicalNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null) + else -> null + } + } + return name + } + + fun getLocalizedName(code: String?): String? { + var name = defaultIndex(getLocalizedNames(), indexOfCode(code), null) + if (name.isNullOrEmpty() && !code.isNullOrEmpty()) { + name = when (code) { + Locale.CHINESE.language -> Locale.CHINESE.getDisplayName(Locale.CHINESE) + NORWEGIAN_LEGACY_LANGUAGE_CODE -> + defaultIndex(getLocalizedNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null) + else -> null + } + } + return name + } + + fun getCanonicalNames(): List { + var names = canonicalNamesRef.get() + if (names == null) { + names = getStringList(R.array.preference_language_canonical_names) + canonicalNamesRef = SoftReference(names) + } + return names + } + + fun getLocalizedNames(): List { + var names = localizedNamesRef.get() + if (names == null) { + names = getStringList(R.array.preference_language_local_names) + localizedNamesRef = SoftReference(names) + } + return names + } + + fun isSupportedCode(code: String?): Boolean { + return getCodes().contains(code) + } + + private fun defaultIndex(list: List, index: Int, defaultValue: T?): T? { + return if (inBounds(list, index)) list[index] else defaultValue + } + + /** + * Searches #codes for the specified language code and returns the index for use in + * #canonicalNames and #localizedNames. + * + * @param code The language code to search for. The special code representing the dynamic system + * language is null. + * @return The index of the language code or -1 if the code is not supported. + */ + private fun indexOfCode(code: String?): Int { + return getCodes().indexOf(code) + } + + /** @return Nonnull immutable list. */ + private fun getStringList(id: Int): List { + return getStringArray(id).toList() + } + + private fun inBounds(list: List<*>, index: Int): Boolean { + return index in list.indices + } + + fun getStringArray(@ArrayRes id: Int): Array { + return resources.getStringArray(id) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LatLng.java b/app/src/main/java/fr/free/nrw/commons/location/LatLng.java deleted file mode 100644 index 4970fc54f..000000000 --- a/app/src/main/java/fr/free/nrw/commons/location/LatLng.java +++ /dev/null @@ -1,198 +0,0 @@ -package fr.free.nrw.commons.location; - -import android.location.Location; -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; - -import androidx.annotation.NonNull; - -/** - * a latitude and longitude point with accuracy information, often of a picture - */ -public class LatLng implements Parcelable { - - private final double latitude; - private final double longitude; - private final float accuracy; - - /** - * Accepts latitude and longitude. - * North and South values are cut off at 90° - * - * @param latitude the latitude - * @param longitude the longitude - * @param accuracy the accuracy - * - * Examples: - * the Statue of Liberty is located at 40.69° N, 74.04° W - * The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0) - * where positive signifies north, east and negative signifies south, west. - */ - public LatLng(double latitude, double longitude, float accuracy) { - if (-180.0D <= longitude && longitude < 180.0D) { - this.longitude = longitude; - } else { - this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D; - } - this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude)); - this.accuracy = accuracy; - } - /** - * An alternate constructor for this class. - * @param in A parcelable which contains the latitude, longitude, and accuracy - */ - public LatLng(Parcel in) { - latitude = in.readDouble(); - longitude = in.readDouble(); - accuracy = in.readFloat(); - } - - /** - * gets the latitude and longitude of a given non-null location - * @param location the non-null location of the user - * @return LatLng the Latitude and Longitude of a given location - */ - public static LatLng from(@NonNull Location location) { - return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy()); - } - - /** - * creates a hash code for the longitude and longitude - */ - public int hashCode() { - byte var1 = 1; - long var2 = Double.doubleToLongBits(this.latitude); - int var3 = 31 * var1 + (int)(var2 ^ var2 >>> 32); - var2 = Double.doubleToLongBits(this.longitude); - var3 = 31 * var3 + (int)(var2 ^ var2 >>> 32); - return var3; - } - - /** - * checks for equality of two LatLng objects - * @param o the second LatLng object - */ - public boolean equals(Object o) { - if (this == o) { - return true; - } else if (!(o instanceof LatLng)) { - return false; - } else { - LatLng var2 = (LatLng)o; - return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude); - } - } - - /** - * returns a string representation of the latitude and longitude - */ - public String toString() { - return "lat/lng: (" + this.latitude + "," + this.longitude + ")"; - } - - /** - * Rounds the float to 4 digits and returns absolute value. - * - * @param coordinate A coordinate value as string. - * @return String of the rounded number. - */ - private String formatCoordinate(double coordinate) { - double roundedNumber = Math.round(coordinate * 10000d) / 10000d; - double absoluteNumber = Math.abs(roundedNumber); - return String.valueOf(absoluteNumber); - } - - /** - * Returns "N" or "S" depending on the latitude. - * - * @return "N" or "S". - */ - private String getNorthSouth() { - if (this.latitude < 0) { - return "S"; - } - - return "N"; - } - - /** - * Returns "E" or "W" depending on the longitude. - * - * @return "E" or "W". - */ - private String getEastWest() { - if (this.longitude >= 0 && this.longitude < 180) { - return "E"; - } - - return "W"; - } - - /** - * Returns a nicely formatted coordinate string. Used e.g. in - * the detail view. - * - * @return The formatted string. - */ - public String getPrettyCoordinateString() { - return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", " - + formatCoordinate(this.longitude) + " " + this.getEastWest(); - } - - /** - * Return the location accuracy in meter. - * - * @return float - */ - public float getAccuracy() { - return accuracy; - } - - /** - * Return the longitude in degrees. - * - * @return double - */ - public double getLongitude() { - return longitude; - } - - /** - * Return the latitude in degrees. - * - * @return double - */ - public double getLatitude() { - return latitude; - } - - public Uri getGmmIntentUri() { - return Uri.parse("geo:" + latitude + "," + longitude + "?z=16"); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeDouble(latitude); - dest.writeDouble(longitude); - dest.writeFloat(accuracy); - } - - public static final Creator CREATOR = new Creator() { - @Override - public LatLng createFromParcel(Parcel in) { - return new LatLng(in); - } - - @Override - public LatLng[] newArray(int size) { - return new LatLng[size]; - } - }; -} - diff --git a/app/src/main/java/fr/free/nrw/commons/location/LatLng.kt b/app/src/main/java/fr/free/nrw/commons/location/LatLng.kt new file mode 100644 index 000000000..4e21b93c2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/location/LatLng.kt @@ -0,0 +1,150 @@ +package fr.free.nrw.commons.location + +import android.location.Location +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.round + + +/** + * A latitude and longitude point with accuracy information, often of a picture. + */ +data class LatLng( + var latitude: Double, + var longitude: Double, + val accuracy: Float +) : Parcelable { + + /** + * Accepts latitude and longitude. + * North and South values are cut off at 90° + * + * Examples: + * the Statue of Liberty is located at 40.69° N, 74.04° W + * The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0) + * where positive signifies north, east and negative signifies south, west. + */ + init { + val adjustedLongitude = when { + longitude in -180.0..180.0 -> longitude + else -> ((longitude - 180.0) % 360.0 + 360.0) % 360.0 - 180.0 + } + latitude = max(-90.0, min(90.0, latitude)) + longitude = adjustedLongitude + } + + /** + * Accepts a non-null [Location] and converts it to a [LatLng]. + */ + companion object { + /** + * gets the latitude and longitude of a given non-null location + * @param location the non-null location of the user + * @return LatLng the Latitude and Longitude of a given location + */ + @JvmStatic + fun from(location: Location): LatLng { + return LatLng(location.latitude, location.longitude, location.accuracy) + } + + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): LatLng { + return LatLng(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + + /** + * An alternate constructor for this class. + * @param parcel A parcelable which contains the latitude, longitude, and accuracy + */ + private constructor(parcel: Parcel) : this( + latitude = parcel.readDouble(), + longitude = parcel.readDouble(), + accuracy = parcel.readFloat() + ) + + /** + * Creates a hash code for the latitude and longitude. + */ + override fun hashCode(): Int { + var result = 1 + val latitudeBits = latitude.toBits() + result = 31 * result + (latitudeBits xor (latitudeBits ushr 32)).toInt() + val longitudeBits = longitude.toBits() + result = 31 * result + (longitudeBits xor (longitudeBits ushr 32)).toInt() + return result + } + + /** + * Checks for equality of two LatLng objects. + * @param other the second LatLng object + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is LatLng) return false + return latitude.toBits() == other.latitude.toBits() && + longitude.toBits() == other.longitude.toBits() + } + + /** + * Returns a string representation of the latitude and longitude. + */ + override fun toString(): String { + return "lat/lng: ($latitude,$longitude)" + } + + /** + * Returns a nicely formatted coordinate string. Used e.g. in + * the detail view. + * + * @return The formatted string. + */ + fun getPrettyCoordinateString(): String { + return "${formatCoordinate(latitude)} ${getNorthSouth()}, " + + "${formatCoordinate(longitude)} ${getEastWest()}" + } + + /** + * Gets a URI for a Google Maps intent at the location. + */ + fun getGmmIntentUri(): Uri { + return Uri.parse("geo:$latitude,$longitude?z=16") + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeDouble(latitude) + parcel.writeDouble(longitude) + parcel.writeFloat(accuracy) + } + + override fun describeContents(): Int = 0 + + private fun formatCoordinate(coordinate: Double): String { + val roundedNumber = round(coordinate * 10000) / 10000 + return abs(roundedNumber).toString() + } + + /** + * Returns "N" or "S" depending on the latitude. + * + * @return "N" or "S". + */ + private fun getNorthSouth(): String = if (latitude < 0) "S" else "N" + + /** + * Returns "E" or "W" depending on the longitude. + * + * @return "E" or "W". + */ + private fun getEastWest(): String = if (longitude in 0.0..179.999) "E" else "W" +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.java b/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.java deleted file mode 100644 index 77e089c9c..000000000 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.java +++ /dev/null @@ -1,186 +0,0 @@ -package fr.free.nrw.commons.location; - -import android.Manifest; -import android.Manifest.permission; -import android.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.provider.Settings; -import android.widget.Toast; -import androidx.core.app.ActivityCompat; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.filepicker.Constants.RequestCodes; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.PermissionUtils; - -/** - * Helper class to handle location permissions. - * - * Location flow for fragments containing a map is as follows: - * Case 1: When location permission has never been asked for or denied before - * Check if permission is already granted or not. - * If not already granted, ask for it (if it isn't denied twice before). - * If now user grants permission, go to Case 3/4, else go to Case 2. - * - * Case 2: When location permission is just asked but has been denied - * Shows a toast to tell the user why location permission is needed. - * Also shows a rationale to the user, on agreeing to which, we go back to Case 1. - * Show current location / nearby pins / nearby images according to the default location. - * - * Case 3: When location permission are already granted, but location services are off - * Asks the user to turn on the location service, using a dialog. - * If the user rejects, checks for the last known location and shows stuff using that location. - * Also displays a toast telling the user why location should be turned on. - * - * Case 4: When location permission has been granted and location services are also on - * Do whatever is required by that particular activity / fragment using current location. - * - */ -public class LocationPermissionsHelper { - - Activity activity; - LocationServiceManager locationManager; - LocationPermissionCallback callback; - - public LocationPermissionsHelper(Activity activity, LocationServiceManager locationManager, - LocationPermissionCallback callback) { - this.activity = activity; - this.locationManager = locationManager; - this.callback = callback; - } - - /** - * Ask for location permission if the user agrees on attaching location with pictures and the - * app does not have the access to location - * - * @param dialogTitleResource Resource id of the title of the dialog - * @param dialogTextResource Resource id of the text of the dialog - */ - public void requestForLocationAccess( - int dialogTitleResource, - int dialogTextResource - ) { - if (checkLocationPermission(activity)) { - callback.onLocationPermissionGranted(); - } else { - if (ActivityCompat.shouldShowRequestPermissionRationale(activity, - permission.ACCESS_FINE_LOCATION)) { - DialogUtil.showAlertDialog(activity, activity.getString(dialogTitleResource), - activity.getString(dialogTextResource), - activity.getString(android.R.string.ok), - activity.getString(android.R.string.cancel), - () -> { - ActivityCompat.requestPermissions(activity, - new String[]{permission.ACCESS_FINE_LOCATION}, 1); - }, - () -> callback.onLocationPermissionDenied( - activity.getString(R.string.upload_map_location_access)), - null, - false); - } else { - ActivityCompat.requestPermissions(activity, - new String[]{permission.ACCESS_FINE_LOCATION}, - RequestCodes.LOCATION); - } - } - } - - /** - * Shows a dialog for user to open the settings page and turn on location services - * - * @param activity Activity object - * @param dialogTextResource int id of the required string resource - */ - public void showLocationOffDialog(Activity activity, int dialogTextResource) { - DialogUtil - .showAlertDialog(activity, - activity.getString(R.string.ask_to_turn_location_on), - activity.getString(dialogTextResource), - activity.getString(R.string.title_app_shortcut_setting), - activity.getString(R.string.cancel), - () -> openLocationSettings(activity), - () -> Toast.makeText(activity, activity.getString(dialogTextResource), - Toast.LENGTH_LONG).show() - ); - } - - /** - * Opens the location access page in settings, for user to turn on location services - * - * @param activity Activtiy object - */ - public void openLocationSettings(Activity activity) { - final Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); - final PackageManager packageManager = activity.getPackageManager(); - - if (intent.resolveActivity(packageManager) != null) { - activity.startActivity(intent); - } else { - Toast.makeText(activity, R.string.cannot_open_location_settings, Toast.LENGTH_LONG) - .show(); - } - } - - /** - * Shows a dialog for user to open the app's settings page and give location permission - * - * @param activity Activity object - * @param dialogTextResource int id of the required string resource - */ - public void showAppSettingsDialog(Activity activity, int dialogTextResource) { - DialogUtil - .showAlertDialog(activity, activity.getString(R.string.location_permission_title), - activity.getString(dialogTextResource), - activity.getString(R.string.title_app_shortcut_setting), - activity.getString(R.string.cancel), - () -> openAppSettings(activity), - () -> Toast.makeText(activity, activity.getString(dialogTextResource), - Toast.LENGTH_LONG).show() - ); - } - - /** - * Opens detailed settings page of the app for the user to turn on location services - * - * @param activity Activity object - */ - public void openAppSettings(Activity activity) { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", activity.getPackageName(), null); - intent.setData(uri); - activity.startActivity(intent); - } - - - /** - * Check if apps have access to location even after having individual access - * - * @return Returns true if location services are on and false otherwise - */ - public boolean isLocationAccessToAppsTurnedOn() { - return (locationManager.isNetworkProviderEnabled() - || locationManager.isGPSProviderEnabled()); - } - - /** - * Checks if location permission is already granted or not - * - * @param activity Activity object - * @return Returns true if location permission is granted and false otherwise - */ - public boolean checkLocationPermission(Activity activity) { - return PermissionUtils.hasPermission(activity, - new String[]{Manifest.permission.ACCESS_FINE_LOCATION}); - } - - /** - * Handle onPermissionDenied within individual classes based on the requirements - */ - public interface LocationPermissionCallback { - - void onLocationPermissionDenied(String toastMessage); - - void onLocationPermissionGranted(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.kt b/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.kt new file mode 100644 index 000000000..771d9efdc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.kt @@ -0,0 +1,200 @@ +package fr.free.nrw.commons.location + +import android.Manifest.permission +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import android.widget.Toast +import androidx.core.app.ActivityCompat +import fr.free.nrw.commons.R +import fr.free.nrw.commons.filepicker.Constants.RequestCodes +import fr.free.nrw.commons.utils.DialogUtil +import fr.free.nrw.commons.utils.PermissionUtils + +/** + * Helper class to handle location permissions. + * + * Location flow for fragments containing a map is as follows: + * Case 1: When location permission has never been asked for or denied before + * Check if permission is already granted or not. + * If not already granted, ask for it (if it isn't denied twice before). + * If now user grants permission, go to Case 3/4, else go to Case 2. + * + * Case 2: When location permission is just asked but has been denied + * Shows a toast to tell the user why location permission is needed. + * Also shows a rationale to the user, on agreeing to which, we go back to Case 1. + * Show current location / nearby pins / nearby images according to the default location. + * + * Case 3: When location permission are already granted, but location services are off + * Asks the user to turn on the location service, using a dialog. + * If the user rejects, checks for the last known location and shows stuff using that location. + * Also displays a toast telling the user why location should be turned on. + * + * Case 4: When location permission has been granted and location services are also on + * Do whatever is required by that particular activity / fragment using current location. + * + */ +class LocationPermissionsHelper( + private val activity: Activity, + private val locationManager: LocationServiceManager, + private val callback: LocationPermissionCallback? +) { + + /** + * Ask for location permission if the user agrees on attaching location with pictures and the + * app does not have the access to location + * + * @param dialogTitleResource Resource id of the title of the dialog + * @param dialogTextResource Resource id of the text of the dialog + */ + fun requestForLocationAccess( + dialogTitleResource: Int, + dialogTextResource: Int + ) { + if (checkLocationPermission(activity)) { + callback?.onLocationPermissionGranted() + } else { + if (ActivityCompat.shouldShowRequestPermissionRationale( + activity, + permission.ACCESS_FINE_LOCATION + ) + ) { + DialogUtil.showAlertDialog( + activity, + activity.getString(dialogTitleResource), + activity.getString(dialogTextResource), + activity.getString(android.R.string.ok), + activity.getString(android.R.string.cancel), + { + ActivityCompat.requestPermissions( + activity, + arrayOf(permission.ACCESS_FINE_LOCATION), + 1 + ) + }, + { + callback?.onLocationPermissionDenied( + activity.getString(R.string.upload_map_location_access) + ) + }, + null, + false + ) + } else { + ActivityCompat.requestPermissions( + activity, + arrayOf(permission.ACCESS_FINE_LOCATION), + RequestCodes.LOCATION + ) + } + } + } + + /** + * Shows a dialog for user to open the settings page and turn on location services + * + * @param activity Activity object + * @param dialogTextResource int id of the required string resource + */ + fun showLocationOffDialog(activity: Activity, dialogTextResource: Int) { + DialogUtil.showAlertDialog( + activity, + activity.getString(R.string.ask_to_turn_location_on), + activity.getString(dialogTextResource), + activity.getString(R.string.title_app_shortcut_setting), + activity.getString(R.string.cancel), + { openLocationSettings(activity) }, + { + Toast.makeText( + activity, + activity.getString(dialogTextResource), + Toast.LENGTH_LONG + ).show() + } + ) + } + + /** + * Opens the location access page in settings, for user to turn on location services + * + * @param activity Activity object + */ + fun openLocationSettings(activity: Activity) { + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + val packageManager = activity.packageManager + + if (intent.resolveActivity(packageManager) != null) { + activity.startActivity(intent) + } else { + Toast.makeText(activity, R.string.cannot_open_location_settings, Toast.LENGTH_LONG) + .show() + } + } + + /** + * Shows a dialog for user to open the app's settings page and give location permission + * + * @param activity Activity object + * @param dialogTextResource int id of the required string resource + */ + fun showAppSettingsDialog(activity: Activity, dialogTextResource: Int) { + DialogUtil.showAlertDialog( + activity, + activity.getString(R.string.location_permission_title), + activity.getString(dialogTextResource), + activity.getString(R.string.title_app_shortcut_setting), + activity.getString(R.string.cancel), + { openAppSettings(activity) }, + { + Toast.makeText( + activity, + activity.getString(dialogTextResource), + Toast.LENGTH_LONG + ).show() + } + ) + } + + /** + * Opens detailed settings page of the app for the user to turn on location services + * + * @param activity Activity object + */ + private fun openAppSettings(activity: Activity) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri = Uri.fromParts("package", activity.packageName, null) + intent.data = uri + activity.startActivity(intent) + } + + /** + * Check if apps have access to location even after having individual access + * + * @return Returns true if location services are on and false otherwise + */ + fun isLocationAccessToAppsTurnedOn(): Boolean { + return locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled() + } + + /** + * Checks if location permission is already granted or not + * + * @param activity Activity object + * @return Returns true if location permission is granted and false otherwise + */ + fun checkLocationPermission(activity: Activity): Boolean { + return PermissionUtils.hasPermission( + activity, + arrayOf(permission.ACCESS_FINE_LOCATION) + ) + } + + /** + * Handle onPermissionDenied within individual classes based on the requirements + */ + interface LocationPermissionCallback { + fun onLocationPermissionDenied(toastMessage: String) + fun onLocationPermissionGranted() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java deleted file mode 100644 index 4c7289ea5..000000000 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ /dev/null @@ -1,274 +0,0 @@ -package fr.free.nrw.commons.location; - -import android.Manifest.permission; -import android.app.Activity; -import android.content.Context; -import android.content.pm.PackageManager; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.Bundle; -import androidx.core.app.ActivityCompat; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; - -import timber.log.Timber; - -public class LocationServiceManager implements LocationListener { - - // Maybe these values can be improved for efficiency - private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 10 * 100; - private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 1; - - private LocationManager locationManager; - private Location lastLocation; - //private Location lastLocationDuplicate; // Will be used for nearby card view on contributions activity - private final List locationListeners = new CopyOnWriteArrayList<>(); - private boolean isLocationManagerRegistered = false; - private Set locationExplanationDisplayed = new HashSet<>(); - private Context context; - - /** - * Constructs a new instance of LocationServiceManager. - * - * @param context the context - */ - public LocationServiceManager(Context context) { - this.context = context; - this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - } - - public LatLng getLastLocation() { - if (lastLocation == null) { - lastLocation = getLastKnownLocation(); - if(lastLocation != null) { - return LatLng.from(lastLocation); - } - else { - return null; - } - } - return LatLng.from(lastLocation); - } - - private Location getLastKnownLocation() { - List providers = locationManager.getProviders(true); - Location bestLocation = null; - for (String provider : providers) { - Location l=null; - if (ActivityCompat.checkSelfPermission(context, permission.ACCESS_FINE_LOCATION) - == PackageManager.PERMISSION_GRANTED - && ActivityCompat.checkSelfPermission(context, permission.ACCESS_COARSE_LOCATION) - == PackageManager.PERMISSION_GRANTED) { - l = locationManager.getLastKnownLocation(provider); - } - if (l == null) { - continue; - } - if (bestLocation == null - || l.getAccuracy() < bestLocation.getAccuracy()) { - bestLocation = l; - } - } - if (bestLocation == null) { - return null; - } - return bestLocation; - } - - /** - * Registers a LocationManager to listen for current location. - */ - public void registerLocationManager() { - if (!isLocationManagerRegistered) { - isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) - && requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); - } - } - - /** - * Requests location updates from the specified provider. - * - * @param locationProvider the location provider - * @return true if successful - */ - public boolean requestLocationUpdatesFromProvider(String locationProvider) { - try { - // If both providers are not available - if (locationManager == null || !(locationManager.getAllProviders().contains(locationProvider))) { - return false; - } - locationManager.requestLocationUpdates(locationProvider, - MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS, - MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS, - this); - return true; - } catch (IllegalArgumentException e) { - Timber.e(e, "Illegal argument exception"); - return false; - } catch (SecurityException e) { - Timber.e(e, "Security exception"); - return false; - } - } - - /** - * Returns whether a given location is better than the current best location. - * - * @param location the location to be tested - * @param currentBestLocation the current best location - * @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly - * LOCATION_SLIGHTLY_CHANGED if location changed slightly - */ - private LocationChangeType isBetterLocation(Location location, Location currentBestLocation) { - - if (currentBestLocation == null) { - // A new location is always better than no location - return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; - } - - // Check whether the new location fix is newer or older - long timeDelta = location.getTime() - currentBestLocation.getTime(); - boolean isSignificantlyNewer = timeDelta > MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS; - boolean isNewer = timeDelta > 0; - - // Check whether the new location fix is more or less accurate - int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); - boolean isLessAccurate = accuracyDelta > 0; - boolean isMoreAccurate = accuracyDelta < 0; - boolean isSignificantlyLessAccurate = accuracyDelta > 200; - - // Check if the old and new location are from the same provider - boolean isFromSameProvider = isSameProvider(location.getProvider(), - currentBestLocation.getProvider()); - - float[] results = new float[5]; - Location.distanceBetween( - currentBestLocation.getLatitude(), - currentBestLocation.getLongitude(), - location.getLatitude(), - location.getLongitude(), - results); - - // If it's been more than two minutes since the current location, use the new location - // because the user has likely moved - if (isSignificantlyNewer - || isMoreAccurate - || (isNewer && !isLessAccurate) - || (isNewer && !isSignificantlyLessAccurate && isFromSameProvider)) { - if (results[0] < 1000) { // Means change is smaller than 1000 meter - return LocationChangeType.LOCATION_SLIGHTLY_CHANGED; - } else { - return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; - } - } else{ - return LocationChangeType.LOCATION_NOT_CHANGED; - } - } - - /** - * Checks whether two providers are the same - */ - private boolean isSameProvider(String provider1, String provider2) { - if (provider1 == null) { - return provider2 == null; - } - return provider1.equals(provider2); - } - - /** - * Unregisters location manager. - */ - public void unregisterLocationManager() { - isLocationManagerRegistered = false; - locationExplanationDisplayed.clear(); - try { - locationManager.removeUpdates(this); - } catch (SecurityException e) { - Timber.e(e, "Security exception"); - } - } - - /** - * Adds a new listener to the list of location listeners. - * - * @param listener the new listener - */ - public void addLocationListener(LocationUpdateListener listener) { - if (!locationListeners.contains(listener)) { - locationListeners.add(listener); - } - } - - /** - * Removes a listener from the list of location listeners. - * - * @param listener the listener to be removed - */ - public void removeLocationListener(LocationUpdateListener listener) { - locationListeners.remove(listener); - } - - @Override - public void onLocationChanged(Location location) { - Timber.d("on location changed"); - if (isBetterLocation(location, lastLocation) - .equals(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) { - lastLocation = location; - //lastLocationDuplicate = location; - for (LocationUpdateListener listener : locationListeners) { - listener.onLocationChangedSignificantly(LatLng.from(lastLocation)); - } - } else if (location.distanceTo(lastLocation) >= 500) { - // Update nearby notification card at every 500 meters. - for (LocationUpdateListener listener : locationListeners) { - listener.onLocationChangedMedium(LatLng.from(lastLocation)); - } - } - - else if (isBetterLocation(location, lastLocation) - .equals(LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) { - lastLocation = location; - //lastLocationDuplicate = location; - for (LocationUpdateListener listener : locationListeners) { - listener.onLocationChangedSlightly(LatLng.from(lastLocation)); - } - } - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - Timber.d("%s's status changed to %d", provider, status); - } - - @Override - public void onProviderEnabled(String provider) { - Timber.d("Provider %s enabled", provider); - } - - @Override - public void onProviderDisabled(String provider) { - Timber.d("Provider %s disabled", provider); - } - - public boolean isNetworkProviderEnabled() { - return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER); - } - - public boolean isGPSProviderEnabled() { - return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); - } - - public enum LocationChangeType{ - LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers - LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving - LOCATION_MEDIUM_CHANGED, //Between slight and significant changes, will be used for nearby card view updates. - LOCATION_NOT_CHANGED, - PERMISSION_JUST_GRANTED, - MAP_UPDATED, - SEARCH_CUSTOM_AREA, - CUSTOM_QUERY - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.kt b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.kt new file mode 100644 index 000000000..3a4c4b72e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.kt @@ -0,0 +1,255 @@ +package fr.free.nrw.commons.location + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import androidx.core.app.ActivityCompat +import timber.log.Timber +import java.util.concurrent.CopyOnWriteArrayList + + +class LocationServiceManager(private val context: Context) : LocationListener { + + companion object { + // Maybe these values can be improved for efficiency + private const val MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 10 * 100L + private const val MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 1f + } + + private val locationManager: LocationManager = + context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + private var lastLocationVar: Location? = null + private val locationListeners = CopyOnWriteArrayList() + private var isLocationManagerRegistered = false + private val locationExplanationDisplayed = mutableSetOf() + + /** + * Constructs a new instance of LocationServiceManager. + * + */ + fun getLastLocation(): LatLng? { + if (lastLocationVar == null) { + lastLocationVar = getLastKnownLocation() + return lastLocationVar?.let { LatLng.from(it) } + } + return LatLng.from(lastLocationVar!!) + } + + private fun getLastKnownLocation(): Location? { + val providers = locationManager.getProviders(true) + var bestLocation: Location? = null + for (provider in providers) { + val location: Location? = if ( + ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION) + == + PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION) + == + PackageManager.PERMISSION_GRANTED + ) { + locationManager.getLastKnownLocation(provider) + } else { + null + } + + if ( + location != null + && + (bestLocation == null || location.accuracy < bestLocation.accuracy) + ) { + bestLocation = location + } + } + return bestLocation + } + + /** + * Registers a LocationManager to listen for current location. + */ + fun registerLocationManager() { + if (!isLocationManagerRegistered) { + isLocationManagerRegistered = + requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) && + requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER) + } + } + + /** + * Requests location updates from the specified provider. + * + * @param locationProvider the location provider + * @return true if successful + */ + fun requestLocationUpdatesFromProvider(locationProvider: String): Boolean { + return try { + if (locationManager.allProviders.contains(locationProvider)) { + locationManager.requestLocationUpdates( + locationProvider, + MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS, + MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS, + this + ) + true + } else { + false + } + } catch (e: IllegalArgumentException) { + Timber.e(e, "Illegal argument exception") + false + } catch (e: SecurityException) { + Timber.e(e, "Security exception") + false + } + } + + /** + * Returns whether a given location is better than the current best location. + * + * @param location the location to be tested + * @param currentBestLocation the current best location + * @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly + * LOCATION_SLIGHTLY_CHANGED if location changed slightly + */ + private fun isBetterLocation(location: Location, currentBestLocation: Location?): LocationChangeType { + if (currentBestLocation == null) { + return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED + } + + val timeDelta = location.time - currentBestLocation.time + val isSignificantlyNewer = timeDelta > MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS + val isNewer = timeDelta > 0 + val accuracyDelta = (location.accuracy - currentBestLocation.accuracy).toInt() + val isMoreAccurate = accuracyDelta < 0 + val isSignificantlyLessAccurate = accuracyDelta > 200 + val isFromSameProvider = isSameProvider(location.provider, currentBestLocation.provider) + + val results = FloatArray(5) + Location.distanceBetween( + currentBestLocation.latitude, currentBestLocation.longitude, + location.latitude, location.longitude, + results + ) + + return when { + isSignificantlyNewer + || + isMoreAccurate + || + (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) -> { + if (results[0] < 1000) LocationChangeType.LOCATION_SLIGHTLY_CHANGED + else LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED + } + else -> LocationChangeType.LOCATION_NOT_CHANGED + } + } + + /** + * Checks whether two providers are the same + */ + private fun isSameProvider(provider1: String?, provider2: String?): Boolean { + return provider1 == provider2 + } + + /** + * Unregisters location manager. + */ + fun unregisterLocationManager() { + isLocationManagerRegistered = false + locationExplanationDisplayed.clear() + try { + locationManager.removeUpdates(this) + } catch (e: SecurityException) { + Timber.e(e, "Security exception") + } + } + + /** + * Adds a new listener to the list of location listeners. + * + * @param listener the new listener + */ + fun addLocationListener(listener: LocationUpdateListener) { + if (!locationListeners.contains(listener)) { + locationListeners.add(listener) + } + } + + /** + * Removes a listener from the list of location listeners. + * + * @param listener the listener to be removed + */ + fun removeLocationListener(listener: LocationUpdateListener) { + locationListeners.remove(listener) + } + + override fun onLocationChanged(location: Location) { + Timber.d("on location changed") + val changeType = isBetterLocation(location, lastLocationVar) + if (changeType == LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) { + lastLocationVar = location + locationListeners.forEach { it.onLocationChangedSignificantly(LatLng.from(location)) } + } else if (lastLocationVar?.let { location.distanceTo(it) }!! >= 500) { + locationListeners.forEach { it.onLocationChangedMedium(LatLng.from(location)) } + } else if (changeType == LocationChangeType.LOCATION_SLIGHTLY_CHANGED) { + lastLocationVar = location + locationListeners.forEach { it.onLocationChangedSlightly(LatLng.from(location)) } + } + } + + @Deprecated("Deprecated in Java", ReplaceWith( + "Timber.d(\"%s's status changed to %d\", provider, status)", + "timber.log.Timber" + ) + ) + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) { + Timber.d("%s's status changed to %d", provider, status) + } + + + + override fun onProviderEnabled(provider: String) { + Timber.d("Provider %s enabled", provider) + } + + override fun onProviderDisabled(provider: String) { + Timber.d("Provider %s disabled", provider) + } + + fun isNetworkProviderEnabled(): Boolean { + return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } + + fun isGPSProviderEnabled(): Boolean { + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) + } + + enum class LocationChangeType { + LOCATION_SIGNIFICANTLY_CHANGED, + LOCATION_SLIGHTLY_CHANGED, + LOCATION_MEDIUM_CHANGED, + LOCATION_NOT_CHANGED, + PERMISSION_JUST_GRANTED, + MAP_UPDATED, + SEARCH_CUSTOM_AREA, + CUSTOM_QUERY + } +} + + + + + + + + + diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java deleted file mode 100644 index 61ff26b11..000000000 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package fr.free.nrw.commons.location; - -public interface LocationUpdateListener { - void onLocationChangedSignificantly(LatLng latLng); // Will be used to update all nearby markers on the map - void onLocationChangedSlightly(LatLng latLng); // Will be used to track users motion - void onLocationChangedMedium(LatLng latLng); // Will be used updating nearby card view notification -} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.kt b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.kt new file mode 100644 index 000000000..e90cc1224 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.kt @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.location + +interface LocationUpdateListener { + // Will be used to update all nearby markers on the map + fun onLocationChangedSignificantly(latLng: LatLng) + + // Will be used to track users motion + fun onLocationChangedSlightly(latLng: LatLng) + + // Will be used updating nearby card view notification + fun onLocationChangedMedium(latLng: LatLng) +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt index 2847fa0c0..fa825d0a6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt @@ -42,8 +42,8 @@ class LanguagesAdapter constructor( AppLanguageLookUpTable(context) init { - languageNamesList = language.localizedNames - languageCodesList = language.codes + languageNamesList = language.getLocalizedNames() + languageCodesList = language.getCodes() } private val filter = LanguageFilter() @@ -117,7 +117,7 @@ class LanguagesAdapter constructor( */ fun getIndexOfUserDefaultLocale(context: Context): Int { val userLanguageCode = context.locale?.language ?: return DEFAULT_INDEX - return language.codes.indexOf(userLanguageCode).takeIf { it >= 0 } ?: DEFAULT_INDEX + return language.getCodes().indexOf(userLanguageCode).takeIf { it >= 0 } ?: DEFAULT_INDEX } fun getIndexOfLanguageCode(languageCode: String): Int = languageCodesList.indexOf(languageCode) @@ -128,17 +128,17 @@ class LanguagesAdapter constructor( override fun performFiltering(constraint: CharSequence?): FilterResults { val filterResults = FilterResults() val temp: LinkedHashMap = LinkedHashMap() - if (constraint != null && language.localizedNames != null) { - val length: Int = language.localizedNames.size + if (constraint != null) { + val length: Int = language.getLocalizedNames().size var i = 0 while (i < length) { - val key: String = language.codes[i] - val value: String = language.localizedNames[i] + val key: String = language.getCodes()[i] + val value: String = language.getLocalizedNames()[i] val defaultlanguagecode = getIndexOfUserDefaultLocale(context) if (value.contains(constraint, true) || Locale(key) .getDisplayName( - Locale(language.codes[defaultlanguagecode]), + Locale(language.getCodes()[defaultlanguagecode]), ).contains(constraint, true) ) { temp[key] = value diff --git a/app/src/test/kotlin/fr/free/nrw/commons/LatLngTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/LatLngTests.kt index d9ef4d6e8..3b208b5c1 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/LatLngTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/LatLngTests.kt @@ -62,5 +62,5 @@ class LatLngTests { private fun assertPrettyCoordinateString( expected: String, place: LatLng, - ) = assertEquals(expected, place.prettyCoordinateString) + ) = assertEquals(expected, place.getPrettyCoordinateString()) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt index ea1d3402d..9f73d2b81 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt @@ -248,11 +248,11 @@ class MediaDetailFragmentUnitTests { @Throws(Exception::class) fun testOnUpdateCoordinatesClickedCurrentLocationNull() { `when`(media.coordinates).thenReturn(null) - `when`(locationManager.lastLocation).thenReturn(null) + `when`(locationManager.getLastLocation()).thenReturn(null) `when`(applicationKvStore.getString(lastLocation)).thenReturn("37.773972,-122.431297") fragment.onUpdateCoordinatesClicked() Mockito.verify(media, Mockito.times(1)).coordinates - Mockito.verify(locationManager, Mockito.times(1)).lastLocation + Mockito.verify(locationManager, Mockito.times(1)).getLastLocation() val shadowActivity: ShadowActivity = shadowOf(activity) val startedIntent = shadowActivity.nextStartedActivity val shadowIntent: ShadowIntent = shadowOf(startedIntent) @@ -276,11 +276,11 @@ class MediaDetailFragmentUnitTests { @Throws(Exception::class) fun testOnUpdateCoordinatesClickedCurrentLocationNotNull() { `when`(media.coordinates).thenReturn(null) - `when`(locationManager.lastLocation).thenReturn(LatLng(-0.000001, -0.999999, 0f)) + `when`(locationManager.getLastLocation()).thenReturn(LatLng(-0.000001, -0.999999, 0f)) `when`(applicationKvStore.getString(lastLocation)).thenReturn("37.773972,-122.431297") fragment.onUpdateCoordinatesClicked() - Mockito.verify(locationManager, Mockito.times(3)).lastLocation + Mockito.verify(locationManager, Mockito.times(3)).getLastLocation() val shadowActivity: ShadowActivity = shadowOf(activity) val startedIntent = shadowActivity.nextStartedActivity val shadowIntent: ShadowIntent = shadowOf(startedIntent) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/LanguagesAdapterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/LanguagesAdapterTest.kt index 801d4e900..f272a8288 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/LanguagesAdapterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/LanguagesAdapterTest.kt @@ -54,8 +54,8 @@ class LanguagesAdapterTest { .from(context) .inflate(R.layout.row_item_languages_spinner, null) as View - languageNamesList = language.localizedNames - languageCodesList = language.codes + languageNamesList = language.getLocalizedNames() + languageCodesList = language.getCodes() languagesAdapter = LanguagesAdapter(context, selectedLanguages) } @@ -124,12 +124,12 @@ class LanguagesAdapterTest { var i = 0 var s = 0 while (i < length) { - val key: String = language.codes[i] - val value: String = language.localizedNames[i] + val key: String = language.getCodes()[i] + val value: String = language.getLocalizedNames()[i] if (value.contains(constraint, true) || Locale(key) .getDisplayName( - Locale(language.codes[defaultlanguagecode!!]), + Locale(language.getCodes()[defaultlanguagecode!!]), ).contains(constraint, true) ) { s++ From 33548fa57d9d56c5208c39aeabd40b0fc2b336ad Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Tue, 3 Dec 2024 00:47:25 -0600 Subject: [PATCH 52/74] Convert profile package to kotlin (#5979) * Convert ViewModelFactory to kotlin * Convert UpdateAvatarResponse and related test to Kotlin * Convert LeaderboardResponse and related test to kotlin * Convert LeaderboardListAdapter to kotlin * Convert UserDetailAdapter to kotlin * Convert LeaderboardListViewModel to kotlin * Convert DataSourceClass to kotlin * Convert the LeaderboardFragment to kotlin * Converted AchievementsFragment to kotlin * Revert "Converted AchievementsFragment to kotlin" This reverts commit 4fcbb81e5dd95c1eab5910cab3d728959ad569f0. --------- Co-authored-by: Nicolas Raoul --- .../profile/leaderboard/DataSourceClass.java | 125 ------ .../profile/leaderboard/DataSourceClass.kt | 79 ++++ .../leaderboard/DataSourceFactory.java | 110 ------ .../profile/leaderboard/DataSourceFactory.kt | 27 ++ .../leaderboard/LeaderboardConstants.java | 45 --- .../leaderboard/LeaderboardConstants.kt | 44 +++ .../leaderboard/LeaderboardFragment.java | 363 ------------------ .../leaderboard/LeaderboardFragment.kt | 319 +++++++++++++++ .../profile/leaderboard/LeaderboardList.java | 137 ------- .../profile/leaderboard/LeaderboardList.kt | 61 +++ .../leaderboard/LeaderboardListAdapter.java | 93 ----- .../leaderboard/LeaderboardListAdapter.kt | 64 +++ .../leaderboard/LeaderboardListViewModel.java | 107 ------ .../leaderboard/LeaderboardListViewModel.kt | 54 +++ .../leaderboard/LeaderboardResponse.java | 237 ------------ .../leaderboard/LeaderboardResponse.kt | 19 + .../leaderboard/UpdateAvatarResponse.java | 77 ---- .../leaderboard/UpdateAvatarResponse.kt | 10 + .../leaderboard/UserDetailAdapter.java | 126 ------ .../profile/leaderboard/UserDetailAdapter.kt | 91 +++++ .../profile/leaderboard/ViewModelFactory.java | 41 -- .../profile/leaderboard/ViewModelFactory.kt | 26 ++ .../leaderboard/LeaderboardApiTest.java | 116 ------ .../commons/leaderboard/LeaderboardApiTest.kt | 121 ++++++ .../leaderboard/UpdateAvatarApiTest.java | 117 ------ .../leaderboard/UpdateAvatarApiTest.kt | 127 ++++++ 26 files changed, 1042 insertions(+), 1694 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.kt delete mode 100644 app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.java create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.kt delete mode 100644 app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.java create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.kt diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.java deleted file mode 100644 index 409450d60..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.java +++ /dev/null @@ -1,125 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADED; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADING; - -import androidx.annotation.NonNull; -import androidx.lifecycle.MutableLiveData; -import androidx.paging.PageKeyedDataSource; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.disposables.CompositeDisposable; -import java.util.Objects; -import timber.log.Timber; - -/** - * This class will call the leaderboard API to get new list when the pagination is performed - */ -public class DataSourceClass extends PageKeyedDataSource { - - private OkHttpJsonApiClient okHttpJsonApiClient; - private SessionManager sessionManager; - private MutableLiveData progressLiveStatus; - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - private String duration; - private String category; - private int limit; - private int offset; - - /** - * Initialise the Data Source Class with API params - * @param okHttpJsonApiClient - * @param sessionManager - * @param duration - * @param category - * @param limit - * @param offset - */ - public DataSourceClass(OkHttpJsonApiClient okHttpJsonApiClient,SessionManager sessionManager, - String duration, String category, int limit, int offset) { - this.okHttpJsonApiClient = okHttpJsonApiClient; - this.sessionManager = sessionManager; - this.duration = duration; - this.category = category; - this.limit = limit; - this.offset = offset; - progressLiveStatus = new MutableLiveData<>(); - } - - - /** - * @return the status of the list - */ - public MutableLiveData getProgressLiveStatus() { - return progressLiveStatus; - } - - /** - * Loads the initial set of data from API - * @param params - * @param callback - */ - @Override - public void loadInitial(@NonNull LoadInitialParams params, - @NonNull LoadInitialCallback callback) { - - compositeDisposable.add(okHttpJsonApiClient - .getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name, - duration, category, String.valueOf(limit), String.valueOf(offset)) - .doOnSubscribe(disposable -> { - compositeDisposable.add(disposable); - progressLiveStatus.postValue(LOADING); - }).subscribe( - response -> { - if (response != null && response.getStatus() == 200) { - progressLiveStatus.postValue(LOADED); - callback.onResult(response.getLeaderboardList(), null, response.getLimit()); - } - }, - t -> { - Timber.e(t, "Fetching leaderboard statistics failed"); - progressLiveStatus.postValue(LOADING); - } - )); - - } - - /** - * Loads any data before the inital page is loaded - * @param params - * @param callback - */ - @Override - public void loadBefore(@NonNull LoadParams params, - @NonNull LoadCallback callback) { - - } - - /** - * Loads the next set of data on scrolling with offset as the limit of the last set of data - * @param params - * @param callback - */ - @Override - public void loadAfter(@NonNull LoadParams params, - @NonNull LoadCallback callback) { - compositeDisposable.add(okHttpJsonApiClient - .getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name, - duration, category, String.valueOf(limit), String.valueOf(params.key)) - .doOnSubscribe(disposable -> { - compositeDisposable.add(disposable); - progressLiveStatus.postValue(LOADING); - }).subscribe( - response -> { - if (response != null && response.getStatus() == 200) { - progressLiveStatus.postValue(LOADED); - callback.onResult(response.getLeaderboardList(), params.key + limit); - } - }, - t -> { - Timber.e(t, "Fetching leaderboard statistics failed"); - progressLiveStatus.postValue(LOADING); - } - )); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.kt new file mode 100644 index 000000000..a6fe747e5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.kt @@ -0,0 +1,79 @@ +package fr.free.nrw.commons.profile.leaderboard + +import android.accounts.Account +import androidx.lifecycle.MutableLiveData +import androidx.paging.PageKeyedDataSource +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADING +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADED +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import timber.log.Timber +import java.util.Objects + +/** + * This class will call the leaderboard API to get new list when the pagination is performed + */ +class DataSourceClass( + private val okHttpJsonApiClient: OkHttpJsonApiClient, + private val sessionManager: SessionManager, + private val duration: String?, + private val category: String?, + private val limit: Int, + private val offset: Int +) : PageKeyedDataSource() { + val progressLiveStatus: MutableLiveData = MutableLiveData() + private val compositeDisposable = CompositeDisposable() + + + override fun loadInitial( + params: LoadInitialParams, callback: LoadInitialCallback + ) { + compositeDisposable.add(okHttpJsonApiClient.getLeaderboard( + sessionManager.currentAccount?.name, + duration, + category, + limit.toString(), + offset.toString() + ).doOnSubscribe { disposable: Disposable? -> + compositeDisposable.add(disposable!!) + progressLiveStatus.postValue(LOADING) + }.subscribe({ response: LeaderboardResponse? -> + if (response != null && response.status == 200) { + progressLiveStatus.postValue(LOADED) + callback.onResult(response.leaderboardList!!, null, response.limit) + } + }, { t: Throwable? -> + Timber.e(t, "Fetching leaderboard statistics failed") + progressLiveStatus.postValue(LOADING) + })) + } + + override fun loadBefore( + params: LoadParams, callback: LoadCallback + ) = Unit + + override fun loadAfter( + params: LoadParams, callback: LoadCallback + ) { + compositeDisposable.add(okHttpJsonApiClient.getLeaderboard( + Objects.requireNonNull(sessionManager.currentAccount).name, + duration, + category, + limit.toString(), + params.key.toString() + ).doOnSubscribe { disposable: Disposable? -> + compositeDisposable.add(disposable!!) + progressLiveStatus.postValue(LOADING) + }.subscribe({ response: LeaderboardResponse? -> + if (response != null && response.status == 200) { + progressLiveStatus.postValue(LOADED) + callback.onResult(response.leaderboardList!!, params.key + limit) + } + }, { t: Throwable? -> + Timber.e(t, "Fetching leaderboard statistics failed") + progressLiveStatus.postValue(LOADING) + })) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.java deleted file mode 100644 index b2965785a..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.java +++ /dev/null @@ -1,110 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import androidx.lifecycle.MutableLiveData; -import androidx.paging.DataSource; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.disposables.CompositeDisposable; - -/** - * This class will create a new instance of the data source class on pagination - */ -public class DataSourceFactory extends DataSource.Factory { - - private MutableLiveData liveData; - private OkHttpJsonApiClient okHttpJsonApiClient; - private CompositeDisposable compositeDisposable; - private SessionManager sessionManager; - private String duration; - private String category; - private int limit; - private int offset; - - /** - * Gets the current set leaderboard list duration - */ - public String getDuration() { - return duration; - } - - /** - * Sets the current set leaderboard duration with the new duration - */ - public void setDuration(final String duration) { - this.duration = duration; - } - - /** - * Gets the current set leaderboard list category - */ - public String getCategory() { - return category; - } - - /** - * Sets the current set leaderboard category with the new category - */ - public void setCategory(final String category) { - this.category = category; - } - - /** - * Gets the current set leaderboard list limit - */ - public int getLimit() { - return limit; - } - - /** - * Sets the current set leaderboard limit with the new limit - */ - public void setLimit(final int limit) { - this.limit = limit; - } - - /** - * Gets the current set leaderboard list offset - */ - public int getOffset() { - return offset; - } - - /** - * Sets the current set leaderboard offset with the new offset - */ - public void setOffset(final int offset) { - this.offset = offset; - } - - /** - * Constructor for DataSourceFactory class - * @param okHttpJsonApiClient client for OKhttp - * @param compositeDisposable composite disposable - * @param sessionManager sessionManager - */ - public DataSourceFactory(OkHttpJsonApiClient okHttpJsonApiClient, CompositeDisposable compositeDisposable, - SessionManager sessionManager) { - this.okHttpJsonApiClient = okHttpJsonApiClient; - this.compositeDisposable = compositeDisposable; - this.sessionManager = sessionManager; - liveData = new MutableLiveData<>(); - } - - /** - * @return the live data - */ - public MutableLiveData getMutableLiveData() { - return liveData; - } - - /** - * Creates the new instance of data source class - * @return - */ - @Override - public DataSource create() { - DataSourceClass dataSourceClass = new DataSourceClass(okHttpJsonApiClient, sessionManager, duration, category, limit, offset); - liveData.postValue(dataSourceClass); - return dataSourceClass; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.kt new file mode 100644 index 000000000..6e979d8c3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.kt @@ -0,0 +1,27 @@ +package fr.free.nrw.commons.profile.leaderboard + +import androidx.lifecycle.MutableLiveData +import androidx.paging.DataSource +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient + +/** + * This class will create a new instance of the data source class on pagination + */ +class DataSourceFactory( + private val okHttpJsonApiClient: OkHttpJsonApiClient, + private val sessionManager: SessionManager +) : DataSource.Factory() { + val mutableLiveData: MutableLiveData = MutableLiveData() + var duration: String? = null + var category: String? = null + var limit: Int = 0 + var offset: Int = 0 + + /** + * Creates the new instance of data source class + */ + override fun create(): DataSource = DataSourceClass( + okHttpJsonApiClient, sessionManager, duration, category, limit, offset + ).also { mutableLiveData.postValue(it) } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.java deleted file mode 100644 index 800287f4f..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.java +++ /dev/null @@ -1,45 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -/** - * This class contains the constant variables for leaderboard - */ -public class LeaderboardConstants { - - /** - * This is the size of the page i.e. number items to load in a batch when pagination is performed - */ - public static final int PAGE_SIZE = 100; - - /** - * This is the starting offset, we set it to 0 to start loading from rank 1 - */ - public static final int START_OFFSET = 0; - - /** - * This is the prefix of the user's homepage url, appending the username will give us complete url - */ - public static final String USER_LINK_PREFIX = "https://commons.wikimedia.org/wiki/User:"; - - /** - * This is the a constant string for the state loading, when the pages are getting loaded we can - * use this constant to identify if we need to show the progress bar or not - */ - public final static String LOADING = "Loading"; - - /** - * This is the a constant string for the state loaded, when the pages are loaded we can - * use this constant to identify if we need to show the progress bar or not - */ - public final static String LOADED = "Loaded"; - - /** - * This API endpoint is to update the leaderboard avatar - */ - public final static String UPDATE_AVATAR_END_POINT = "/update_avatar.py"; - - /** - * This API endpoint is to get leaderboard data - */ - public final static String LEADERBOARD_END_POINT = "/leaderboard.py"; - -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.kt new file mode 100644 index 000000000..bf8d45c5f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.kt @@ -0,0 +1,44 @@ +package fr.free.nrw.commons.profile.leaderboard + +/** + * This class contains the constant variables for leaderboard + */ +object LeaderboardConstants { + /** + * This is the size of the page i.e. number items to load in a batch when pagination is performed + */ + const val PAGE_SIZE: Int = 100 + + /** + * This is the starting offset, we set it to 0 to start loading from rank 1 + */ + const val START_OFFSET: Int = 0 + + /** + * This is the prefix of the user's homepage url, appending the username will give us complete url + */ + const val USER_LINK_PREFIX: String = "https://commons.wikimedia.org/wiki/User:" + + sealed class LoadingStatus { + /** + * This is the state loading, when the pages are getting loaded we can + * use this constant to identify if we need to show the progress bar or not + */ + data object LOADING: LoadingStatus() + /** + * This is the state loaded, when the pages are loaded we can + * use this constant to identify if we need to show the progress bar or not + */ + data object LOADED: LoadingStatus() + } + + /** + * This API endpoint is to update the leaderboard avatar + */ + const val UPDATE_AVATAR_END_POINT: String = "/update_avatar.py" + + /** + * This API endpoint is to get leaderboard data + */ + const val LEADERBOARD_END_POINT: String = "/leaderboard.py" +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.java deleted file mode 100644 index a9cc222ea..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.java +++ /dev/null @@ -1,363 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADED; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADING; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.START_OFFSET; - -import android.accounts.Account; -import android.content.Context; -import android.os.Bundle; -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.Toast; -import androidx.annotation.Nullable; -import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.MergeAdapter; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.databinding.FragmentLeaderboardBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import fr.free.nrw.commons.profile.ProfileActivity; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.util.Objects; -import javax.inject.Inject; -import timber.log.Timber; - -/** - * This class extends the CommonsDaggerSupportFragment and creates leaderboard fragment - */ -public class LeaderboardFragment extends CommonsDaggerSupportFragment { - - - @Inject - SessionManager sessionManager; - - @Inject - OkHttpJsonApiClient okHttpJsonApiClient; - - @Inject - ViewModelFactory viewModelFactory; - - /** - * View model for the paged leaderboard list - */ - private LeaderboardListViewModel viewModel; - - /** - * Composite disposable for API call - */ - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - - /** - * Duration of the leaderboard API - */ - private String duration; - - /** - * Category of the Leaderboard API - */ - private String category; - - /** - * Page size of the leaderboard API - */ - private int limit = PAGE_SIZE; - - /** - * offset for the leaderboard API - */ - private int offset = START_OFFSET; - - /** - * Set initial User Rank to 0 - */ - private int userRank; - - /** - * This variable represents if user wants to scroll to his rank or not - */ - private boolean scrollToRank; - - private String userName; - - private FragmentLeaderboardBinding binding; - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (getArguments() != null) { - userName = getArguments().getString(ProfileActivity.KEY_USERNAME); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentLeaderboardBinding.inflate(inflater, container, false); - - hideLayouts(); - - // Leaderboard currently unimplemented in Beta flavor. Skip all API calls and disable menu - if(ConfigUtils.isBetaFlavour()) { - binding.progressBar.setVisibility(View.GONE); - binding.scroll.setVisibility(View.GONE); - return binding.getRoot(); - } - - binding.progressBar.setVisibility(View.VISIBLE); - setSpinners(); - - /** - * This array is for the duration filter, we have three filters weekly, yearly and all-time - * each filter have a key and value pair, the value represents the param of the API - */ - String[] durationValues = getContext().getResources().getStringArray(R.array.leaderboard_duration_values); - - /** - * This array is for the category filter, we have three filters upload, used and nearby - * each filter have a key and value pair, the value represents the param of the API - */ - String[] categoryValues = getContext().getResources().getStringArray(R.array.leaderboard_category_values); - - duration = durationValues[0]; - category = categoryValues[0]; - - setLeaderboard(duration, category, limit, offset); - - binding.durationSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - - duration = durationValues[binding.durationSpinner.getSelectedItemPosition()]; - refreshLeaderboard(); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - } - }); - - binding.categorySpinner.setOnItemSelectedListener(new OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - category = categoryValues[binding.categorySpinner.getSelectedItemPosition()]; - refreshLeaderboard(); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - } - }); - - - binding.scroll.setOnClickListener(view -> scrollToUserRank()); - - - return binding.getRoot(); - } - - @Override - public void setMenuVisibility(boolean visible) { - super.setMenuVisibility(visible); - - // Whenever this fragment is revealed in a menu, - // notify Beta users the page data is unavailable - if(ConfigUtils.isBetaFlavour() && visible) { - Context ctx = null; - if(getContext() != null) { - ctx = getContext(); - } else if(getView() != null && getView().getContext() != null) { - ctx = getView().getContext(); - } - if(ctx != null) { - Toast.makeText(ctx, - R.string.leaderboard_unavailable_beta, - Toast.LENGTH_LONG).show(); - } - } - } - - /** - * Refreshes the leaderboard list - */ - private void refreshLeaderboard() { - scrollToRank = false; - if (viewModel != null) { - viewModel.refresh(duration, category, limit, offset); - setLeaderboard(duration, category, limit, offset); - } - } - - /** - * Performs Auto Scroll to the User's Rank - * We use userRank+1 to load one extra user and prevent overlapping of my rank button - * If you are viewing the leaderboard below userRank, it scrolls to the user rank at the top - */ - private void scrollToUserRank() { - - if(userRank==0){ - Toast.makeText(getContext(),R.string.no_achievements_yet,Toast.LENGTH_SHORT).show(); - }else { - if (binding == null) { - return; - } - if (Objects.requireNonNull(binding.leaderboardList.getAdapter()).getItemCount() - > userRank + 1) { - binding.leaderboardList.smoothScrollToPosition(userRank + 1); - } else { - if (viewModel != null) { - viewModel.refresh(duration, category, userRank + 1, 0); - setLeaderboard(duration, category, userRank + 1, 0); - scrollToRank = true; - } - } - } - - } - - /** - * Set the spinners for the leaderboard filters - */ - private void setSpinners() { - ArrayAdapter categoryAdapter = ArrayAdapter.createFromResource(getContext(), - R.array.leaderboard_categories, android.R.layout.simple_spinner_item); - categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - binding.categorySpinner.setAdapter(categoryAdapter); - - ArrayAdapter durationAdapter = ArrayAdapter.createFromResource(getContext(), - R.array.leaderboard_durations, android.R.layout.simple_spinner_item); - durationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - binding.durationSpinner.setAdapter(durationAdapter); - } - - /** - * To call the API to get results - * which then sets the views using setLeaderboardUser method - */ - private void setLeaderboard(String duration, String category, int limit, int offset) { - if (checkAccount()) { - try { - compositeDisposable.add(okHttpJsonApiClient - .getLeaderboard(Objects.requireNonNull(userName), - duration, category, null, null) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null && response.getStatus() == 200) { - userRank = response.getRank(); - setViews(response, duration, category, limit, offset); - } - }, - t -> { - Timber.e(t, "Fetching leaderboard statistics failed"); - onError(); - } - )); - } - catch (Exception e){ - Timber.d(e+"success"); - } - } - } - - /** - * Set the views - * @param response Leaderboard Response Object - */ - private void setViews(LeaderboardResponse response, String duration, String category, int limit, int offset) { - viewModel = new ViewModelProvider(this, viewModelFactory).get(LeaderboardListViewModel.class); - viewModel.setParams(duration, category, limit, offset); - LeaderboardListAdapter leaderboardListAdapter = new LeaderboardListAdapter(); - UserDetailAdapter userDetailAdapter= new UserDetailAdapter(response); - MergeAdapter mergeAdapter = new MergeAdapter(userDetailAdapter, leaderboardListAdapter); - LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext()); - binding.leaderboardList.setLayoutManager(linearLayoutManager); - binding.leaderboardList.setAdapter(mergeAdapter); - viewModel.getListLiveData().observe(getViewLifecycleOwner(), leaderboardListAdapter::submitList); - viewModel.getProgressLoadStatus().observe(getViewLifecycleOwner(), status -> { - if (Objects.requireNonNull(status).equalsIgnoreCase(LOADING)) { - showProgressBar(); - } else if (status.equalsIgnoreCase(LOADED)) { - hideProgressBar(); - if (scrollToRank) { - binding.leaderboardList.smoothScrollToPosition(userRank + 1); - } - } - }); - } - - /** - * to hide progressbar - */ - private void hideProgressBar() { - if (binding != null) { - binding.progressBar.setVisibility(View.GONE); - binding.categorySpinner.setVisibility(View.VISIBLE); - binding.durationSpinner.setVisibility(View.VISIBLE); - binding.scroll.setVisibility(View.VISIBLE); - binding.leaderboardList.setVisibility(View.VISIBLE); - } - } - - /** - * to show progressbar - */ - private void showProgressBar() { - if (binding != null) { - binding.progressBar.setVisibility(View.VISIBLE); - binding.scroll.setVisibility(View.INVISIBLE); - } - } - - /** - * used to hide the layouts while fetching results from api - */ - private void hideLayouts(){ - binding.categorySpinner.setVisibility(View.INVISIBLE); - binding.durationSpinner.setVisibility(View.INVISIBLE); - binding.leaderboardList.setVisibility(View.INVISIBLE); - } - - /** - * check to ensure that user is logged in - * @return - */ - private boolean checkAccount(){ - Account currentAccount = sessionManager.getCurrentAccount(); - if (currentAccount == null) { - Timber.d("Current account is null"); - ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.user_not_logged_in)); - sessionManager.forceLogin(getActivity()); - return false; - } - return true; - } - - /** - * Shows a generic error toast when error occurs while loading leaderboard - */ - private void onError() { - ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.error_occurred)); - if (binding!=null) { - binding.progressBar.setVisibility(View.GONE); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - compositeDisposable.clear(); - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt new file mode 100644 index 000000000..e77c24c8d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt @@ -0,0 +1,319 @@ +package fr.free.nrw.commons.profile.leaderboard + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.MergeAdapter +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.databinding.FragmentLeaderboardBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.profile.ProfileActivity +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADED +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADING +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.START_OFFSET +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.utils.ViewUtil.showLongToast +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import java.util.Objects +import javax.inject.Inject + +/** + * This class extends the CommonsDaggerSupportFragment and creates leaderboard fragment + */ +class LeaderboardFragment : CommonsDaggerSupportFragment() { + @Inject + lateinit var sessionManager: SessionManager + + @Inject + lateinit var okHttpJsonApiClient: OkHttpJsonApiClient + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private var viewModel: LeaderboardListViewModel? = null + private var duration: String? = null + private var category: String? = null + private val limit: Int = PAGE_SIZE + private val offset: Int = START_OFFSET + private var userRank = 0 + private var scrollToRank = false + private var userName: String? = null + private var binding: FragmentLeaderboardBinding? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { userName = it.getString(ProfileActivity.KEY_USERNAME) } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentLeaderboardBinding.inflate(inflater, container, false) + + hideLayouts() + + // Leaderboard currently unimplemented in Beta flavor. Skip all API calls and disable menu + if (isBetaFlavour) { + binding!!.progressBar.visibility = View.GONE + binding!!.scroll.visibility = View.GONE + return binding!!.root + } + + binding!!.progressBar.visibility = View.VISIBLE + setSpinners() + + /* + * This array is for the duration filter, we have three filters weekly, yearly and all-time + * each filter have a key and value pair, the value represents the param of the API + */ + val durationValues = requireContext().resources + .getStringArray(R.array.leaderboard_duration_values) + duration = durationValues[0] + + /* + * This array is for the category filter, we have three filters upload, used and nearby + * each filter have a key and value pair, the value represents the param of the API + */ + val categoryValues = requireContext().resources + .getStringArray(R.array.leaderboard_category_values) + category = categoryValues[0] + + setLeaderboard(duration, category, limit, offset) + + with(binding!!) { + durationSpinner.onItemSelectedListener = SelectionListener { + duration = durationValues[durationSpinner.selectedItemPosition] + refreshLeaderboard() + } + + categorySpinner.onItemSelectedListener = SelectionListener { + category = categoryValues[categorySpinner.selectedItemPosition] + refreshLeaderboard() + } + + scroll.setOnClickListener { scrollToUserRank() } + + return root + } + } + + override fun setMenuVisibility(visible: Boolean) { + super.setMenuVisibility(visible) + + // Whenever this fragment is revealed in a menu, + // notify Beta users the page data is unavailable + if (isBetaFlavour && visible) { + val ctx: Context? = if (context != null) { + context + } else if (view != null && requireView().context != null) { + requireView().context + } else { + null + } + + ctx?.let { + Toast.makeText(it, R.string.leaderboard_unavailable_beta, Toast.LENGTH_LONG).show() + } + } + } + + /** + * Refreshes the leaderboard list + */ + private fun refreshLeaderboard() { + scrollToRank = false + viewModel?.let { + it.refresh(duration, category, limit, offset) + setLeaderboard(duration, category, limit, offset) + } + } + + /** + * Performs Auto Scroll to the User's Rank + * We use userRank+1 to load one extra user and prevent overlapping of my rank button + * If you are viewing the leaderboard below userRank, it scrolls to the user rank at the top + */ + private fun scrollToUserRank() { + if (userRank == 0) { + Toast.makeText(context, R.string.no_achievements_yet, Toast.LENGTH_SHORT).show() + } else { + if (binding == null) { + return + } + val itemCount = binding?.leaderboardList?.adapter?.itemCount ?: 0 + if (itemCount > userRank + 1) { + binding!!.leaderboardList.smoothScrollToPosition(userRank + 1) + } else { + viewModel?.let { + it.refresh(duration, category, userRank + 1, 0) + setLeaderboard(duration, category, userRank + 1, 0) + scrollToRank = true + } + } + } + } + + /** + * Set the spinners for the leaderboard filters + */ + private fun setSpinners() { + val categoryAdapter = ArrayAdapter.createFromResource( + requireContext(), + R.array.leaderboard_categories, android.R.layout.simple_spinner_item + ) + categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding!!.categorySpinner.adapter = categoryAdapter + + val durationAdapter = ArrayAdapter.createFromResource( + requireContext(), + R.array.leaderboard_durations, android.R.layout.simple_spinner_item + ) + durationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding!!.durationSpinner.adapter = durationAdapter + } + + /** + * To call the API to get results + * which then sets the views using setLeaderboardUser method + */ + private fun setLeaderboard(duration: String?, category: String?, limit: Int, offset: Int) { + if (checkAccount()) { + try { + compositeDisposable.add( + okHttpJsonApiClient.getLeaderboard( + Objects.requireNonNull(userName), + duration, category, null, null + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response: LeaderboardResponse? -> + if (response != null && response.status == 200) { + userRank = response.rank!! + setViews(response, duration, category, limit, offset) + } + }, + { t: Throwable? -> + Timber.e(t, "Fetching leaderboard statistics failed") + onError() + } + )) + } catch (e: Exception) { + Timber.d(e, "success") + } + } + } + + /** + * Set the views + * @param response Leaderboard Response Object + */ + private fun setViews( + response: LeaderboardResponse, + duration: String?, + category: String?, + limit: Int, + offset: Int + ) { + viewModel = ViewModelProvider(this, viewModelFactory).get( + LeaderboardListViewModel::class.java + ) + viewModel!!.setParams(duration, category, limit, offset) + val leaderboardListAdapter = LeaderboardListAdapter() + val userDetailAdapter = UserDetailAdapter(response) + val mergeAdapter = MergeAdapter(userDetailAdapter, leaderboardListAdapter) + val linearLayoutManager = LinearLayoutManager(context) + binding!!.leaderboardList.layoutManager = linearLayoutManager + binding!!.leaderboardList.adapter = mergeAdapter + viewModel!!.listLiveData.observe(viewLifecycleOwner, leaderboardListAdapter::submitList) + + viewModel!!.progressLoadStatus.observe(viewLifecycleOwner) { status -> + when (status) { + LOADING -> { + showProgressBar() + } + LOADED -> { + hideProgressBar() + if (scrollToRank) { + binding!!.leaderboardList.smoothScrollToPosition(userRank + 1) + } + } + } + } + } + + /** + * to hide progressbar + */ + private fun hideProgressBar() = binding?.let { + it.progressBar.visibility = View.GONE + it.categorySpinner.visibility = View.VISIBLE + it.durationSpinner.visibility = View.VISIBLE + it.scroll.visibility = View.VISIBLE + it.leaderboardList.visibility = View.VISIBLE + } + + /** + * to show progressbar + */ + private fun showProgressBar() = binding?.let { + it.progressBar.visibility = View.VISIBLE + it.scroll.visibility = View.INVISIBLE + } + + /** + * used to hide the layouts while fetching results from api + */ + private fun hideLayouts() = binding?.let { + it.categorySpinner.visibility = View.INVISIBLE + it.durationSpinner.visibility = View.INVISIBLE + it.leaderboardList.visibility = View.INVISIBLE + } + + /** + * check to ensure that user is logged in + */ + private fun checkAccount() = if (sessionManager.currentAccount == null) { + Timber.d("Current account is null") + showLongToast(requireActivity(), resources.getString(R.string.user_not_logged_in)) + sessionManager.forceLogin(requireActivity()) + false + } else { + true + } + + /** + * Shows a generic error toast when error occurs while loading leaderboard + */ + private fun onError() { + showLongToast(requireActivity(), resources.getString(R.string.error_occurred)) + binding?.let { it.progressBar.visibility = View.GONE } + } + + override fun onDestroy() { + super.onDestroy() + compositeDisposable.clear() + binding = null + } + + private class SelectionListener(private val handler: () -> Unit): AdapterView.OnItemSelectedListener { + override fun onItemSelected(adapterView: AdapterView<*>?, view: View, i: Int, l: Long) = + handler() + + override fun onNothingSelected(p0: AdapterView<*>?) = Unit + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.java deleted file mode 100644 index 5558f3d9e..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.java +++ /dev/null @@ -1,137 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.DiffUtil.ItemCallback; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * This class represents the leaderboard API response sub part of i.e. leaderboard list - * The leaderboard list will contain the ranking of the users from 1 to n, - * avatars, username and count in the selected category. - */ -public class LeaderboardList { - - /** - * Username of the user - * Example value - Syced - */ - @SerializedName("username") - @Expose - private String username; - - /** - * Count in the category - * Example value - 10 - */ - @SerializedName("category_count") - @Expose - private Integer categoryCount; - - /** - * URL of the avatar of user - * Example value = https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png - */ - @SerializedName("avatar") - @Expose - private String avatar; - - /** - * Rank of the user - * Example value - 1 - */ - @SerializedName("rank") - @Expose - private Integer rank; - - /** - * @return the username of the user in the leaderboard list - */ - public String getUsername() { - return username; - } - - /** - * Sets the username of the user in the leaderboard list - */ - public void setUsername(String username) { - this.username = username; - } - - /** - * @return the category count of the user in the leaderboard list - */ - public Integer getCategoryCount() { - return categoryCount; - } - - /** - * Sets the category count of the user in the leaderboard list - */ - public void setCategoryCount(Integer categoryCount) { - this.categoryCount = categoryCount; - } - - /** - * @return the avatar of the user in the leaderboard list - */ - public String getAvatar() { - return avatar; - } - - /** - * Sets the avatar of the user in the leaderboard list - */ - public void setAvatar(String avatar) { - this.avatar = avatar; - } - - /** - * @return the rank of the user in the leaderboard list - */ - public Integer getRank() { - return rank; - } - - /** - * Sets the rank of the user in the leaderboard list - */ - public void setRank(Integer rank) { - this.rank = rank; - } - - - /** - * This method checks for the diff in the callbacks for paged lists - */ - public static DiffUtil.ItemCallback DIFF_CALLBACK = - new ItemCallback() { - @Override - public boolean areItemsTheSame(@NonNull LeaderboardList oldItem, - @NonNull LeaderboardList newItem) { - return newItem == oldItem; - } - - @Override - public boolean areContentsTheSame(@NonNull LeaderboardList oldItem, - @NonNull LeaderboardList newItem) { - return newItem.getRank().equals(oldItem.getRank()); - } - }; - - /** - * Returns true if two objects are equal, false otherwise - * @param obj - * @return - */ - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - - LeaderboardList leaderboardList = (LeaderboardList) obj; - return leaderboardList.getRank().equals(this.getRank()); - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.kt new file mode 100644 index 000000000..dc6d93e15 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.kt @@ -0,0 +1,61 @@ +package fr.free.nrw.commons.profile.leaderboard + +import androidx.recyclerview.widget.DiffUtil +import com.google.gson.annotations.SerializedName + +/** + * This class represents the leaderboard API response sub part of i.e. leaderboard list + * The leaderboard list will contain the ranking of the users from 1 to n, + * avatars, username and count in the selected category. + */ +data class LeaderboardList ( + @SerializedName("username") + var username: String? = null, + @SerializedName("category_count") + var categoryCount: Int? = null, + @SerializedName("avatar") + var avatar: String? = null, + @SerializedName("rank") + var rank: Int? = null +) { + + /** + * Returns true if two objects are equal, false otherwise + * @param other + * @return + */ + override fun equals(other: Any?): Boolean { + if (other === this) { + return true + } + + val leaderboardList = other as LeaderboardList + return leaderboardList.rank == rank + } + + override fun hashCode(): Int { + var result = username?.hashCode() ?: 0 + result = 31 * result + (categoryCount ?: 0) + result = 31 * result + (avatar?.hashCode() ?: 0) + result = 31 * result + (rank ?: 0) + return result + } + + companion object { + /** + * This method checks for the diff in the callbacks for paged lists + */ + var DIFF_CALLBACK: DiffUtil.ItemCallback = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: LeaderboardList, + newItem: LeaderboardList + ): Boolean = newItem === oldItem + + override fun areContentsTheSame( + oldItem: LeaderboardList, + newItem: LeaderboardList + ): Boolean = newItem.rank == oldItem.rank + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.java deleted file mode 100644 index 9af24159a..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.java +++ /dev/null @@ -1,93 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - - -import android.app.Activity; -import android.content.Context; -import android.net.Uri; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.paging.PagedListAdapter; -import androidx.recyclerview.widget.RecyclerView; -import com.facebook.drawee.view.SimpleDraweeView; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.profile.ProfileActivity; - -/** - * This class extends RecyclerView.Adapter and creates the List section of the leaderboard - */ -public class LeaderboardListAdapter extends PagedListAdapter { - - public LeaderboardListAdapter() { - super(LeaderboardList.DIFF_CALLBACK); - } - - public class ListViewHolder extends RecyclerView.ViewHolder { - TextView rank; - SimpleDraweeView avatar; - TextView username; - TextView count; - - public ListViewHolder(View itemView) { - super(itemView); - this.rank = itemView.findViewById(R.id.user_rank); - this.avatar = itemView.findViewById(R.id.user_avatar); - this.username = itemView.findViewById(R.id.user_name); - this.count = itemView.findViewById(R.id.user_count); - } - - /** - * This method will return the Context - * @return Context - */ - public Context getContext() { - return itemView.getContext(); - } - } - - /** - * Overrides the onCreateViewHolder and inflates the recyclerview list item layout - * @param parent - * @param viewType - * @return - */ - @NonNull - @Override - public LeaderboardListAdapter.ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.leaderboard_list_element, parent, false); - - return new ListViewHolder(view); - } - - /** - * Overrides the onBindViewHolder Set the view at the specific position with the specific value - * @param holder - * @param position - */ - @Override - public void onBindViewHolder(@NonNull LeaderboardListAdapter.ListViewHolder holder, int position) { - TextView rank = holder.rank; - SimpleDraweeView avatar = holder.avatar; - TextView username = holder.username; - TextView count = holder.count; - - rank.setText(getItem(position).getRank().toString()); - - avatar.setImageURI(Uri.parse(getItem(position).getAvatar())); - username.setText(getItem(position).getUsername()); - count.setText(getItem(position).getCategoryCount().toString()); - - /* - Now that we have our in app profile-section, lets take the user there - */ - holder.itemView.setOnClickListener(view -> { - if (view.getContext() instanceof ProfileActivity) { - ((Activity) (view.getContext())).finish(); - } - ProfileActivity.startYourself(view.getContext(), getItem(position).getUsername(), true); - }); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt new file mode 100644 index 000000000..c7bccf950 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt @@ -0,0 +1,64 @@ +package fr.free.nrw.commons.profile.leaderboard + +import android.app.Activity +import android.net.Uri +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.facebook.drawee.view.SimpleDraweeView +import fr.free.nrw.commons.R +import fr.free.nrw.commons.profile.ProfileActivity +import fr.free.nrw.commons.profile.leaderboard.LeaderboardList.Companion.DIFF_CALLBACK +import fr.free.nrw.commons.profile.leaderboard.LeaderboardListAdapter.ListViewHolder + + +/** + * This class extends RecyclerView.Adapter and creates the List section of the leaderboard + */ +class LeaderboardListAdapter : PagedListAdapter(DIFF_CALLBACK) { + inner class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var rank: TextView? = itemView.findViewById(R.id.user_rank) + var avatar: SimpleDraweeView? = itemView.findViewById(R.id.user_avatar) + var username: TextView? = itemView.findViewById(R.id.user_name) + var count: TextView? = itemView.findViewById(R.id.user_count) + } + + /** + * Overrides the onCreateViewHolder and inflates the recyclerview list item layout + * @param parent + * @param viewType + * @return + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder = + ListViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.leaderboard_list_element, parent, false) + ) + + /** + * Overrides the onBindViewHolder Set the view at the specific position with the specific value + * @param holder + * @param position + */ + override fun onBindViewHolder(holder: ListViewHolder, position: Int) = with (holder) { + val item = getItem(position)!! + + rank?.text = item.rank.toString() + avatar?.setImageURI(Uri.parse(item.avatar)) + username?.text = item.username + count?.text = item.categoryCount.toString() + + /* + Now that we have our in app profile-section, lets take the user there + */ + itemView.setOnClickListener { view: View -> + if (view.context is ProfileActivity) { + ((view.context) as Activity).finish() + } + ProfileActivity.startYourself(view.context, item.username, true) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.java deleted file mode 100644 index 909b4f646..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.java +++ /dev/null @@ -1,107 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Transformations; -import androidx.lifecycle.ViewModel; -import androidx.paging.LivePagedListBuilder; -import androidx.paging.PagedList; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.disposables.CompositeDisposable; - -/** - * Extends the ViewModel class and creates the LeaderboardList View Model - */ -public class LeaderboardListViewModel extends ViewModel { - - private DataSourceFactory dataSourceFactory; - private LiveData> listLiveData; - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - private LiveData progressLoadStatus = new MutableLiveData<>(); - - /** - * Constructor for a new LeaderboardListViewModel - * @param okHttpJsonApiClient - * @param sessionManager - */ - public LeaderboardListViewModel(OkHttpJsonApiClient okHttpJsonApiClient, SessionManager - sessionManager) { - - dataSourceFactory = new DataSourceFactory(okHttpJsonApiClient, - compositeDisposable, sessionManager); - initializePaging(); - } - - - /** - * Initialises the paging - */ - private void initializePaging() { - - PagedList.Config pagedListConfig = - new PagedList.Config.Builder() - .setEnablePlaceholders(false) - .setInitialLoadSizeHint(PAGE_SIZE) - .setPageSize(PAGE_SIZE).build(); - - listLiveData = new LivePagedListBuilder<>(dataSourceFactory, pagedListConfig) - .build(); - - progressLoadStatus = Transformations - .switchMap(dataSourceFactory.getMutableLiveData(), DataSourceClass::getProgressLiveStatus); - - } - - /** - * Refreshes the paged list with the new params and starts the loading of new data - * @param duration - * @param category - * @param limit - * @param offset - */ - public void refresh(String duration, String category, int limit, int offset) { - dataSourceFactory.setDuration(duration); - dataSourceFactory.setCategory(category); - dataSourceFactory.setLimit(limit); - dataSourceFactory.setOffset(offset); - dataSourceFactory.getMutableLiveData().getValue().invalidate(); - } - - /** - * Sets the new params for the paged list API calls - * @param duration - * @param category - * @param limit - * @param offset - */ - public void setParams(String duration, String category, int limit, int offset) { - dataSourceFactory.setDuration(duration); - dataSourceFactory.setCategory(category); - dataSourceFactory.setLimit(limit); - dataSourceFactory.setOffset(offset); - } - - /** - * @return the loading status of paged list - */ - public LiveData getProgressLoadStatus() { - return progressLoadStatus; - } - - /** - * @return the paged list with live data - */ - public LiveData> getListLiveData() { - return listLiveData; - } - - @Override - protected void onCleared() { - super.onCleared(); - compositeDisposable.clear(); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.kt new file mode 100644 index 000000000..7d649b67b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.kt @@ -0,0 +1,54 @@ +package fr.free.nrw.commons.profile.leaderboard + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.map +import androidx.lifecycle.switchMap +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE + +/** + * Extends the ViewModel class and creates the LeaderboardList View Model + */ +class LeaderboardListViewModel( + okHttpJsonApiClient: OkHttpJsonApiClient, + sessionManager: SessionManager +) : ViewModel() { + private val dataSourceFactory = DataSourceFactory(okHttpJsonApiClient, sessionManager) + + val listLiveData: LiveData> = LivePagedListBuilder( + dataSourceFactory, + PagedList.Config.Builder() + .setEnablePlaceholders(false) + .setInitialLoadSizeHint(PAGE_SIZE) + .setPageSize(PAGE_SIZE).build() + ).build() + + val progressLoadStatus: LiveData = + dataSourceFactory.mutableLiveData.switchMap { it.progressLiveStatus } + + /** + * Refreshes the paged list with the new params and starts the loading of new data + */ + fun refresh(duration: String?, category: String?, limit: Int, offset: Int) { + dataSourceFactory.duration = duration + dataSourceFactory.category = category + dataSourceFactory.limit = limit + dataSourceFactory.offset = offset + dataSourceFactory.mutableLiveData.value!!.invalidate() + } + + /** + * Sets the new params for the paged list API calls + */ + fun setParams(duration: String?, category: String?, limit: Int, offset: Int) { + dataSourceFactory.duration = duration + dataSourceFactory.category = category + dataSourceFactory.limit = limit + dataSourceFactory.offset = offset + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.java deleted file mode 100644 index 34294fca9..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.java +++ /dev/null @@ -1,237 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import java.util.List; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * GSON Response Class for Leaderboard API response - */ -public class LeaderboardResponse { - - /** - * Status Code returned from the API - * Example value - 200 - */ - @SerializedName("status") - @Expose - private Integer status; - - /** - * Username returned from the API - * Example value - Syced - */ - @SerializedName("username") - @Expose - private String username; - - /** - * Category count returned from the API - * Example value - 10 - */ - @SerializedName("category_count") - @Expose - private Integer categoryCount; - - /** - * Limit returned from the API - * Example value - 10 - */ - @SerializedName("limit") - @Expose - private int limit; - - /** - * Avatar returned from the API - * Example value - https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png - */ - @SerializedName("avatar") - @Expose - private String avatar; - - /** - * Offset returned from the API - * Example value - 0 - */ - @SerializedName("offset") - @Expose - private int offset; - - /** - * Duration returned from the API - * Example value - yearly - */ - @SerializedName("duration") - @Expose - private String duration; - - /** - * Leaderboard list returned from the API - * Example value - [{ - * "username": "Fæ", - * "category_count": 107147, - * "avatar": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png", - * "rank": 1 - * }] - */ - @SerializedName("leaderboard_list") - @Expose - private List leaderboardList = null; - - /** - * Category returned from the API - * Example value - upload - */ - @SerializedName("category") - @Expose - private String category; - - /** - * Rank returned from the API - * Example value - 1 - */ - @SerializedName("rank") - @Expose - private Integer rank; - - /** - * @return the status code - */ - public Integer getStatus() { - return status; - } - - /** - * Sets the status code - */ - public void setStatus(Integer status) { - this.status = status; - } - - /** - * @return the username - */ - public String getUsername() { - return username; - } - - /** - * Sets the username - */ - public void setUsername(String username) { - this.username = username; - } - - /** - * @return the category count - */ - public Integer getCategoryCount() { - return categoryCount; - } - - /** - * Sets the category count - */ - public void setCategoryCount(Integer categoryCount) { - this.categoryCount = categoryCount; - } - - /** - * @return the limit - */ - public int getLimit() { - return limit; - } - - /** - * Sets the limit - */ - public void setLimit(int limit) { - this.limit = limit; - } - - /** - * @return the avatar - */ - public String getAvatar() { - return avatar; - } - - /** - * Sets the avatar - */ - public void setAvatar(String avatar) { - this.avatar = avatar; - } - - /** - * @return the offset - */ - public int getOffset() { - return offset; - } - - /** - * Sets the offset - */ - public void setOffset(int offset) { - this.offset = offset; - } - - /** - * @return the duration - */ - public String getDuration() { - return duration; - } - - /** - * Sets the duration - */ - public void setDuration(String duration) { - this.duration = duration; - } - - /** - * @return the leaderboard list - */ - public List getLeaderboardList() { - return leaderboardList; - } - - /** - * Sets the leaderboard list - */ - public void setLeaderboardList(List leaderboardList) { - this.leaderboardList = leaderboardList; - } - - /** - * @return the category - */ - public String getCategory() { - return category; - } - - /** - * Sets the category - */ - public void setCategory(String category) { - this.category = category; - } - - /** - * @return the rank - */ - public Integer getRank() { - return rank; - } - - /** - * Sets the rank - */ - public void setRank(Integer rank) { - this.rank = rank; - } - -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.kt new file mode 100644 index 000000000..8be342650 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.kt @@ -0,0 +1,19 @@ +package fr.free.nrw.commons.profile.leaderboard + +import com.google.gson.annotations.SerializedName + +/** + * GSON Response Class for Leaderboard API response + */ +data class LeaderboardResponse( + @SerializedName("status") var status: Int? = null, + @SerializedName("username") var username: String? = null, + @SerializedName("category_count") var categoryCount: Int? = null, + @SerializedName("limit") var limit: Int = 0, + @SerializedName("avatar") var avatar: String? = null, + @SerializedName("offset") var offset: Int = 0, + @SerializedName("duration") var duration: String? = null, + @SerializedName("leaderboard_list") var leaderboardList: List? = null, + @SerializedName("category") var category: String? = null, + @SerializedName("rank") var rank: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.java deleted file mode 100644 index 15449a488..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.java +++ /dev/null @@ -1,77 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * GSON Response Class for Update Avatar API response - */ -public class UpdateAvatarResponse { - - /** - * Status Code returned from the API - * Example value - 200 - */ - @SerializedName("status") - @Expose - private String status; - - /** - * Message returned from the API - * Example value - Avatar Updated - */ - @SerializedName("message") - @Expose - private String message; - - /** - * Username returned from the API - * Example value - Syced - */ - @SerializedName("user") - @Expose - private String user; - - /** - * @return the status code - */ - public String getStatus() { - return status; - } - - /** - * Sets the status code - */ - public void setStatus(String status) { - this.status = status; - } - - /** - * @return the message - */ - public String getMessage() { - return message; - } - - /** - * Sets the message - */ - public void setMessage(String message) { - this.message = message; - } - - /** - * @return the username - */ - public String getUser() { - return user; - } - - /** - * Sets the username - */ - public void setUser(String user) { - this.user = user; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.kt new file mode 100644 index 000000000..75fb8f268 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.kt @@ -0,0 +1,10 @@ +package fr.free.nrw.commons.profile.leaderboard + +/** + * GSON Response Class for Update Avatar API response + */ +data class UpdateAvatarResponse( + var status: String? = null, + var message: String? = null, + var user: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.java deleted file mode 100644 index 75b9de938..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.java +++ /dev/null @@ -1,126 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; -import android.net.Uri; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import com.facebook.drawee.view.SimpleDraweeView; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; - - -/** - * This class extends RecyclerView.Adapter and creates the UserDetail section of the leaderboard - */ -public class UserDetailAdapter extends RecyclerView.Adapter { - - private LeaderboardResponse leaderboardResponse; - - /** - * Stores the username of currently logged in user. - */ - private String currentlyLoggedInUserName = null; - - public UserDetailAdapter(LeaderboardResponse leaderboardResponse) { - this.leaderboardResponse = leaderboardResponse; - } - - public class DataViewHolder extends RecyclerView.ViewHolder { - - private TextView rank; - private SimpleDraweeView avatar; - private TextView username; - private TextView count; - - public DataViewHolder(@NonNull View itemView) { - super(itemView); - this.rank = itemView.findViewById(R.id.rank); - this.avatar = itemView.findViewById(R.id.avatar); - this.username = itemView.findViewById(R.id.username); - this.count = itemView.findViewById(R.id.count); - } - - /** - * This method will return the Context - * @return Context - */ - public Context getContext() { - return itemView.getContext(); - } - } - - /** - * Overrides the onCreateViewHolder and sets the view with leaderboard user element layout - * @param parent - * @param viewType - * @return - */ - @NonNull - @Override - public UserDetailAdapter.DataViewHolder onCreateViewHolder(@NonNull ViewGroup parent, - int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.leaderboard_user_element, parent, false); - return new DataViewHolder(view); - } - - /** - * Overrides the onBindViewHolder Set the view at the specific position with the specific value - * @param holder - * @param position - */ - @Override - public void onBindViewHolder(@NonNull UserDetailAdapter.DataViewHolder holder, int position) { - TextView rank = holder.rank; - SimpleDraweeView avatar = holder.avatar; - TextView username = holder.username; - TextView count = holder.count; - - rank.setText(String.format("%s %d", - holder.getContext().getResources().getString(R.string.rank_prefix), - leaderboardResponse.getRank())); - - avatar.setImageURI( - Uri.parse(leaderboardResponse.getAvatar())); - username.setText(leaderboardResponse.getUsername()); - count.setText(String.format("%s %d", - holder.getContext().getResources().getString(R.string.count_prefix), - leaderboardResponse.getCategoryCount())); - - // When user tap on avatar shows the toast on how to change avatar - // fixing: https://github.com/commons-app/apps-android-commons/issues/47747 - if (currentlyLoggedInUserName == null) { - // If the current login username has not been fetched yet, then fetch it. - final AccountManager accountManager = AccountManager.get(username.getContext()); - final Account[] allAccounts = accountManager.getAccountsByType( - BuildConfig.ACCOUNT_TYPE); - if (allAccounts.length != 0) { - currentlyLoggedInUserName = allAccounts[0].name; - } - } - if (currentlyLoggedInUserName != null && currentlyLoggedInUserName.equals( - leaderboardResponse.getUsername())) { - - avatar.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Toast.makeText(v.getContext(), - R.string.set_up_avatar_toast_string, - Toast.LENGTH_LONG).show(); - } - }); - } - } - - @Override - public int getItemCount() { - return 1; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt new file mode 100644 index 000000000..34fd5ab58 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt @@ -0,0 +1,91 @@ +package fr.free.nrw.commons.profile.leaderboard + +import android.accounts.AccountManager +import android.net.Uri +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.recyclerview.widget.RecyclerView +import com.facebook.drawee.view.SimpleDraweeView +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.R +import fr.free.nrw.commons.profile.leaderboard.UserDetailAdapter.DataViewHolder +import java.util.Locale + +/** + * This class extends RecyclerView.Adapter and creates the UserDetail section of the leaderboard + */ +class UserDetailAdapter(private val leaderboardResponse: LeaderboardResponse) : + RecyclerView.Adapter() { + /** + * Stores the username of currently logged in user. + */ + private var currentlyLoggedInUserName: String? = null + + class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val rank: TextView = itemView.findViewById(R.id.rank) + val avatar: SimpleDraweeView = itemView.findViewById(R.id.avatar) + val username: TextView = itemView.findViewById(R.id.username) + val count: TextView = itemView.findViewById(R.id.count) + } + + /** + * Overrides the onCreateViewHolder and sets the view with leaderboard user element layout + * @param parent + * @param viewType + * @return + */ + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): DataViewHolder = DataViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.leaderboard_user_element, parent, false) + ) + + /** + * Overrides the onBindViewHolder Set the view at the specific position with the specific value + * @param holder + * @param position + */ + override fun onBindViewHolder(holder: DataViewHolder, position: Int) = with(holder) { + val resources = itemView.context.resources + + avatar.setImageURI(Uri.parse(leaderboardResponse.avatar)) + username.text = leaderboardResponse.username + rank.text = String.format( + Locale.getDefault(), + "%s %d", + resources.getString(R.string.rank_prefix), + leaderboardResponse.rank + ) + count.text = String.format( + Locale.getDefault(), + "%s %d", + resources.getString(R.string.count_prefix), + leaderboardResponse.categoryCount + ) + + // When user tap on avatar shows the toast on how to change avatar + // fixing: https://github.com/commons-app/apps-android-commons/issues/47747 + if (currentlyLoggedInUserName == null) { + // If the current login username has not been fetched yet, then fetch it. + val accountManager = AccountManager.get(itemView.context) + val allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE) + if (allAccounts.isNotEmpty()) { + currentlyLoggedInUserName = allAccounts[0].name + } + } + if (currentlyLoggedInUserName != null && currentlyLoggedInUserName == leaderboardResponse.username) { + avatar.setOnClickListener { v: View -> + Toast.makeText( + v.context, R.string.set_up_avatar_toast_string, Toast.LENGTH_LONG + ).show() + } + } + } + + override fun getItemCount(): Int = 1 +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.java deleted file mode 100644 index fece77110..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.java +++ /dev/null @@ -1,41 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import androidx.annotation.NonNull; -import androidx.lifecycle.ViewModel; -import androidx.lifecycle.ViewModelProvider; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import javax.inject.Inject; - -/** - * This class extends the ViewModelProvider.Factory and creates a ViewModelFactory class - * for leaderboardListViewModel - */ -public class ViewModelFactory implements ViewModelProvider.Factory { - - private OkHttpJsonApiClient okHttpJsonApiClient; - private SessionManager sessionManager; - - - @Inject - public ViewModelFactory(OkHttpJsonApiClient okHttpJsonApiClient, SessionManager sessionManager) { - this.okHttpJsonApiClient = okHttpJsonApiClient; - this.sessionManager = sessionManager; - } - - - /** - * Creats a new LeaderboardListViewModel - * @param modelClass - * @param - * @return - */ - @NonNull - @Override - public T create(@NonNull Class modelClass) { - if (modelClass.isAssignableFrom(LeaderboardListViewModel.class)) { - return (T) new LeaderboardListViewModel(okHttpJsonApiClient, sessionManager); - } - throw new IllegalArgumentException("Unknown class name"); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.kt new file mode 100644 index 000000000..f325355e0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.kt @@ -0,0 +1,26 @@ +package fr.free.nrw.commons.profile.leaderboard + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import javax.inject.Inject + + +/** + * This class extends the ViewModelProvider.Factory and creates a ViewModelFactory class + * for leaderboardListViewModel + */ +class ViewModelFactory @Inject constructor( + private val okHttpJsonApiClient: OkHttpJsonApiClient, + private val sessionManager: SessionManager +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + if (modelClass.isAssignableFrom(LeaderboardListViewModel::class.java)) { + LeaderboardListViewModel(okHttpJsonApiClient, sessionManager) as T + } else { + throw IllegalArgumentException("Unknown class name") + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.java b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.java deleted file mode 100644 index 51d806a88..000000000 --- a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.java +++ /dev/null @@ -1,116 +0,0 @@ -package fr.free.nrw.commons.leaderboard; - -import com.google.gson.Gson; -import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Request.Builder; -import okhttp3.Response; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -/** - * This class tests the Leaderboard API calls - */ -public class LeaderboardApiTest { - - MockWebServer server; - private static final String TEST_USERNAME = "user"; - private static final String TEST_AVATAR = "avatar"; - private static final int TEST_USER_RANK = 1; - private static final int TEST_USER_COUNT = 0; - - private static final String FILE_NAME = "leaderboard_sample_response.json"; - private static final String ENDPOINT = "/leaderboard.py"; - - /** - * This method initialises a Mock Server - */ - @Before - public void initTest() { - server = new MockWebServer(); - } - - /** - * This method will setup a Mock Server and load Test JSON Response File - * @throws Exception - */ - @Before - public void setUp() throws Exception { - - String testResponseBody = convertStreamToString(getClass().getClassLoader().getResourceAsStream(FILE_NAME)); - - server.enqueue(new MockResponse().setBody(testResponseBody)); - server.start(); - } - - /** - * This method converts a Input Stream to String - * @param is takes Input Stream of JSON File as Parameter - * @return a String with JSON data - * @throws Exception - */ - private static String convertStreamToString(InputStream is) throws Exception { - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); - } - reader.close(); - return sb.toString(); - } - - /** - * This method will call the Mock Server and Test it with sample values. - * It will test the Leaderboard API call functionality and check if the object is - * being created with the correct values - * @throws IOException - */ - @Test - public void apiTest() throws IOException { - HttpUrl httpUrl = server.url(ENDPOINT); - LeaderboardResponse response = sendRequest(new OkHttpClient(), httpUrl); - - Assert.assertEquals(TEST_AVATAR, response.getAvatar()); - Assert.assertEquals(TEST_USERNAME, response.getUsername()); - Assert.assertEquals(Integer.valueOf(TEST_USER_RANK), response.getRank()); - Assert.assertEquals(Integer.valueOf(TEST_USER_COUNT), response.getCategoryCount()); - } - - /** - * This method will call the Mock API and returns the Leaderboard Response Object - * @param okHttpClient - * @param httpUrl - * @return Leaderboard Response Object - * @throws IOException - */ - private LeaderboardResponse sendRequest(OkHttpClient okHttpClient, HttpUrl httpUrl) - throws IOException { - Request request = new Builder().url(httpUrl).build(); - Response response = okHttpClient.newCall(request).execute(); - if (response.isSuccessful()) { - Gson gson = new Gson(); - return gson.fromJson(response.body().string(), LeaderboardResponse.class); - } - return null; - } - - /** - * This method shuts down the Mock Server - * @throws IOException - */ - @After - public void shutdown() throws IOException { - server.shutdown(); - } -} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.kt new file mode 100644 index 000000000..ac0da42f3 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.kt @@ -0,0 +1,121 @@ +package fr.free.nrw.commons.leaderboard + +import com.google.gson.Gson +import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader + +/** + * This class tests the Leaderboard API calls + */ +class LeaderboardApiTest { + lateinit var server: MockWebServer + + /** + * This method initialises a Mock Server + */ + @Before + fun initTest() { + server = MockWebServer() + } + + /** + * This method will setup a Mock Server and load Test JSON Response File + * @throws Exception + */ + @Before + @Throws(Exception::class) + fun setUp() { + val testResponseBody = convertStreamToString( + javaClass.classLoader!!.getResourceAsStream(FILE_NAME) + ) + + server.enqueue(MockResponse().setBody(testResponseBody)) + server.start() + } + + /** + * This method will call the Mock Server and Test it with sample values. + * It will test the Leaderboard API call functionality and check if the object is + * being created with the correct values + * @throws IOException + */ + @Test + @Throws(IOException::class) + fun apiTest() { + val httpUrl = server.url(ENDPOINT) + val response = sendRequest(OkHttpClient(), httpUrl) + + Assert.assertEquals(TEST_AVATAR, response!!.avatar) + Assert.assertEquals(TEST_USERNAME, response.username) + Assert.assertEquals(TEST_USER_RANK, response.rank) + Assert.assertEquals(TEST_USER_COUNT, response.categoryCount) + } + + /** + * This method will call the Mock API and returns the Leaderboard Response Object + * @param okHttpClient + * @param httpUrl + * @return Leaderboard Response Object + * @throws IOException + */ + @Throws(IOException::class) + private fun sendRequest(okHttpClient: OkHttpClient, httpUrl: HttpUrl): LeaderboardResponse? { + val request: Request = Request.Builder().url(httpUrl).build() + val response = okHttpClient.newCall(request).execute() + if (response.isSuccessful) { + val gson = Gson() + return gson.fromJson(response.body!!.string(), LeaderboardResponse::class.java) + } + return null + } + + /** + * This method shuts down the Mock Server + * @throws IOException + */ + @After + @Throws(IOException::class) + fun shutdown() { + server.shutdown() + } + + companion object { + private const val TEST_USERNAME = "user" + private const val TEST_AVATAR = "avatar" + private const val TEST_USER_RANK = 1 + private const val TEST_USER_COUNT = 0 + + private const val FILE_NAME = "leaderboard_sample_response.json" + private const val ENDPOINT = "/leaderboard.py" + + /** + * This method converts a Input Stream to String + * @param is takes Input Stream of JSON File as Parameter + * @return a String with JSON data + * @throws Exception + */ + @Throws(Exception::class) + private fun convertStreamToString(`is`: InputStream): String { + val reader = BufferedReader(InputStreamReader(`is`)) + val sb = StringBuilder() + var line: String? + while ((reader.readLine().also { line = it }) != null) { + sb.append(line).append("\n") + } + reader.close() + return sb.toString() + } + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.java b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.java deleted file mode 100644 index 7c2b25d3b..000000000 --- a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package fr.free.nrw.commons.leaderboard; - -import com.google.gson.Gson; -import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Request.Builder; -import okhttp3.Response; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -public class UpdateAvatarApiTest { - - private static final String TEST_USERNAME = "user"; - private static final String TEST_STATUS = "200"; - private static final String TEST_MESSAGE = "Avatar Updated"; - private static final String FILE_NAME = "update_leaderboard_avatar_sample_response.json"; - private static final String ENDPOINT = "/update_avatar.py"; - MockWebServer server; - - /** - * This method converts a Input Stream to String - * - * @param is takes Input Stream of JSON File as Parameter - * @return a String with JSON data - * @throws Exception - */ - private static String convertStreamToString(final InputStream is) throws Exception { - final BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - final StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); - } - reader.close(); - return sb.toString(); - } - - /** - * This method initialises a Mock Server - */ - @Before - public void initTest() { - server = new MockWebServer(); - } - - /** - * This method will setup a Mock Server and load Test JSON Response File - * - * @throws Exception - */ - @Before - public void setUp() throws Exception { - - final String testResponseBody = convertStreamToString( - getClass().getClassLoader().getResourceAsStream(FILE_NAME)); - - server.enqueue(new MockResponse().setBody(testResponseBody)); - server.start(); - } - - /** - * This method will call the Mock Server and Test it with sample values. It will test the Update - * Avatar API call functionality and check if the object is being created with the correct - * values - * - * @throws IOException - */ - @Test - public void apiTest() throws IOException { - final HttpUrl httpUrl = server.url(ENDPOINT); - final UpdateAvatarResponse response = sendRequest(new OkHttpClient(), httpUrl); - - Assert.assertEquals(TEST_USERNAME, response.getUser()); - Assert.assertEquals(TEST_STATUS, response.getStatus()); - Assert.assertEquals(TEST_MESSAGE, response.getMessage()); - } - - /** - * This method will call the Mock API and returns the Update Avatar Response Object - * - * @param okHttpClient - * @param httpUrl - * @return Update Avatar Response Object - * @throws IOException - */ - private UpdateAvatarResponse sendRequest(final OkHttpClient okHttpClient, final HttpUrl httpUrl) - throws IOException { - final Request request = new Builder().url(httpUrl).build(); - final Response response = okHttpClient.newCall(request).execute(); - if (response.isSuccessful()) { - final Gson gson = new Gson(); - return gson.fromJson(response.body().string(), UpdateAvatarResponse.class); - } - return null; - } - - /** - * This method shuts down the Mock Server - * - * @throws IOException - */ - @After - public void shutdown() throws IOException { - server.shutdown(); - } -} - diff --git a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.kt new file mode 100644 index 000000000..6b7f064cf --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.kt @@ -0,0 +1,127 @@ +package fr.free.nrw.commons.leaderboard + +import com.google.gson.Gson +import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader + +class UpdateAvatarApiTest { + lateinit var server: MockWebServer + + /** + * This method initialises a Mock Server + */ + @Before + fun initTest() { + server = MockWebServer() + } + + /** + * This method will setup a Mock Server and load Test JSON Response File + * + * @throws Exception + */ + @Before + @Throws(Exception::class) + fun setUp() { + val testResponseBody = convertStreamToString( + javaClass.classLoader!!.getResourceAsStream(FILE_NAME) + ) + + server.enqueue(MockResponse().setBody(testResponseBody)) + server.start() + } + + /** + * This method will call the Mock Server and Test it with sample values. It will test the Update + * Avatar API call functionality and check if the object is being created with the correct + * values + * + * @throws IOException + */ + @Test + @Throws(IOException::class) + fun apiTest() { + val httpUrl = server.url(ENDPOINT) + val response = sendRequest(OkHttpClient(), httpUrl) + Assert.assertNotNull(response) + + with(response!!) { + Assert.assertEquals(TEST_USERNAME, user) + Assert.assertEquals(TEST_STATUS, status) + Assert.assertEquals(TEST_MESSAGE, message) + } + } + + /** + * This method will call the Mock API and returns the Update Avatar Response Object + * + * @param okHttpClient + * @param httpUrl + * @return Update Avatar Response Object + * @throws IOException + */ + @Throws(IOException::class) + private fun sendRequest(okHttpClient: OkHttpClient, httpUrl: HttpUrl): UpdateAvatarResponse? { + val request: Request = Request.Builder().url(httpUrl).build() + val response = okHttpClient.newCall(request).execute() + if (response.isSuccessful) { + val gson = Gson() + return gson.fromJson( + response.body!!.string(), + UpdateAvatarResponse::class.java + ) + } + return null + } + + /** + * This method shuts down the Mock Server + * + * @throws IOException + */ + @After + @Throws(IOException::class) + fun shutdown() { + server.shutdown() + } + + companion object { + private const val TEST_USERNAME = "user" + private const val TEST_STATUS = "200" + private const val TEST_MESSAGE = "Avatar Updated" + private const val FILE_NAME = "update_leaderboard_avatar_sample_response.json" + private const val ENDPOINT = "/update_avatar.py" + + /** + * This method converts a Input Stream to String + * + * @param is takes Input Stream of JSON File as Parameter + * @return a String with JSON data + * @throws Exception + */ + @Throws(Exception::class) + private fun convertStreamToString(`is`: InputStream): String { + val reader = BufferedReader(InputStreamReader(`is`)) + val sb = StringBuilder() + var line: String? + while ((reader.readLine().also { line = it }) != null) { + sb.append(line).append("\n") + } + reader.close() + return sb.toString() + } + } +} + From 9dd504e56096bc5892edff901d3a939e50846939 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 5 Dec 2024 13:01:47 +0100 Subject: [PATCH 53/74] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-tr/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 54593c681..2e4e46481 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -21,6 +21,7 @@ * Okkerem * Oyuncu * Rapsar +* RuzDD * SaldırganSincap * Sayginer * Sezgin İbiş @@ -146,6 +147,7 @@ Kategori ara Medyanızın tasvir ettiği ögeleri arayın (dağ, Tac Mahal, vb.) Kaydet + Taşma menüsü Yenile Liste !Henüz yükleme yok) @@ -800,6 +802,7 @@ Lütfen bir yorum girin Tartışma \' %1$s \' öğesi hakkında bir şeyler yazın. Herkes tarafından görülebilir olacaktır. + \'%1$s\' artık yok, dolayısı ile resmi çekilemez. Diğer sorun veya bilgi (lütfen aşağıda açıklayınız). Geri bildiriminiz aşağıdaki wiki sayfasına gönderilir: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Tüm yüklemeleri iptal etmek istediğinizden emin misiniz? @@ -807,5 +810,10 @@ Yüklemeler Beklemede Başarısız + Sil + İptal + %1$s klasörü başarıyla silindi + %1$s klasörü silinemedi Bu yerin zaten bir resmi var. + Şimdi bu yerin bir resime sahip olup olmadığı denetleniyor. From 3777f18bf9c7efa4c679023b91ba9bad6cc10ad0 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Thu, 5 Dec 2024 08:13:38 -0600 Subject: [PATCH 54/74] Convert mwapi/wikidata to kotlin (part 1) (#5991) * Convert OkHttpJsonApiClient and CategoryApi to kotlin * Convert GsonUtil to kotlin * Convert WikidataConstants to kotlin * Convert WikidataEditListener to kotlin * Convert WikidataEditService to kotlin * work in progress * Convert RequiredFieldsCheckOnReadTypeAdapterFactory to kotlin * Converted type adapters * Convert WikiSiteTypeAdapter to kotlin * Fixed nullability --- .../commons/campaigns/CampaignsPresenter.kt | 4 +- .../free/nrw/commons/di/NetworkingModule.kt | 3 +- .../free/nrw/commons/mwapi/CategoryApi.java | 99 --- .../fr/free/nrw/commons/mwapi/CategoryApi.kt | 83 +++ .../commons/mwapi/OkHttpJsonApiClient.java | 677 ------------------ .../nrw/commons/mwapi/OkHttpJsonApiClient.kt | 543 ++++++++++++++ .../free/nrw/commons/upload/FileProcessor.kt | 4 +- .../nrw/commons/upload/worker/UploadWorker.kt | 4 +- .../commons/wikidata/CommonsServiceFactory.kt | 5 +- .../free/nrw/commons/wikidata/GsonUtil.java | 34 - .../fr/free/nrw/commons/wikidata/GsonUtil.kt | 29 + .../commons/wikidata/WikidataConstants.java | 11 - .../nrw/commons/wikidata/WikidataConstants.kt | 11 + .../wikidata/WikidataEditListener.java | 16 - .../commons/wikidata/WikidataEditListener.kt | 11 + .../wikidata/WikidataEditListenerImpl.java | 20 - .../wikidata/WikidataEditListenerImpl.kt | 13 + .../commons/wikidata/WikidataEditService.java | 271 ------- .../commons/wikidata/WikidataEditService.kt | 252 +++++++ .../wikidata/json/NamespaceTypeAdapter.java | 29 - .../wikidata/json/NamespaceTypeAdapter.kt | 26 + .../json/PostProcessingTypeAdapter.java | 34 - .../json/PostProcessingTypeAdapter.kt | 35 + ...edFieldsCheckOnReadTypeAdapterFactory.java | 94 --- ...iredFieldsCheckOnReadTypeAdapterFactory.kt | 75 ++ .../json/RuntimeTypeAdapterFactory.java | 280 -------- .../json/RuntimeTypeAdapterFactory.kt | 273 +++++++ .../commons/wikidata/json/UriTypeAdapter.java | 22 - .../commons/wikidata/json/UriTypeAdapter.kt | 19 + .../wikidata/json/WikiSiteTypeAdapter.java | 63 -- .../wikidata/json/WikiSiteTypeAdapter.kt | 61 ++ .../wikidata/json/annotations/Required.java | 21 - .../wikidata/json/annotations/Required.kt | 12 + .../model/notifications/Notification.java | 2 +- .../nrw/commons/wikidata/mwapi/UserInfo.java | 34 - .../nrw/commons/wikidata/mwapi/UserInfo.kt | 21 + .../free/nrw/commons/MockWebServerTest.java | 2 +- .../campaigns/CampaignsPresenterTest.kt | 6 +- .../free/nrw/commons/mwapi/UserClientTest.kt | 9 +- .../nearby/NearbyParentFragmentUnitTest.kt | 2 +- .../notification/NotificationClientTest.kt | 26 +- 41 files changed, 1490 insertions(+), 1746 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.kt diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt index ffbf92540..4743e0e54 100644 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt @@ -52,12 +52,12 @@ class CampaignsPresenter @Inject constructor( return } - okHttpJsonApiClient.campaigns + okHttpJsonApiClient.getCampaigns() .observeOn(mainThreadScheduler) .subscribeOn(ioScheduler) .doOnSubscribe { disposable = it } .subscribe({ campaignResponseDTO -> - val campaigns = campaignResponseDTO.campaigns?.toMutableList() + val campaigns = campaignResponseDTO?.campaigns?.toMutableList() if (campaigns.isNullOrEmpty()) { Timber.e("The campaigns list is empty") view!!.showCampaigns(null) diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt index 5ecc04120..7ca3b4fd0 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt @@ -170,14 +170,13 @@ class NetworkingModule { @Named(NAMED_WIKI_DATA_WIKI_SITE) fun provideWikidataWikiSite(): WikiSite = WikiSite(BuildConfig.WIKIDATA_URL) - /** * Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere. * @return returns a singleton Gson instance */ @Provides @Singleton - fun provideGson(): Gson = GsonUtil.getDefaultGson() + fun provideGson(): Gson = GsonUtil.defaultGson @Provides @Singleton diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java deleted file mode 100644 index f587893c5..000000000 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java +++ /dev/null @@ -1,99 +0,0 @@ -package fr.free.nrw.commons.mwapi; - -import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_PREFIX; - -import com.google.gson.Gson; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.category.CategoryItem; -import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage; -import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse; -import io.reactivex.Single; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import javax.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; -import timber.log.Timber; - -/** - * Uses the OkHttp library to implement calls to the Commons MediaWiki API to match GPS coordinates - * with nearby Commons categories. Parses the results using GSON to obtain a list of relevant - * categories. Note: that caller is responsible for executing the request() method on a background - * thread. - */ -public class CategoryApi { - - private final OkHttpClient okHttpClient; - private final Gson gson; - - @Inject - public CategoryApi(final OkHttpClient okHttpClient, final Gson gson) { - this.okHttpClient = okHttpClient; - this.gson = gson; - } - - public Single> request(String coords) { - return Single.fromCallable(() -> { - HttpUrl apiUrl = buildUrl(coords); - Timber.d("URL: %s", apiUrl.toString()); - - Request request = new Request.Builder().get().url(apiUrl).build(); - Response response = okHttpClient.newCall(request).execute(); - ResponseBody body = response.body(); - if (body == null) { - return Collections.emptyList(); - } - - MwQueryResponse apiResponse = gson.fromJson(body.charStream(), MwQueryResponse.class); - Set categories = new LinkedHashSet<>(); - if (apiResponse != null && apiResponse.query() != null && apiResponse.query().pages() != null) { - for (MwQueryPage page : apiResponse.query().pages()) { - if (page.categories() != null) { - for (MwQueryPage.Category category : page.categories()) { - categories.add(new CategoryItem(category.title().replace(CATEGORY_PREFIX, ""), "", "", false)); - } - } - } - } - return new ArrayList<>(categories); - }); - } - - /** - * Builds URL with image coords for MediaWiki API calls - * Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2 - * - * @param coords Coordinates to build query with - * @return URL for API query - */ - private HttpUrl buildUrl(final String coords) { - return HttpUrl - .parse(BuildConfig.WIKIMEDIA_API_HOST) - .newBuilder() - .addQueryParameter("action", "query") - .addQueryParameter("prop", "categories|coordinates|pageprops") - .addQueryParameter("format", "json") - .addQueryParameter("clshow", "!hidden") - .addQueryParameter("coprop", "type|name|dim|country|region|globe") - .addQueryParameter("codistancefrompoint", coords) - .addQueryParameter("generator", "geosearch") - .addQueryParameter("ggscoord", coords) - .addQueryParameter("ggsradius", "10000") - .addQueryParameter("ggslimit", "10") - .addQueryParameter("ggsnamespace", "6") - .addQueryParameter("ggsprop", "type|name|dim|country|region|globe") - .addQueryParameter("ggsprimary", "all") - .addQueryParameter("formatversion", "2") - .build(); - } - -} - - - diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.kt b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.kt new file mode 100644 index 000000000..1f8c51187 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.kt @@ -0,0 +1,83 @@ +package fr.free.nrw.commons.mwapi + +import com.google.gson.Gson +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.category.CATEGORY_PREFIX +import fr.free.nrw.commons.category.CategoryItem +import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse +import io.reactivex.Single +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import timber.log.Timber +import javax.inject.Inject + +/** + * Uses the OkHttp library to implement calls to the Commons MediaWiki API to match GPS coordinates + * with nearby Commons categories. Parses the results using GSON to obtain a list of relevant + * categories. Note: that caller is responsible for executing the request() method on a background + * thread. + */ +class CategoryApi @Inject constructor( + private val okHttpClient: OkHttpClient, + private val gson: Gson +) { + private val apiUrl : HttpUrl by lazy { BuildConfig.WIKIMEDIA_API_HOST.toHttpUrlOrNull()!! } + + fun request(coords: String): Single> = Single.fromCallable { + val apiUrl = buildUrl(coords) + Timber.d("URL: %s", apiUrl.toString()) + + val request: Request = Request.Builder().get().url(apiUrl).build() + val response = okHttpClient.newCall(request).execute() + val body = response.body ?: return@fromCallable emptyList() + + val apiResponse = gson.fromJson(body.charStream(), MwQueryResponse::class.java) + val categories: MutableSet = mutableSetOf() + if (apiResponse?.query() != null && apiResponse.query()!!.pages() != null) { + for (page in apiResponse.query()!!.pages()!!) { + if (page.categories() != null) { + for (category in page.categories()!!) { + categories.add( + CategoryItem( + name = category.title().replace(CATEGORY_PREFIX, ""), + description = "", + thumbnail = "", + isSelected = false + ) + ) + } + } + } + } + ArrayList(categories) + } + + /** + * Builds URL with image coords for MediaWiki API calls + * Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2 + * + * @param coords Coordinates to build query with + * @return URL for API query + */ + private fun buildUrl(coords: String): HttpUrl = apiUrl.newBuilder() + .addQueryParameter("action", "query") + .addQueryParameter("prop", "categories|coordinates|pageprops") + .addQueryParameter("format", "json") + .addQueryParameter("clshow", "!hidden") + .addQueryParameter("coprop", "type|name|dim|country|region|globe") + .addQueryParameter("codistancefrompoint", coords) + .addQueryParameter("generator", "geosearch") + .addQueryParameter("ggscoord", coords) + .addQueryParameter("ggsradius", "10000") + .addQueryParameter("ggslimit", "10") + .addQueryParameter("ggsnamespace", "6") + .addQueryParameter("ggsprop", "type|name|dim|country|region|globe") + .addQueryParameter("ggsprimary", "all") + .addQueryParameter("formatversion", "2") + .build() +} + + + diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java deleted file mode 100644 index 8ed37a293..000000000 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ /dev/null @@ -1,677 +0,0 @@ -package fr.free.nrw.commons.mwapi; - -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LEADERBOARD_END_POINT; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.UPDATE_AVATAR_END_POINT; - -import android.text.TextUtils; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.gson.Gson; -import fr.free.nrw.commons.campaigns.CampaignResponseDTO; -import fr.free.nrw.commons.explore.depictions.DepictsClient; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.nearby.model.ItemsClass; -import fr.free.nrw.commons.nearby.model.NearbyResponse; -import fr.free.nrw.commons.nearby.model.NearbyResultItem; -import fr.free.nrw.commons.nearby.model.PlaceBindings; -import fr.free.nrw.commons.profile.achievements.FeaturedImages; -import fr.free.nrw.commons.profile.achievements.FeedbackResponse; -import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse; -import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse; -import fr.free.nrw.commons.upload.FileUtils; -import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.wikidata.model.GetWikidataEditCountResponse; -import io.reactivex.Observable; -import io.reactivex.Single; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.inject.Inject; -import javax.inject.Singleton; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.jetbrains.annotations.NotNull; -import timber.log.Timber; - -/** - * Test methods in ok http api client - */ -@Singleton -public class OkHttpJsonApiClient { - - private final OkHttpClient okHttpClient; - private final DepictsClient depictsClient; - private final HttpUrl wikiMediaToolforgeUrl; - private final String sparqlQueryUrl; - private final String campaignsUrl; - private final Gson gson; - - - @Inject - public OkHttpJsonApiClient(OkHttpClient okHttpClient, - DepictsClient depictsClient, - HttpUrl wikiMediaToolforgeUrl, - String sparqlQueryUrl, - String campaignsUrl, - Gson gson) { - this.okHttpClient = okHttpClient; - this.depictsClient = depictsClient; - this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl; - this.sparqlQueryUrl = sparqlQueryUrl; - this.campaignsUrl = campaignsUrl; - this.gson = gson; - } - - /** - * The method will gradually calls the leaderboard API and fetches the leaderboard - * - * @param userName username of leaderboard user - * @param duration duration for leaderboard - * @param category category for leaderboard - * @param limit page size limit for list - * @param offset offset for the list - * @return LeaderboardResponse object - */ - @NonNull - public Observable getLeaderboard(String userName, String duration, - String category, String limit, String offset) { - final String fetchLeaderboardUrlTemplate = wikiMediaToolforgeUrl - + LEADERBOARD_END_POINT; - String url = String.format(Locale.ENGLISH, - fetchLeaderboardUrlTemplate, - userName, - duration, - category, - limit, - offset); - HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); - urlBuilder.addQueryParameter("user", userName); - urlBuilder.addQueryParameter("duration", duration); - urlBuilder.addQueryParameter("category", category); - urlBuilder.addQueryParameter("limit", limit); - urlBuilder.addQueryParameter("offset", offset); - Timber.i("Url %s", urlBuilder.toString()); - Request request = new Request.Builder() - .url(urlBuilder.toString()) - .build(); - return Observable.fromCallable(() -> { - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.body() != null && response.isSuccessful()) { - String json = response.body().string(); - if (json == null) { - return new LeaderboardResponse(); - } - Timber.d("Response for leaderboard is %s", json); - try { - return gson.fromJson(json, LeaderboardResponse.class); - } catch (Exception e) { - return new LeaderboardResponse(); - } - } - return new LeaderboardResponse(); - }); - } - - /** - * This method will update the leaderboard user avatar - * - * @param username username to update - * @param avatar url of the new avatar - * @return UpdateAvatarResponse object - */ - @NonNull - public Single setAvatar(String username, String avatar) { - final String urlTemplate = wikiMediaToolforgeUrl - + UPDATE_AVATAR_END_POINT; - return Single.fromCallable(() -> { - String url = String.format(Locale.ENGLISH, - urlTemplate, - username, - avatar); - HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); - urlBuilder.addQueryParameter("user", username); - urlBuilder.addQueryParameter("avatar", avatar); - Timber.i("Url %s", urlBuilder.toString()); - Request request = new Request.Builder() - .url(urlBuilder.toString()) - .build(); - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.body() != null && response.isSuccessful()) { - String json = response.body().string(); - if (json == null) { - return null; - } - try { - return gson.fromJson(json, UpdateAvatarResponse.class); - } catch (Exception e) { - return new UpdateAvatarResponse(); - } - } - return null; - }); - } - - @NonNull - public Single getUploadCount(String userName) { - HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder(); - urlBuilder - .addPathSegments("uploadsbyuser.py") - .addQueryParameter("user", userName); - - if (ConfigUtils.isBetaFlavour()) { - urlBuilder.addQueryParameter("labs", "commonswiki"); - } - - Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - return Single.fromCallable(() -> { - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.isSuccessful()) { - ResponseBody responseBody = response.body(); - if (null != responseBody) { - String responseBodyString = responseBody.string().trim(); - if (!TextUtils.isEmpty(responseBodyString)) { - try { - return Integer.parseInt(responseBodyString); - } catch (NumberFormatException e) { - Timber.e(e); - } - } - } - } - return 0; - }); - } - - @NonNull - public Single getWikidataEdits(String userName) { - HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder(); - urlBuilder - .addPathSegments("wikidataedits.py") - .addQueryParameter("user", userName); - - if (ConfigUtils.isBetaFlavour()) { - urlBuilder.addQueryParameter("labs", "commonswiki"); - } - - Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - return Single.fromCallable(() -> { - Response response = okHttpClient.newCall(request).execute(); - if (response != null && - response.isSuccessful() && response.body() != null) { - String json = response.body().string(); - if (json == null) { - return 0; - } - // Extract JSON from response - json = json.substring(json.indexOf('{')); - GetWikidataEditCountResponse countResponse = gson - .fromJson(json, GetWikidataEditCountResponse.class); - if (null != countResponse) { - return countResponse.getWikidataEditCount(); - } - } - return 0; - }); - } - - /** - * This takes userName as input, which is then used to fetch the feedback/achievements - * statistics using OkHttp and JavaRx. This function return JSONObject - * - * @param userName MediaWiki user name - * @return - */ - public Single getAchievements(String userName) { - final String fetchAchievementUrlTemplate = - wikiMediaToolforgeUrl + (ConfigUtils.isBetaFlavour() ? "/feedback.py?labs=commonswiki" - : "/feedback.py"); - return Single.fromCallable(() -> { - String url = String.format( - Locale.ENGLISH, - fetchAchievementUrlTemplate, - userName); - HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); - urlBuilder.addQueryParameter("user", userName); - Request request = new Request.Builder() - .url(urlBuilder.toString()) - .build(); - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.body() != null && response.isSuccessful()) { - String json = response.body().string(); - if (json == null) { - return null; - } - // Extract JSON from response - json = json.substring(json.indexOf('{')); - Timber.d("Response for achievements is %s", json); - try { - return gson.fromJson(json, FeedbackResponse.class); - } catch (Exception e) { - return new FeedbackResponse(0, 0, 0, new FeaturedImages(0, 0), 0, ""); - } - - - } - return null; - }); - } - - /** - * Make API Call to get Nearby Places - * - * @param cur Search lat long - * @param language Language - * @param radius Search Radius - * @return - * @throws Exception - */ - @Nullable - public List getNearbyPlaces(final LatLng cur, final String language, final double radius, - final String customQuery) - throws Exception { - - Timber.d("Fetching nearby items at radius %s", radius); - Timber.d("CUSTOM_SPARQL: %s", String.valueOf(customQuery != null)); - final String wikidataQuery; - if (customQuery != null) { - wikidataQuery = customQuery; - } else { - wikidataQuery = FileUtils.readFromResource( - "/queries/radius_query_for_upload_wizard.rq"); - } - final String query = wikidataQuery - .replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius)) - .replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.getLatitude())) - .replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude())) - .replace("${LANG}", language); - - final HttpUrl.Builder urlBuilder = HttpUrl - .parse(sparqlQueryUrl) - .newBuilder() - .addQueryParameter("query", query) - .addQueryParameter("format", "json"); - - final Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - final Response response = okHttpClient.newCall(request).execute(); - if (response.body() != null && response.isSuccessful()) { - final String json = response.body().string(); - final NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class); - final List bindings = nearbyResponse.getResults().getBindings(); - final List places = new ArrayList<>(); - for (final NearbyResultItem item : bindings) { - final Place placeFromNearbyItem = Place.from(item); - placeFromNearbyItem.setMonument(false); - places.add(placeFromNearbyItem); - } - return places; - } - throw new Exception(response.message()); - } - - /** - * Retrieves nearby places based on screen coordinates and optional query parameters. - * - * @param screenTopRight The top right corner of the screen (latitude, longitude). - * @param screenBottomLeft The bottom left corner of the screen (latitude, longitude). - * @param language The language for the query. - * @param shouldQueryForMonuments Flag indicating whether to include monuments in the query. - * @param customQuery Optional custom SPARQL query to use instead of default - * queries. - * @return A list of nearby places. - * @throws Exception If an error occurs during the retrieval process. - */ - @Nullable - public List getNearbyPlaces( - final fr.free.nrw.commons.location.LatLng screenTopRight, - final fr.free.nrw.commons.location.LatLng screenBottomLeft, final String language, - final boolean shouldQueryForMonuments, final String customQuery) - throws Exception { - - Timber.d("CUSTOM_SPARQL: %s", String.valueOf(customQuery != null)); - - final String wikidataQuery; - if (customQuery != null) { - wikidataQuery = customQuery; - } else if (!shouldQueryForMonuments) { - wikidataQuery = FileUtils.readFromResource("/queries/rectangle_query_for_nearby.rq"); - } else { - wikidataQuery = FileUtils.readFromResource( - "/queries/rectangle_query_for_nearby_monuments.rq"); - } - - final double westCornerLat = screenTopRight.getLatitude(); - final double westCornerLong = screenTopRight.getLongitude(); - final double eastCornerLat = screenBottomLeft.getLatitude(); - final double eastCornerLong = screenBottomLeft.getLongitude(); - - final String query = wikidataQuery - .replace("${LAT_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLat)) - .replace("${LONG_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLong)) - .replace("${LAT_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLat)) - .replace("${LONG_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLong)) - .replace("${LANG}", language); - final HttpUrl.Builder urlBuilder = HttpUrl - .parse(sparqlQueryUrl) - .newBuilder() - .addQueryParameter("query", query) - .addQueryParameter("format", "json"); - - final Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - final Response response = okHttpClient.newCall(request).execute(); - if (response.body() != null && response.isSuccessful()) { - final String json = response.body().string(); - final NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class); - final List bindings = nearbyResponse.getResults().getBindings(); - final List places = new ArrayList<>(); - for (final NearbyResultItem item : bindings) { - final Place placeFromNearbyItem = Place.from(item); - if (shouldQueryForMonuments && item.getMonument() != null) { - placeFromNearbyItem.setMonument(true); - } else { - placeFromNearbyItem.setMonument(false); - } - places.add(placeFromNearbyItem); - } - return places; - } - throw new Exception(response.message()); - } - - /** - * Retrieves a list of places based on the provided list of places and language. - * - * @param placeList A list of Place objects for which to fetch information. - * @param language The language code to use for the query. - * @return A list of Place objects with additional information retrieved from Wikidata, or null - * if an error occurs. - * @throws IOException If there is an issue with reading the resource file or executing the HTTP - * request. - */ - @Nullable - public List getPlaces( - final List placeList, final String language) throws IOException { - final String wikidataQuery = FileUtils.readFromResource("/queries/query_for_item.rq"); - String qids = ""; - for (final Place place : placeList) { - qids += "\n" + ("wd:" + place.getWikiDataEntityId()); - } - final String query = wikidataQuery - .replace("${ENTITY}", qids) - .replace("${LANG}", language); - final HttpUrl.Builder urlBuilder = HttpUrl - .parse(sparqlQueryUrl) - .newBuilder() - .addQueryParameter("query", query) - .addQueryParameter("format", "json"); - - final Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - try (Response response = okHttpClient.newCall(request).execute()) { - if (response.isSuccessful()) { - final String json = response.body().string(); - final NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class); - final List bindings = nearbyResponse.getResults().getBindings(); - final List places = new ArrayList<>(); - for (final NearbyResultItem item : bindings) { - final Place placeFromNearbyItem = Place.from(item); - places.add(placeFromNearbyItem); - } - return places; - } else { - throw new IOException("Unexpected response code: " + response.code()); - } - } - } - - /** - * Make API Call to get Places - * - * @param leftLatLng Left lat long - * @param rightLatLng Right lat long - * @return - * @throws Exception - */ - @Nullable - public String getPlacesAsKML(final LatLng leftLatLng, final LatLng rightLatLng) - throws Exception { - String kmlString = "\n" + - "\n" + - "\n" + - " "; - List placeBindings = runQuery(leftLatLng, - rightLatLng); - if (placeBindings != null) { - for (PlaceBindings item : placeBindings) { - if (item.getItem() != null && item.getLabel() != null && item.getClas() != null) { - String input = item.getLocation().getValue(); - Pattern pattern = Pattern.compile( - "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)"); - Matcher matcher = pattern.matcher(input); - - if (matcher.find()) { - String longStr = matcher.group(1); - String latStr = matcher.group(2); - String itemUrl = item.getItem().getValue(); - String itemName = item.getLabel().getValue().replace("&", "&"); - String itemLatitude = latStr; - String itemLongitude = longStr; - String itemClass = item.getClas().getValue(); - - String formattedItemName = - !itemClass.isEmpty() ? itemName + " (" + itemClass + ")" - : itemName; - - String kmlEntry = "\n \n" + - " " + formattedItemName + "\n" + - " " + itemUrl + "\n" + - " \n" + - " " + itemLongitude + "," - + itemLatitude - + "\n" + - " \n" + - " "; - kmlString = kmlString + kmlEntry; - } else { - Timber.e("No match found"); - } - } - } - } - kmlString = kmlString + "\n \n" + - "\n"; - return kmlString; - } - - /** - * Make API Call to get Places - * - * @param leftLatLng Left lat long - * @param rightLatLng Right lat long - * @return - * @throws Exception - */ - @Nullable - public String getPlacesAsGPX(final LatLng leftLatLng, final LatLng rightLatLng) - throws Exception { - String gpxString = "\n" + - "" - + "\n"; - - List placeBindings = runQuery(leftLatLng, rightLatLng); - if (placeBindings != null) { - for (PlaceBindings item : placeBindings) { - if (item.getItem() != null && item.getLabel() != null && item.getClas() != null) { - String input = item.getLocation().getValue(); - Pattern pattern = Pattern.compile( - "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)"); - Matcher matcher = pattern.matcher(input); - - if (matcher.find()) { - String longStr = matcher.group(1); - String latStr = matcher.group(2); - String itemUrl = item.getItem().getValue(); - String itemName = item.getLabel().getValue().replace("&", "&"); - String itemLatitude = latStr; - String itemLongitude = longStr; - String itemClass = item.getClas().getValue(); - - String formattedItemName = - !itemClass.isEmpty() ? itemName + " (" + itemClass + ")" - : itemName; - - String gpxEntry = - "\n \n" + - " " + itemName + "\n" + - " " + itemUrl + "\n" + - " "; - gpxString = gpxString + gpxEntry; - - } else { - Timber.e("No match found"); - } - } - } - - } - gpxString = gpxString + "\n"; - return gpxString; - } - - private List runQuery(final LatLng currentLatLng, final LatLng nextLatLng) - throws IOException { - - final String wikidataQuery = FileUtils.readFromResource("/queries/places_query.rq"); - final String query = wikidataQuery - .replace("${LONGITUDE}", - String.format(Locale.ROOT, "%.2f", currentLatLng.getLongitude())) - .replace("${LATITUDE}", String.format(Locale.ROOT, "%.4f", currentLatLng.getLatitude())) - .replace("${NEXT_LONGITUDE}", - String.format(Locale.ROOT, "%.4f", nextLatLng.getLongitude())) - .replace("${NEXT_LATITUDE}", - String.format(Locale.ROOT, "%.4f", nextLatLng.getLatitude())); - - final HttpUrl.Builder urlBuilder = HttpUrl - .parse(sparqlQueryUrl) - .newBuilder() - .addQueryParameter("query", query) - .addQueryParameter("format", "json"); - - final Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - final Response response = okHttpClient.newCall(request).execute(); - if (response.body() != null && response.isSuccessful()) { - final String json = response.body().string(); - final ItemsClass item = gson.fromJson(json, ItemsClass.class); - return item.getResults().getBindings(); - } else { - return null; - } - } - - /** - * Make API Call to get Nearby Places Implementation does not expects a custom query - * - * @param cur Search lat long - * @param language Language - * @param radius Search Radius - * @return - * @throws Exception - */ - @Nullable - public List getNearbyPlaces(final LatLng cur, final String language, final double radius) - throws Exception { - return getNearbyPlaces(cur, language, radius, null); - } - - /** - * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example: - * bridge -> suspended bridge, aqueduct, etc - */ - public Single> getChildDepictions(String qid, int startPosition, - int limit) throws IOException { - return depictedItemsFrom( - sparqlQuery(qid, startPosition, limit, "/queries/subclasses_query.rq")); - } - - /** - * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example: - * bridge -> suspended bridge, aqueduct, etc - */ - public Single> getParentDepictions(String qid, int startPosition, - int limit) throws IOException { - return depictedItemsFrom(sparqlQuery(qid, startPosition, limit, - "/queries/parentclasses_query.rq")); - } - - private Single> depictedItemsFrom(Request request) { - return depictsClient.toDepictions(Single.fromCallable(() -> { - try (ResponseBody body = okHttpClient.newCall(request).execute().body()) { - return gson.fromJson(body.string(), SparqlResponse.class); - } - }).doOnError(Timber::e)); - } - - @NotNull - private Request sparqlQuery(String qid, int startPosition, int limit, String fileName) - throws IOException { - String query = FileUtils.readFromResource(fileName) - .replace("${QID}", qid) - .replace("${LANG}", "\"" + Locale.getDefault().getLanguage() + "\"") - .replace("${LIMIT}", "" + limit) - .replace("${OFFSET}", "" + startPosition); - HttpUrl.Builder urlBuilder = HttpUrl - .parse(sparqlQueryUrl) - .newBuilder() - .addQueryParameter("query", query) - .addQueryParameter("format", "json"); - return new Request.Builder() - .url(urlBuilder.build()) - .build(); - } - - public Single getCampaigns() { - return Single.fromCallable(() -> { - Request request = new Request.Builder().url(campaignsUrl) - .build(); - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.body() != null && response.isSuccessful()) { - String json = response.body().string(); - if (json == null) { - return null; - } - return gson.fromJson(json, CampaignResponseDTO.class); - } - return null; - }); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt new file mode 100644 index 000000000..c3ae11b94 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt @@ -0,0 +1,543 @@ +package fr.free.nrw.commons.mwapi + +import android.text.TextUtils +import com.google.gson.Gson +import fr.free.nrw.commons.campaigns.CampaignResponseDTO +import fr.free.nrw.commons.explore.depictions.DepictsClient +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.nearby.model.ItemsClass +import fr.free.nrw.commons.nearby.model.NearbyResponse +import fr.free.nrw.commons.nearby.model.PlaceBindings +import fr.free.nrw.commons.profile.achievements.FeaturedImages +import fr.free.nrw.commons.profile.achievements.FeedbackResponse +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants +import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse +import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse +import fr.free.nrw.commons.upload.FileUtils +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.wikidata.model.GetWikidataEditCountResponse +import io.reactivex.Observable +import io.reactivex.Single +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import timber.log.Timber +import java.io.IOException +import java.util.Locale +import java.util.regex.Pattern +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Test methods in ok http api client + */ +@Singleton +class OkHttpJsonApiClient @Inject constructor( + private val okHttpClient: OkHttpClient, + private val depictsClient: DepictsClient, + private val wikiMediaToolforgeUrl: HttpUrl, + private val sparqlQueryUrl: String, + private val campaignsUrl: String, + private val gson: Gson +) { + fun getLeaderboard( + userName: String?, duration: String?, + category: String?, limit: String?, offset: String? + ): Observable { + val fetchLeaderboardUrlTemplate = + wikiMediaToolforgeUrl.toString() + LeaderboardConstants.LEADERBOARD_END_POINT + val url = String.format(Locale.ENGLISH, + fetchLeaderboardUrlTemplate, userName, duration, category, limit, offset) + val urlBuilder: HttpUrl.Builder = url.toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("user", userName) + .addQueryParameter("duration", duration) + .addQueryParameter("category", category) + .addQueryParameter("limit", limit) + .addQueryParameter("offset", offset) + Timber.i("Url %s", urlBuilder.toString()) + val request: Request = Request.Builder() + .url(urlBuilder.toString()) + .build() + return Observable.fromCallable({ + val response: Response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + val json: String = response.body!!.string() + Timber.d("Response for leaderboard is %s", json) + try { + return@fromCallable gson.fromJson( + json, + LeaderboardResponse::class.java + ) + } catch (e: Exception) { + return@fromCallable LeaderboardResponse() + } + } + LeaderboardResponse() + }) + } + + fun setAvatar(username: String?, avatar: String?): Single { + val urlTemplate = wikiMediaToolforgeUrl + .toString() + LeaderboardConstants.UPDATE_AVATAR_END_POINT + return Single.fromCallable({ + val url = String.format(Locale.ENGLISH, urlTemplate, username, avatar) + val urlBuilder: HttpUrl.Builder = url.toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("user", username) + .addQueryParameter("avatar", avatar) + Timber.i("Url %s", urlBuilder.toString()) + val request: Request = Request.Builder() + .url(urlBuilder.toString()) + .build() + val response: Response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + val json: String = response.body!!.string() ?: return@fromCallable null + try { + return@fromCallable gson.fromJson( + json, + UpdateAvatarResponse::class.java + ) + } catch (e: Exception) { + return@fromCallable UpdateAvatarResponse() + } + } + null + }) + } + + fun getUploadCount(userName: String?): Single { + val urlBuilder: HttpUrl.Builder = wikiMediaToolforgeUrl.newBuilder() + .addPathSegments("uploadsbyuser.py") + .addQueryParameter("user", userName) + + if (isBetaFlavour) { + urlBuilder.addQueryParameter("labs", "commonswiki") + } + + val request: Request = Request.Builder() + .url(urlBuilder.build()) + .build() + + return Single.fromCallable({ + val response: Response = okHttpClient.newCall(request).execute() + if (response != null && response.isSuccessful) { + val responseBody = response.body + if (null != responseBody) { + val responseBodyString = responseBody.string().trim { it <= ' ' } + if (!TextUtils.isEmpty(responseBodyString)) { + try { + return@fromCallable responseBodyString.toInt() + } catch (e: NumberFormatException) { + Timber.e(e) + } + } + } + } + 0 + }) + } + + fun getWikidataEdits(userName: String?): Single { + val urlBuilder: HttpUrl.Builder = wikiMediaToolforgeUrl.newBuilder() + .addPathSegments("wikidataedits.py") + .addQueryParameter("user", userName) + + if (isBetaFlavour) { + urlBuilder.addQueryParameter("labs", "commonswiki") + } + + val request: Request = Request.Builder() + .url(urlBuilder.build()) + .build() + + return Single.fromCallable({ + val response: Response = okHttpClient.newCall(request).execute() + if (response != null && response.isSuccessful && response.body != null) { + var json: String = response.body!!.string() + // Extract JSON from response + json = json.substring(json.indexOf('{')) + val countResponse = gson + .fromJson( + json, + GetWikidataEditCountResponse::class.java + ) + if (null != countResponse) { + return@fromCallable countResponse.wikidataEditCount + } + } + 0 + }) + } + + fun getAchievements(userName: String?): Single { + val suffix = if (isBetaFlavour) "/feedback.py?labs=commonswiki" else "/feedback.py" + val fetchAchievementUrlTemplate = wikiMediaToolforgeUrl.toString() + suffix + return Single.fromCallable({ + val url = String.format( + Locale.ENGLISH, + fetchAchievementUrlTemplate, + userName + ) + val urlBuilder: HttpUrl.Builder = url.toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("user", userName) + val request: Request = Request.Builder() + .url(urlBuilder.toString()) + .build() + val response: Response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + var json: String = response.body!!.string() + // Extract JSON from response + json = json.substring(json.indexOf('{')) + Timber.d("Response for achievements is %s", json) + try { + return@fromCallable gson.fromJson( + json, + FeedbackResponse::class.java + ) + } catch (e: Exception) { + return@fromCallable FeedbackResponse(0, 0, 0, FeaturedImages(0, 0), 0, "") + } + } + null + }) + } + + @JvmOverloads + @Throws(Exception::class) + fun getNearbyPlaces( + cur: LatLng, language: String, radius: Double, + customQuery: String? = null + ): List? { + Timber.d("Fetching nearby items at radius %s", radius) + Timber.d("CUSTOM_SPARQL: %s", (customQuery != null).toString()) + val wikidataQuery: String = if (customQuery != null) { + customQuery + } else { + FileUtils.readFromResource("/queries/radius_query_for_upload_wizard.rq") + } + val query = wikidataQuery + .replace("\${RAD}", String.format(Locale.ROOT, "%.2f", radius)) + .replace("\${LAT}", String.format(Locale.ROOT, "%.4f", cur.latitude)) + .replace("\${LONG}", String.format(Locale.ROOT, "%.4f", cur.longitude)) + .replace("\${LANG}", language) + + val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!! + .newBuilder() + .addQueryParameter("query", query) + .addQueryParameter("format", "json") + + val request: Request = Request.Builder() + .url(urlBuilder.build()) + .build() + + val response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + val json = response.body!!.string() + val nearbyResponse = gson.fromJson(json, NearbyResponse::class.java) + val bindings = nearbyResponse.results.bindings + val places: MutableList = ArrayList() + for (item in bindings) { + val placeFromNearbyItem = Place.from(item) + placeFromNearbyItem.isMonument = false + places.add(placeFromNearbyItem) + } + return places + } + throw Exception(response.message) + } + + @Throws(Exception::class) + fun getNearbyPlaces( + screenTopRight: LatLng, + screenBottomLeft: LatLng, language: String, + shouldQueryForMonuments: Boolean, customQuery: String? + ): List? { + Timber.d("CUSTOM_SPARQL: %s", (customQuery != null).toString()) + + val wikidataQuery: String = if (customQuery != null) { + customQuery + } else if (!shouldQueryForMonuments) { + FileUtils.readFromResource("/queries/rectangle_query_for_nearby.rq") + } else { + FileUtils.readFromResource("/queries/rectangle_query_for_nearby_monuments.rq") + } + + val westCornerLat = screenTopRight.latitude + val westCornerLong = screenTopRight.longitude + val eastCornerLat = screenBottomLeft.latitude + val eastCornerLong = screenBottomLeft.longitude + + val query = wikidataQuery + .replace("\${LAT_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLat)) + .replace("\${LONG_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLong)) + .replace("\${LAT_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLat)) + .replace("\${LONG_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLong)) + .replace("\${LANG}", language) + val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!! + .newBuilder() + .addQueryParameter("query", query) + .addQueryParameter("format", "json") + + val request: Request = Request.Builder() + .url(urlBuilder.build()) + .build() + + val response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + val json = response.body!!.string() + val nearbyResponse = gson.fromJson(json, NearbyResponse::class.java) + val bindings = nearbyResponse.results.bindings + val places: MutableList = ArrayList() + for (item in bindings) { + val placeFromNearbyItem = Place.from(item) + if (shouldQueryForMonuments && item.getMonument() != null) { + placeFromNearbyItem.isMonument = true + } else { + placeFromNearbyItem.isMonument = false + } + places.add(placeFromNearbyItem) + } + return places + } + throw Exception(response.message) + } + + @Throws(IOException::class) + fun getPlaces( + placeList: List, language: String + ): List? { + val wikidataQuery = FileUtils.readFromResource("/queries/query_for_item.rq") + var qids = "" + for (place in placeList) { + qids += """ +${"wd:" + place.wikiDataEntityId}""" + } + val query = wikidataQuery + .replace("\${ENTITY}", qids) + .replace("\${LANG}", language) + val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!! + .newBuilder() + .addQueryParameter("query", query) + .addQueryParameter("format", "json") + + val request: Request = Request.Builder().url(urlBuilder.build()).build() + + okHttpClient.newCall(request).execute().use { response -> + if (response.isSuccessful) { + val json = response.body!!.string() + val nearbyResponse = gson.fromJson(json, NearbyResponse::class.java) + val bindings = nearbyResponse.results.bindings + val places: MutableList = ArrayList() + for (item in bindings) { + val placeFromNearbyItem = Place.from(item) + places.add(placeFromNearbyItem) + } + return places + } else { + throw IOException("Unexpected response code: " + response.code) + } + } + } + + @Throws(Exception::class) + fun getPlacesAsKML(leftLatLng: LatLng, rightLatLng: LatLng): String? { + var kmlString = """ + + + """ + val placeBindings = runQuery( + leftLatLng, + rightLatLng + ) + if (placeBindings != null) { + for ((item1, label, location, clas) in placeBindings) { + if (item1 != null && label != null && clas != null) { + val input = location.value + val pattern = Pattern.compile( + "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)" + ) + val matcher = pattern.matcher(input) + + if (matcher.find()) { + val longStr = matcher.group(1) + val latStr = matcher.group(2) + val itemUrl = item1.value + val itemName = label.value.replace("&", "&") + val itemLatitude = latStr + val itemLongitude = longStr + val itemClass = clas.value + + val formattedItemName = + if (!itemClass.isEmpty()) + "$itemName ($itemClass)" + else + itemName + + val kmlEntry = (""" + + $formattedItemName + $itemUrl + + $itemLongitude,$itemLatitude + + """) + kmlString = kmlString + kmlEntry + } else { + Timber.e("No match found") + } + } + } + } + kmlString = """$kmlString + + +""" + return kmlString + } + + @Throws(Exception::class) + fun getPlacesAsGPX(leftLatLng: LatLng, rightLatLng: LatLng): String? { + var gpxString = (""" + +""") + + val placeBindings = runQuery(leftLatLng, rightLatLng) + if (placeBindings != null) { + for ((item1, label, location, clas) in placeBindings) { + if (item1 != null && label != null && clas != null) { + val input = location.value + val pattern = Pattern.compile( + "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)" + ) + val matcher = pattern.matcher(input) + + if (matcher.find()) { + val longStr = matcher.group(1) + val latStr = matcher.group(2) + val itemUrl = item1.value + val itemName = label.value.replace("&", "&") + val itemLatitude = latStr + val itemLongitude = longStr + val itemClass = clas.value + + val formattedItemName = if (!itemClass.isEmpty()) + "$itemName ($itemClass)" + else + itemName + + val gpxEntry = + (""" + + $itemName + $itemUrl + """) + gpxString = gpxString + gpxEntry + } else { + Timber.e("No match found") + } + } + } + } + gpxString = "$gpxString\n" + return gpxString + } + + @Throws(IOException::class) + fun getChildDepictions( + qid: String, startPosition: Int, + limit: Int + ): Single> = + depictedItemsFrom(sparqlQuery(qid, startPosition, limit, "/queries/subclasses_query.rq")) + + @Throws(IOException::class) + fun getParentDepictions( + qid: String, startPosition: Int, + limit: Int + ): Single> = depictedItemsFrom( + sparqlQuery( + qid, + startPosition, + limit, + "/queries/parentclasses_query.rq" + ) + ) + + fun getCampaigns(): Single { + return Single.fromCallable({ + val request: Request = Request.Builder().url(campaignsUrl).build() + val response: Response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + val json: String = response.body!!.string() + return@fromCallable gson.fromJson( + json, + CampaignResponseDTO::class.java + ) + } + null + }) + } + + private fun depictedItemsFrom(request: Request): Single> { + return depictsClient.toDepictions(Single.fromCallable({ + okHttpClient.newCall(request).execute().body.use { body -> + return@fromCallable gson.fromJson( + body!!.string(), + SparqlResponse::class.java + ) + } + }).doOnError({ t: Throwable? -> Timber.e(t) })) + } + + @Throws(IOException::class) + private fun sparqlQuery( + qid: String, + startPosition: Int, + limit: Int, + fileName: String + ): Request { + val query = FileUtils.readFromResource(fileName) + .replace("\${QID}", qid) + .replace("\${LANG}", "\"" + Locale.getDefault().language + "\"") + .replace("\${LIMIT}", "" + limit) + .replace("\${OFFSET}", "" + startPosition) + val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!! + .newBuilder() + .addQueryParameter("query", query) + .addQueryParameter("format", "json") + return Request.Builder().url(urlBuilder.build()).build() + } + + @Throws(IOException::class) + private fun runQuery(currentLatLng: LatLng, nextLatLng: LatLng): List? { + val wikidataQuery = FileUtils.readFromResource("/queries/places_query.rq") + val query = wikidataQuery + .replace("\${LONGITUDE}", String.format(Locale.ROOT, "%.2f", currentLatLng.longitude)) + .replace("\${LATITUDE}", String.format(Locale.ROOT, "%.4f", currentLatLng.latitude)) + .replace("\${NEXT_LONGITUDE}", String.format(Locale.ROOT, "%.4f", nextLatLng.longitude)) + .replace("\${NEXT_LATITUDE}", String.format(Locale.ROOT, "%.4f", nextLatLng.latitude)) + + val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!! + .newBuilder() + .addQueryParameter("query", query) + .addQueryParameter("format", "json") + + val request: Request = Request.Builder().url(urlBuilder.build()).build() + + val response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + val json = response.body!!.string() + val item = gson.fromJson(json, ItemsClass::class.java) + return item.results.bindings + } else { + return null + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt index 68c6f13fb..d51ab1796 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt @@ -194,7 +194,7 @@ class FileProcessor requireNotNull(imageCoordinates.decimalCoords) compositeDisposable.add( apiCall - .request(imageCoordinates.decimalCoords) + .request(imageCoordinates.decimalCoords!!) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .subscribe( @@ -220,7 +220,7 @@ class FileProcessor .concatMap { Observable.fromCallable { okHttpJsonApiClient.getNearbyPlaces( - imageCoordinates.latLng, + imageCoordinates.latLng!!, Locale.getDefault().language, it, ) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index 00cd29a6d..ae2c461f8 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -496,14 +496,14 @@ class UploadWorker( withContext(Dispatchers.Main) { wikidataEditService.handleImageClaimResult( - contribution.wikidataPlace, + contribution.wikidataPlace!!, revisionID, ) } } else { withContext(Dispatchers.Main) { wikidataEditService.handleImageClaimResult( - contribution.wikidataPlace, + contribution.wikidataPlace!!, null, ) } diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt index ca523a21f..bc0ba24fa 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt @@ -10,11 +10,10 @@ class CommonsServiceFactory( ) { val builder: Retrofit.Builder by lazy { // All instances of retrofit share this configuration, but create it lazily - Retrofit - .Builder() + Retrofit.Builder() .client(okHttpClient) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson())) + .addConverterFactory(GsonConverterFactory.create(GsonUtil.defaultGson)) } val retrofitCache: MutableMap = mutableMapOf() diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.java b/app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.java deleted file mode 100644 index c9d37eda5..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.java +++ /dev/null @@ -1,34 +0,0 @@ -package fr.free.nrw.commons.wikidata; - -import android.net.Uri; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import fr.free.nrw.commons.wikidata.json.RequiredFieldsCheckOnReadTypeAdapterFactory; -import fr.free.nrw.commons.wikidata.model.DataValue; -import fr.free.nrw.commons.wikidata.model.WikiSite; -import fr.free.nrw.commons.wikidata.json.NamespaceTypeAdapter; -import fr.free.nrw.commons.wikidata.json.PostProcessingTypeAdapter; -import fr.free.nrw.commons.wikidata.json.UriTypeAdapter; -import fr.free.nrw.commons.wikidata.json.WikiSiteTypeAdapter; -import fr.free.nrw.commons.wikidata.model.page.Namespace; - -public final class GsonUtil { - private static final String DATE_FORMAT = "MMM dd, yyyy HH:mm:ss"; - - private static final GsonBuilder DEFAULT_GSON_BUILDER = new GsonBuilder() - .setDateFormat(DATE_FORMAT) - .registerTypeAdapterFactory(DataValue.getPolymorphicTypeAdapter()) - .registerTypeHierarchyAdapter(Uri.class, new UriTypeAdapter().nullSafe()) - .registerTypeHierarchyAdapter(Namespace.class, new NamespaceTypeAdapter().nullSafe()) - .registerTypeAdapter(WikiSite.class, new WikiSiteTypeAdapter().nullSafe()) - .registerTypeAdapterFactory(new RequiredFieldsCheckOnReadTypeAdapterFactory()) - .registerTypeAdapterFactory(new PostProcessingTypeAdapter()); - - private static final Gson DEFAULT_GSON = DEFAULT_GSON_BUILDER.create(); - - public static Gson getDefaultGson() { - return DEFAULT_GSON; - } - - private GsonUtil() { } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.kt new file mode 100644 index 000000000..1a0ae0aeb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.kt @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.wikidata + +import android.net.Uri +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import fr.free.nrw.commons.wikidata.json.NamespaceTypeAdapter +import fr.free.nrw.commons.wikidata.json.PostProcessingTypeAdapter +import fr.free.nrw.commons.wikidata.json.RequiredFieldsCheckOnReadTypeAdapterFactory +import fr.free.nrw.commons.wikidata.json.UriTypeAdapter +import fr.free.nrw.commons.wikidata.json.WikiSiteTypeAdapter +import fr.free.nrw.commons.wikidata.model.DataValue.Companion.polymorphicTypeAdapter +import fr.free.nrw.commons.wikidata.model.WikiSite +import fr.free.nrw.commons.wikidata.model.page.Namespace + +object GsonUtil { + private const val DATE_FORMAT = "MMM dd, yyyy HH:mm:ss" + + private val DEFAULT_GSON_BUILDER: GsonBuilder by lazy { + GsonBuilder().setDateFormat(DATE_FORMAT) + .registerTypeAdapterFactory(polymorphicTypeAdapter) + .registerTypeHierarchyAdapter(Uri::class.java, UriTypeAdapter().nullSafe()) + .registerTypeHierarchyAdapter(Namespace::class.java, NamespaceTypeAdapter().nullSafe()) + .registerTypeAdapter(WikiSite::class.java, WikiSiteTypeAdapter().nullSafe()) + .registerTypeAdapterFactory(RequiredFieldsCheckOnReadTypeAdapterFactory()) + .registerTypeAdapterFactory(PostProcessingTypeAdapter()) + } + + val defaultGson: Gson by lazy { DEFAULT_GSON_BUILDER.create() } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java deleted file mode 100644 index f89b5aee0..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java +++ /dev/null @@ -1,11 +0,0 @@ -package fr.free.nrw.commons.wikidata; - -public class WikidataConstants { - public static final String PLACE_OBJECT = "place"; - public static final String BOOKMARKS_ITEMS = "bookmarks.items"; - public static final String SELECTED_NEARBY_PLACE = "selected.nearby.place"; - public static final String SELECTED_NEARBY_PLACE_CATEGORY = "selected.nearby.place.category"; - - public static final String MW_API_PREFIX = "w/api.php?format=json&formatversion=2&errorformat=plaintext&"; - public static final String WIKIPEDIA_URL = "https://wikipedia.org/"; -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.kt new file mode 100644 index 000000000..6343342cb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.kt @@ -0,0 +1,11 @@ +package fr.free.nrw.commons.wikidata + +object WikidataConstants { + const val PLACE_OBJECT: String = "place" + const val BOOKMARKS_ITEMS: String = "bookmarks.items" + const val SELECTED_NEARBY_PLACE: String = "selected.nearby.place" + const val SELECTED_NEARBY_PLACE_CATEGORY: String = "selected.nearby.place.category" + + const val MW_API_PREFIX: String = "w/api.php?format=json&formatversion=2&errorformat=plaintext&" + const val WIKIPEDIA_URL: String = "https://wikipedia.org/" +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java deleted file mode 100644 index 30fb26ddc..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java +++ /dev/null @@ -1,16 +0,0 @@ -package fr.free.nrw.commons.wikidata; - -public abstract class WikidataEditListener { - - protected WikidataP18EditListener wikidataP18EditListener; - - public abstract void onSuccessfulWikidataEdit(); - - public void setAuthenticationStateListener(WikidataP18EditListener wikidataP18EditListener) { - this.wikidataP18EditListener = wikidataP18EditListener; - } - - public interface WikidataP18EditListener { - void onWikidataEditSuccessful(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.kt new file mode 100644 index 000000000..5e382b4ce --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.kt @@ -0,0 +1,11 @@ +package fr.free.nrw.commons.wikidata + +abstract class WikidataEditListener { + var authenticationStateListener: WikidataP18EditListener? = null + + abstract fun onSuccessfulWikidataEdit() + + interface WikidataP18EditListener { + fun onWikidataEditSuccessful() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java deleted file mode 100644 index a97d0eded..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java +++ /dev/null @@ -1,20 +0,0 @@ -package fr.free.nrw.commons.wikidata; - -/** - * Listener for wikidata edits - */ -public class WikidataEditListenerImpl extends WikidataEditListener { - - public WikidataEditListenerImpl() { - } - - /** - * Fired when wikidata P18 edit is successful. If there's an active listener, then it is fired - */ - @Override - public void onSuccessfulWikidataEdit() { - if (wikidataP18EditListener != null) { - wikidataP18EditListener.onWikidataEditSuccessful(); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.kt new file mode 100644 index 000000000..6827ab30c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.kt @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.wikidata + +/** + * Listener for wikidata edits + */ +class WikidataEditListenerImpl : WikidataEditListener() { + /** + * Fired when wikidata P18 edit is successful. If there's an active listener, then it is fired + */ + override fun onSuccessfulWikidataEdit() { + authenticationStateListener?.onWikidataEditSuccessful() + } +} 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 deleted file mode 100644 index 21567f5e4..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java +++ /dev/null @@ -1,271 +0,0 @@ -package fr.free.nrw.commons.wikidata; - - -import static fr.free.nrw.commons.media.MediaClientKt.PAGE_ID_PREFIX; - -import android.annotation.SuppressLint; -import android.content.Context; -import androidx.annotation.Nullable; -import com.google.gson.Gson; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.upload.UploadResult; -import fr.free.nrw.commons.upload.WikidataItem; -import fr.free.nrw.commons.upload.WikidataPlace; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import fr.free.nrw.commons.wikidata.model.DataValue; -import fr.free.nrw.commons.wikidata.model.DataValue.ValueString; -import fr.free.nrw.commons.wikidata.model.EditClaim; -import fr.free.nrw.commons.wikidata.model.RemoveClaim; -import fr.free.nrw.commons.wikidata.model.SnakPartial; -import fr.free.nrw.commons.wikidata.model.StatementPartial; -import fr.free.nrw.commons.wikidata.model.WikiBaseMonolingualTextValue; -import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse; -import io.reactivex.Observable; -import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.UUID; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import timber.log.Timber; - -/** - * This class is meant to handle the Wikidata edits made through the app It will talk with MediaWiki - * Apis to make the necessary calls, log the edits and fire listeners on successful edits - */ -@Singleton -public class WikidataEditService { - - public static final String COMMONS_APP_TAG = "wikimedia-commons-app"; - - private final Context context; - private final WikidataEditListener wikidataEditListener; - private final JsonKvStore directKvStore; - private final WikiBaseClient wikiBaseClient; - private final WikidataClient wikidataClient; - private final Gson gson; - - @Inject - public WikidataEditService(final Context context, - final WikidataEditListener wikidataEditListener, - @Named("default_preferences") final JsonKvStore directKvStore, - final WikiBaseClient wikiBaseClient, - final WikidataClient wikidataClient, final Gson gson) { - this.context = context; - this.wikidataEditListener = wikidataEditListener; - this.directKvStore = directKvStore; - this.wikiBaseClient = wikiBaseClient; - this.wikidataClient = wikidataClient; - this.gson = gson; - } - - /** - * Edits the wikibase entity by adding DEPICTS property. Adding DEPICTS property requires call - * to the wikibase API to set tag against the entity. - */ - @SuppressLint("CheckResult") - private Observable addDepictsProperty( - final String fileEntityId, - final List depictedItems - ) { - final EditClaim data = editClaim( - ConfigUtils.isBetaFlavour() ? Collections.singletonList("Q10") - // Wikipedia:Sandbox (Q10) - : depictedItems - ); - - return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data)) - .doOnNext(success -> { - if (success) { - Timber.d("DEPICTS property was set successfully for %s", fileEntityId); - } else { - Timber.d("Unable to set DEPICTS property for %s", fileEntityId); - } - }) - .doOnError(throwable -> { - Timber.e(throwable, "Error occurred while setting DEPICTS property"); - ViewUtil.showLongToast(context, throwable.toString()); - }) - .subscribeOn(Schedulers.io()); - } - - /** - * Takes depicts ID as a parameter and create a uploadable data with the Id - * and send the data for POST operation - * - * @param fileEntityId ID of the file - * @param depictedItems IDs of the selected depict item - * @return Observable - */ - @SuppressLint("CheckResult") - public Observable updateDepictsProperty( - final String fileEntityId, - final List depictedItems - ) { - final String entityId = PAGE_ID_PREFIX + fileEntityId; - final List claimIds = getDepictionsClaimIds(entityId); - - final RemoveClaim data = removeClaim( /* Please consider removeClaim scenario for BetaDebug */ - ConfigUtils.isBetaFlavour() ? Collections.singletonList("Q10") - // Wikipedia:Sandbox (Q10) - : claimIds - ); - - return wikiBaseClient.postDeleteClaims(entityId, gson.toJson(data)) - .doOnError(throwable -> { - Timber.e(throwable, "Error occurred while removing existing claims for DEPICTS property"); - ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); - }).switchMap(success-> { - if(success) { - Timber.d("DEPICTS property was deleted successfully"); - return addDepictsProperty(fileEntityId, depictedItems); - } else { - Timber.d("Unable to delete DEPICTS property"); - return Observable.empty(); - } - }); - } - - @SuppressLint("CheckResult") - private List getDepictionsClaimIds(final String entityId) { - return wikiBaseClient.getClaimIdsByProperty(entityId, WikidataProperties.DEPICTS.getPropertyName()) - .subscribeOn(Schedulers.io()) - .blockingFirst(); - } - - private EditClaim editClaim(final List entityIds) { - return EditClaim.from(entityIds, WikidataProperties.DEPICTS.getPropertyName()); - } - - private RemoveClaim removeClaim(final List claimIds) { - return RemoveClaim.from(claimIds); - } - - /** - * Show a success toast when the edit is made successfully - */ - private void showSuccessToast(final String wikiItemName) { - final String successStringTemplate = context.getString(R.string.successful_wikidata_edit); - final String successMessage = String - .format(Locale.getDefault(), successStringTemplate, wikiItemName); - ViewUtil.showLongToast(context, successMessage); - } - - /** - * Adds label to Wikidata using the fileEntityId and the edit token, obtained from - * csrfTokenClient - * - * @param fileEntityId - * @return - */ - @SuppressLint("CheckResult") - private Observable addCaption(final long fileEntityId, final String languageCode, - final String captionValue) { - return wikiBaseClient.addLabelsToWikidata(fileEntityId, languageCode, captionValue) - .doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse)) - .doOnError(throwable -> { - Timber.e(throwable, "Error occurred while setting Captions"); - ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); - }) - .map(mwPostResponse -> mwPostResponse != null); - } - - private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) { - if (response != null) { - Timber.d("Caption successfully set, revision id = %s", response); - } else { - Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId); - } - } - - public Long createClaim(@Nullable final WikidataPlace wikidataPlace, final String fileName, - final Map captions) { - if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) { - Timber - .d("Image location and nearby place location mismatched, so Wikidata item won't be edited"); - return null; - } - return addImageAndMediaLegends(wikidataPlace, fileName, captions); - } - - public Long addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName, - final Map captions) { - final SnakPartial p18 = new SnakPartial("value", - WikidataProperties.IMAGE.getPropertyName(), - new ValueString(fileName.replace("File:", ""))); - - final List snaks = new ArrayList<>(); - for (final Map.Entry entry : captions.entrySet()) { - snaks.add(new SnakPartial("value", - WikidataProperties.MEDIA_LEGENDS.getPropertyName(), new DataValue.MonoLingualText( - new WikiBaseMonolingualTextValue(entry.getValue(), entry.getKey())))); - } - - final String id = wikidataItem.getId() + "$" + UUID.randomUUID().toString(); - final StatementPartial claim = new StatementPartial(p18, "statement", "normal", id, - Collections.singletonMap(WikidataProperties.MEDIA_LEGENDS.getPropertyName(), snaks), - Arrays.asList(WikidataProperties.MEDIA_LEGENDS.getPropertyName())); - - return wikidataClient.setClaim(claim, COMMONS_APP_TAG).blockingSingle(); - } - - public void handleImageClaimResult(final WikidataItem wikidataItem, final Long revisionId) { - if (revisionId != null) { - if (wikidataEditListener != null) { - wikidataEditListener.onSuccessfulWikidataEdit(); - } - showSuccessToast(wikidataItem.getName()); - } else { - Timber.d("Unable to make wiki data edit for entity %s", wikidataItem); - ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); - } - } - - public Observable addDepictionsAndCaptions( - final UploadResult uploadResult, - final Contribution contribution - ) { - return wikiBaseClient.getFileEntityId(uploadResult) - .doOnError(throwable -> { - Timber - .e(throwable, "Error occurred while getting EntityID to set DEPICTS property"); - ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); - }) - .switchMap(fileEntityId -> { - if (fileEntityId != null) { - Timber.d("EntityId for image was received successfully: %s", fileEntityId); - return Observable.concat( - depictionEdits(contribution, fileEntityId), - captionEdits(contribution, fileEntityId) - ); - } else { - Timber.d("Error acquiring EntityId for image: %s", uploadResult); - return Observable.empty(); - } - } - ); - } - - private Observable captionEdits(Contribution contribution, Long fileEntityId) { - return Observable.fromIterable(contribution.getMedia().getCaptions().entrySet()) - .concatMap(entry -> addCaption(fileEntityId, entry.getKey(), entry.getValue())); - } - - private Observable depictionEdits(Contribution contribution, Long fileEntityId) { - final List depictIDs = new ArrayList<>(); - for (final WikidataItem wikidataItem : - contribution.getDepictedItems()) { - depictIDs.add(wikidataItem.getId()); - } - return addDepictsProperty(fileEntityId.toString(), depictIDs); - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.kt new file mode 100644 index 000000000..396f92824 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.kt @@ -0,0 +1,252 @@ +package fr.free.nrw.commons.wikidata + +import android.annotation.SuppressLint +import android.content.Context +import com.google.gson.Gson +import fr.free.nrw.commons.R +import fr.free.nrw.commons.contributions.Contribution +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.media.PAGE_ID_PREFIX +import fr.free.nrw.commons.upload.UploadResult +import fr.free.nrw.commons.upload.WikidataItem +import fr.free.nrw.commons.upload.WikidataPlace +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.utils.ViewUtil.showLongToast +import fr.free.nrw.commons.wikidata.WikidataProperties.DEPICTS +import fr.free.nrw.commons.wikidata.WikidataProperties.IMAGE +import fr.free.nrw.commons.wikidata.WikidataProperties.MEDIA_LEGENDS +import fr.free.nrw.commons.wikidata.model.DataValue.MonoLingualText +import fr.free.nrw.commons.wikidata.model.DataValue.ValueString +import fr.free.nrw.commons.wikidata.model.EditClaim +import fr.free.nrw.commons.wikidata.model.RemoveClaim +import fr.free.nrw.commons.wikidata.model.SnakPartial +import fr.free.nrw.commons.wikidata.model.StatementPartial +import fr.free.nrw.commons.wikidata.model.WikiBaseMonolingualTextValue +import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse +import io.reactivex.Observable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import java.util.Arrays +import java.util.Collections +import java.util.Locale +import java.util.Objects +import java.util.UUID +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + + +/** + * This class is meant to handle the Wikidata edits made through the app It will talk with MediaWiki + * Apis to make the necessary calls, log the edits and fire listeners on successful edits + */ +@Singleton +class WikidataEditService @Inject constructor( + private val context: Context, + private val wikidataEditListener: WikidataEditListener?, + @param:Named("default_preferences") private val directKvStore: JsonKvStore, + private val wikiBaseClient: WikiBaseClient, + private val wikidataClient: WikidataClient, private val gson: Gson +) { + @SuppressLint("CheckResult") + private fun addDepictsProperty( + fileEntityId: String, + depictedItems: List + ): Observable { + val data = EditClaim.from( + if (isBetaFlavour) listOf("Q10") else depictedItems, DEPICTS.propertyName + ) + + return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data)) + .doOnNext { success: Boolean -> + if (success) { + Timber.d("DEPICTS property was set successfully for %s", fileEntityId) + } else { + Timber.d("Unable to set DEPICTS property for %s", fileEntityId) + } + } + .doOnError { throwable: Throwable -> + Timber.e(throwable, "Error occurred while setting DEPICTS property") + showLongToast(context, throwable.toString()) + } + .subscribeOn(Schedulers.io()) + } + + @SuppressLint("CheckResult") + fun updateDepictsProperty( + fileEntityId: String?, + depictedItems: List + ): Observable { + val entityId: String = PAGE_ID_PREFIX + fileEntityId + val claimIds = getDepictionsClaimIds(entityId) + + /* Please consider removeClaim scenario for BetaDebug */ + val data = RemoveClaim.from(if (isBetaFlavour) listOf("Q10") else claimIds) + + return wikiBaseClient.postDeleteClaims(entityId, gson.toJson(data)) + .doOnError { throwable: Throwable? -> + Timber.e( + throwable, + "Error occurred while removing existing claims for DEPICTS property" + ) + showLongToast( + context, + context.getString(R.string.wikidata_edit_failure) + ) + }.switchMap { success: Boolean -> + if (success) { + Timber.d("DEPICTS property was deleted successfully") + return@switchMap addDepictsProperty(fileEntityId!!, depictedItems) + } else { + Timber.d("Unable to delete DEPICTS property") + return@switchMap Observable.empty() + } + } + } + + @SuppressLint("CheckResult") + private fun getDepictionsClaimIds(entityId: String): List { + return wikiBaseClient.getClaimIdsByProperty(entityId, DEPICTS.propertyName) + .subscribeOn(Schedulers.io()) + .blockingFirst() + } + + private fun showSuccessToast(wikiItemName: String) { + val successStringTemplate = context.getString(R.string.successful_wikidata_edit) + val successMessage = String.format(Locale.getDefault(), successStringTemplate, wikiItemName) + showLongToast(context, successMessage) + } + + @SuppressLint("CheckResult") + private fun addCaption( + fileEntityId: Long, languageCode: String, + captionValue: String + ): Observable { + return wikiBaseClient.addLabelsToWikidata(fileEntityId, languageCode, captionValue) + .doOnNext { mwPostResponse: MwPostResponse? -> + onAddCaptionResponse( + fileEntityId, + mwPostResponse + ) + } + .doOnError { throwable: Throwable? -> + Timber.e(throwable, "Error occurred while setting Captions") + showLongToast( + context, + context.getString(R.string.wikidata_edit_failure) + ) + } + .map(Objects::nonNull) + } + + private fun onAddCaptionResponse(fileEntityId: Long, response: MwPostResponse?) { + if (response != null) { + Timber.d("Caption successfully set, revision id = %s", response) + } else { + Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId) + } + } + + fun createClaim( + wikidataPlace: WikidataPlace?, fileName: String, + captions: Map + ): Long? { + if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) { + Timber.d( + "Image location and nearby place location mismatched, so Wikidata item won't be edited" + ) + return null + } + return addImageAndMediaLegends(wikidataPlace!!, fileName, captions) + } + + fun addImageAndMediaLegends( + wikidataItem: WikidataItem, fileName: String, + captions: Map + ): Long { + val p18 = SnakPartial( + "value", + IMAGE.propertyName, + ValueString(fileName.replace("File:", "")) + ) + + val snaks: MutableList = ArrayList() + for ((key, value) in captions) { + snaks.add( + SnakPartial( + "value", + MEDIA_LEGENDS.propertyName, MonoLingualText( + WikiBaseMonolingualTextValue(value!!, key!!) + ) + ) + ) + } + + val id = wikidataItem.id + "$" + UUID.randomUUID().toString() + val claim = StatementPartial( + p18, "statement", "normal", id, Collections.singletonMap>( + MEDIA_LEGENDS.propertyName, snaks + ), Arrays.asList(MEDIA_LEGENDS.propertyName) + ) + + return wikidataClient.setClaim(claim, COMMONS_APP_TAG).blockingSingle() + } + + fun handleImageClaimResult(wikidataItem: WikidataItem, revisionId: Long?) { + if (revisionId != null) { + wikidataEditListener?.onSuccessfulWikidataEdit() + showSuccessToast(wikidataItem.name) + } else { + Timber.d("Unable to make wiki data edit for entity %s", wikidataItem) + showLongToast(context, context.getString(R.string.wikidata_edit_failure)) + } + } + + fun addDepictionsAndCaptions( + uploadResult: UploadResult, + contribution: Contribution + ): Observable { + return wikiBaseClient.getFileEntityId(uploadResult) + .doOnError { throwable: Throwable? -> + Timber.e( + throwable, + "Error occurred while getting EntityID to set DEPICTS property" + ) + showLongToast( + context, + context.getString(R.string.wikidata_edit_failure) + ) + } + .switchMap { fileEntityId: Long? -> + if (fileEntityId != null) { + Timber.d("EntityId for image was received successfully: %s", fileEntityId) + return@switchMap Observable.concat( + depictionEdits(contribution, fileEntityId), + captionEdits(contribution, fileEntityId) + ) + } else { + Timber.d("Error acquiring EntityId for image: %s", uploadResult) + return@switchMap Observable.empty() + } + } + } + + private fun captionEdits(contribution: Contribution, fileEntityId: Long): Observable { + return Observable.fromIterable(contribution.media.captions.entries) + .concatMap { addCaption(fileEntityId, it.key, it.value) } + } + + private fun depictionEdits( + contribution: Contribution, + fileEntityId: Long + ): Observable = addDepictsProperty(fileEntityId.toString(), buildList { + for ((_, _, _, _, _, _, id) in contribution.depictedItems) { + add(id) + } + }) + + companion object { + const val COMMONS_APP_TAG: String = "wikimedia-commons-app" + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.java deleted file mode 100644 index cc6dcc9f9..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.java +++ /dev/null @@ -1,29 +0,0 @@ -package fr.free.nrw.commons.wikidata.json; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; - -import fr.free.nrw.commons.wikidata.model.page.Namespace; - -import java.io.IOException; - -public class NamespaceTypeAdapter extends TypeAdapter { - - @Override - public void write(JsonWriter out, Namespace namespace) throws IOException { - out.value(namespace.code()); - } - - @Override - public Namespace read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.STRING) { - // Prior to 3210ce44, we marshaled Namespace as the name string of the enum, instead of - // the code number. This introduces a backwards-compatible check for the string value. - // TODO: remove after April 2017, when all older namespaces have been deserialized. - return Namespace.valueOf(in.nextString()); - } - return Namespace.of(in.nextInt()); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.kt new file mode 100644 index 000000000..09f1dc5e8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.kt @@ -0,0 +1,26 @@ +package fr.free.nrw.commons.wikidata.json + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import fr.free.nrw.commons.wikidata.model.page.Namespace +import java.io.IOException + +class NamespaceTypeAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter, namespace: Namespace) { + out.value(namespace.code().toLong()) + } + + @Throws(IOException::class) + override fun read(reader: JsonReader): Namespace { + if (reader.peek() == JsonToken.STRING) { + // Prior to 3210ce44, we marshaled Namespace as the name string of the enum, instead of + // the code number. This introduces a backwards-compatible check for the string value. + // TODO: remove after April 2017, when all older namespaces have been deserialized. + return Namespace.valueOf(reader.nextString()) + } + return Namespace.of(reader.nextInt()) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.java deleted file mode 100644 index b6b67d4d2..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.java +++ /dev/null @@ -1,34 +0,0 @@ -package fr.free.nrw.commons.wikidata.json; - -import com.google.gson.Gson; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import java.io.IOException; - -public class PostProcessingTypeAdapter implements TypeAdapterFactory { - public interface PostProcessable { - void postProcess(); - } - - public TypeAdapter create(Gson gson, TypeToken type) { - final TypeAdapter delegate = gson.getDelegateAdapter(this, type); - - return new TypeAdapter() { - public void write(JsonWriter out, T value) throws IOException { - delegate.write(out, value); - } - - public T read(JsonReader in) throws IOException { - T obj = delegate.read(in); - if (obj instanceof PostProcessable) { - ((PostProcessable)obj).postProcess(); - } - return obj; - } - }; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.kt new file mode 100644 index 000000000..cf07eabf4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.kt @@ -0,0 +1,35 @@ +package fr.free.nrw.commons.wikidata.json + +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import java.io.IOException + +class PostProcessingTypeAdapter : TypeAdapterFactory { + interface PostProcessable { + fun postProcess() + } + + override fun create(gson: Gson, type: TypeToken): TypeAdapter { + val delegate = gson.getDelegateAdapter(this, type) + + return object : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter, value: T) { + delegate.write(out, value) + } + + @Throws(IOException::class) + override fun read(reader: JsonReader): T { + val obj = delegate.read(reader) + if (obj is PostProcessable) { + (obj as PostProcessable).postProcess() + } + return obj + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.java deleted file mode 100644 index c01b9fe66..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.java +++ /dev/null @@ -1,94 +0,0 @@ -package fr.free.nrw.commons.wikidata.json; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.collection.ArraySet; - -import com.google.gson.Gson; -import com.google.gson.JsonParseException; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import fr.free.nrw.commons.wikidata.json.annotations.Required; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.Collections; -import java.util.Set; - -/** - * TypeAdapterFactory that provides TypeAdapters that return null values for objects that are - * missing fields annotated with @Required. - * - * BEWARE: This means that a List or other Collection of objects that have @Required fields can - * contain null elements after deserialization! - * - * TODO: Handle null values in lists during deserialization, perhaps with a new @RequiredElements - * annotation and another corresponding TypeAdapter(Factory). - */ -public class RequiredFieldsCheckOnReadTypeAdapterFactory implements TypeAdapterFactory { - @Nullable @Override public final TypeAdapter create(@NonNull Gson gson, @NonNull TypeToken typeToken) { - Class rawType = typeToken.getRawType(); - Set requiredFields = collectRequiredFields(rawType); - - if (requiredFields.isEmpty()) { - return null; - } - - setFieldsAccessible(requiredFields, true); - return new Adapter<>(gson.getDelegateAdapter(this, typeToken), requiredFields); - } - - @NonNull private Set collectRequiredFields(@NonNull Class clazz) { - Field[] fields = clazz.getDeclaredFields(); - Set required = new ArraySet<>(); - for (Field field : fields) { - if (field.isAnnotationPresent(Required.class)) { - required.add(field); - } - } - return Collections.unmodifiableSet(required); - } - - private void setFieldsAccessible(Iterable fields, boolean accessible) { - for (Field field : fields) { - field.setAccessible(accessible); - } - } - - private static final class Adapter extends TypeAdapter { - @NonNull private final TypeAdapter delegate; - @NonNull private final Set requiredFields; - - private Adapter(@NonNull TypeAdapter delegate, @NonNull final Set requiredFields) { - this.delegate = delegate; - this.requiredFields = requiredFields; - } - - @Override public void write(JsonWriter out, T value) throws IOException { - delegate.write(out, value); - } - - @Override @Nullable public T read(JsonReader in) throws IOException { - T deserialized = delegate.read(in); - return allRequiredFieldsPresent(deserialized, requiredFields) ? deserialized : null; - } - - private boolean allRequiredFieldsPresent(@NonNull T deserialized, - @NonNull Set required) { - for (Field field : required) { - try { - if (field.get(deserialized) == null) { - return false; - } - } catch (IllegalArgumentException | IllegalAccessException e) { - throw new JsonParseException(e); - } - } - return true; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.kt new file mode 100644 index 000000000..ec26e8345 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.kt @@ -0,0 +1,75 @@ +package fr.free.nrw.commons.wikidata.json + +import com.google.gson.Gson +import com.google.gson.JsonParseException +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import fr.free.nrw.commons.wikidata.json.annotations.Required +import java.io.IOException +import java.lang.reflect.Field + +/** + * TypeAdapterFactory that provides TypeAdapters that return null values for objects that are + * missing fields annotated with @Required. + * + * BEWARE: This means that a List or other Collection of objects that have @Required fields can + * contain null elements after deserialization! + * + * TODO: Handle null values in lists during deserialization, perhaps with a new @RequiredElements + * annotation and another corresponding TypeAdapter(Factory). + */ +class RequiredFieldsCheckOnReadTypeAdapterFactory : TypeAdapterFactory { + override fun create(gson: Gson, typeToken: TypeToken): TypeAdapter? { + val rawType: Class<*> = typeToken.rawType + val requiredFields = collectRequiredFields(rawType) + + if (requiredFields.isEmpty()) { + return null + } + + for (field in requiredFields) { + field.isAccessible = true + } + + return Adapter(gson.getDelegateAdapter(this, typeToken), requiredFields) + } + + private fun collectRequiredFields(clazz: Class<*>): Set = buildSet { + for (field in clazz.declaredFields) { + if (field.isAnnotationPresent(Required::class.java)) add(field) + } + } + + private class Adapter( + private val delegate: TypeAdapter, + private val requiredFields: Set + ) : TypeAdapter() { + + @Throws(IOException::class) + override fun write(out: JsonWriter, value: T?) = + delegate.write(out, value) + + @Throws(IOException::class) + override fun read(reader: JsonReader): T? = + if (allRequiredFieldsPresent(delegate.read(reader), requiredFields)) + delegate.read(reader) + else + null + + fun allRequiredFieldsPresent(deserialized: T, required: Set): Boolean { + for (field in required) { + try { + if (field[deserialized] == null) return false + } catch (e: IllegalArgumentException) { + throw JsonParseException(e) + } catch (e: IllegalAccessException) { + throw JsonParseException(e) + } + } + return true + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.java deleted file mode 100644 index 828dfbd68..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.java +++ /dev/null @@ -1,280 +0,0 @@ -package fr.free.nrw.commons.wikidata.json; - -/* - * Copyright (C) 2011 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import android.util.Log; -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; - -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.internal.Streams; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -/** - * Adapts values whose runtime type may differ from their declaration type. This - * is necessary when a field's type is not the same type that GSON should create - * when deserializing that field. For example, consider these types: - *
   {@code
- *   abstract class Shape {
- *     int x;
- *     int y;
- *   }
- *   class Circle extends Shape {
- *     int radius;
- *   }
- *   class Rectangle extends Shape {
- *     int width;
- *     int height;
- *   }
- *   class Diamond extends Shape {
- *     int width;
- *     int height;
- *   }
- *   class Drawing {
- *     Shape bottomShape;
- *     Shape topShape;
- *   }
- * }
- *

Without additional type information, the serialized JSON is ambiguous. Is - * the bottom shape in this drawing a rectangle or a diamond?

   {@code
- *   {
- *     "bottomShape": {
- *       "width": 10,
- *       "height": 5,
- *       "x": 0,
- *       "y": 0
- *     },
- *     "topShape": {
- *       "radius": 2,
- *       "x": 4,
- *       "y": 1
- *     }
- *   }}
- * This class addresses this problem by adding type information to the - * serialized JSON and honoring that type information when the JSON is - * deserialized:
   {@code
- *   {
- *     "bottomShape": {
- *       "type": "Diamond",
- *       "width": 10,
- *       "height": 5,
- *       "x": 0,
- *       "y": 0
- *     },
- *     "topShape": {
- *       "type": "Circle",
- *       "radius": 2,
- *       "x": 4,
- *       "y": 1
- *     }
- *   }}
- * Both the type field name ({@code "type"}) and the type labels ({@code - * "Rectangle"}) are configurable. - * - *

Registering Types

- * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field - * name to the {@link #of} factory method. If you don't supply an explicit type - * field name, {@code "type"} will be used.
   {@code
- *   RuntimeTypeAdapterFactory shapeAdapterFactory
- *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
- * }
- * Next register all of your subtypes. Every subtype must be explicitly - * registered. This protects your application from injection attacks. If you - * don't supply an explicit type label, the type's simple name will be used. - *
   {@code
- *   shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
- *   shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
- *   shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
- * }
- * Finally, register the type adapter factory in your application's GSON builder: - *
   {@code
- *   Gson gson = new GsonBuilder()
- *       .registerTypeAdapterFactory(shapeAdapterFactory)
- *       .create();
- * }
- * Like {@code GsonBuilder}, this API supports chaining:
   {@code
- *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
- *       .registerSubtype(Rectangle.class)
- *       .registerSubtype(Circle.class)
- *       .registerSubtype(Diamond.class);
- * }
- * - *

Serialization and deserialization

- * In order to serialize and deserialize a polymorphic object, - * you must specify the base type explicitly. - *
   {@code
- *   Diamond diamond = new Diamond();
- *   String json = gson.toJson(diamond, Shape.class);
- * }
- * And then: - *
   {@code
- *   Shape shape = gson.fromJson(json, Shape.class);
- * }
- */ -public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { - private final Class baseType; - private final String typeFieldName; - private final Map> labelToSubtype = new LinkedHashMap>(); - private final Map, String> subtypeToLabel = new LinkedHashMap, String>(); - private final boolean maintainType; - - private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName, boolean maintainType) { - if (typeFieldName == null || baseType == null) { - throw new NullPointerException(); - } - this.baseType = baseType; - this.typeFieldName = typeFieldName; - this.maintainType = maintainType; - } - - /** - * Creates a new runtime type adapter using for {@code baseType} using {@code - * typeFieldName} as the type field name. Type field names are case sensitive. - * {@code maintainType} flag decide if the type will be stored in pojo or not. - */ - public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { - return new RuntimeTypeAdapterFactory(baseType, typeFieldName, maintainType); - } - - /** - * Creates a new runtime type adapter using for {@code baseType} using {@code - * typeFieldName} as the type field name. Type field names are case sensitive. - */ - public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { - return new RuntimeTypeAdapterFactory(baseType, typeFieldName, false); - } - - /** - * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as - * the type field name. - */ - public static RuntimeTypeAdapterFactory of(Class baseType) { - return new RuntimeTypeAdapterFactory(baseType, "type", false); - } - - /** - * Registers {@code type} identified by {@code label}. Labels are case - * sensitive. - * - * @throws IllegalArgumentException if either {@code type} or {@code label} - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { - if (type == null || label == null) { - throw new NullPointerException(); - } - if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { - throw new IllegalArgumentException("types and labels must be unique"); - } - labelToSubtype.put(label, type); - subtypeToLabel.put(type, label); - return this; - } - - /** - * Registers {@code type} identified by its {@link Class#getSimpleName simple - * name}. Labels are case sensitive. - * - * @throws IllegalArgumentException if either {@code type} or its simple name - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type) { - return registerSubtype(type, type.getSimpleName()); - } - - public TypeAdapter create(Gson gson, TypeToken type) { - if (type.getRawType() != baseType) { - return null; - } - - final Map> labelToDelegate - = new LinkedHashMap>(); - final Map, TypeAdapter> subtypeToDelegate - = new LinkedHashMap, TypeAdapter>(); - for (Map.Entry> entry : labelToSubtype.entrySet()) { - TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); - labelToDelegate.put(entry.getKey(), delegate); - subtypeToDelegate.put(entry.getValue(), delegate); - } - - return new TypeAdapter() { - @Override public R read(JsonReader in) throws IOException { - JsonElement jsonElement = Streams.parse(in); - JsonElement labelJsonElement; - if (maintainType) { - labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); - } else { - labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); - } - - if (labelJsonElement == null) { - throw new JsonParseException("cannot deserialize " + baseType - + " because it does not define a field named " + typeFieldName); - } - String label = labelJsonElement.getAsString(); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); - if (delegate == null) { - - Log.e("RuntimeTypeAdapter", "cannot deserialize " + baseType + " subtype named " - + label + "; did you forget to register a subtype? " +jsonElement); - return null; - } - return delegate.fromJsonTree(jsonElement); - } - - @Override public void write(JsonWriter out, R value) throws IOException { - Class srcType = value.getClass(); - String label = subtypeToLabel.get(srcType); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); - if (delegate == null) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + "; did you forget to register a subtype?"); - } - JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); - - if (maintainType) { - Streams.write(jsonObject, out); - return; - } - - JsonObject clone = new JsonObject(); - - if (jsonObject.has(typeFieldName)) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + " because it already defines a field named " + typeFieldName); - } - clone.add(typeFieldName, new JsonPrimitive(label)); - - for (Map.Entry e : jsonObject.entrySet()) { - clone.add(e.getKey(), e.getValue()); - } - Streams.write(clone, out); - } - }.nullSafe(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.kt new file mode 100644 index 000000000..87acc939f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.kt @@ -0,0 +1,273 @@ +package fr.free.nrw.commons.wikidata.json + +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.internal.Streams +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import timber.log.Timber +import java.io.IOException + +/* +* Copyright (C) 2011 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +/** + * Adapts values whose runtime type may differ from their declaration type. This + * is necessary when a field's type is not the same type that GSON should create + * when deserializing that field. For example, consider these types: + *
   `abstract class Shape {
+ * int x;
+ * int y;
+ * }
+ * class Circle extends Shape {
+ * int radius;
+ * }
+ * class Rectangle extends Shape {
+ * int width;
+ * int height;
+ * }
+ * class Diamond extends Shape {
+ * int width;
+ * int height;
+ * }
+ * class Drawing {
+ * Shape bottomShape;
+ * Shape topShape;
+ * }
+`
* + * + * Without additional type information, the serialized JSON is ambiguous. Is + * the bottom shape in this drawing a rectangle or a diamond?
   `{
+ * "bottomShape": {
+ * "width": 10,
+ * "height": 5,
+ * "x": 0,
+ * "y": 0
+ * },
+ * "topShape": {
+ * "radius": 2,
+ * "x": 4,
+ * "y": 1
+ * }
+ * }`
+ * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized:
   `{
+ * "bottomShape": {
+ * "type": "Diamond",
+ * "width": 10,
+ * "height": 5,
+ * "x": 0,
+ * "y": 0
+ * },
+ * "topShape": {
+ * "type": "Circle",
+ * "radius": 2,
+ * "x": 4,
+ * "y": 1
+ * }
+ * }`
+ * Both the type field name (`"type"`) and the type labels (`"Rectangle"`) are configurable. + * + *

Registering Types

+ * Create a `RuntimeTypeAdapterFactory` by passing the base type and type field + * name to the [.of] factory method. If you don't supply an explicit type + * field name, `"type"` will be used.
   `RuntimeTypeAdapterFactory shapeAdapterFactory
+ * = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+`
* + * Next register all of your subtypes. Every subtype must be explicitly + * registered. This protects your application from injection attacks. If you + * don't supply an explicit type label, the type's simple name will be used. + *
   `shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ * shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+`
* + * Finally, register the type adapter factory in your application's GSON builder: + *
   `Gson gson = new GsonBuilder()
+ * .registerTypeAdapterFactory(shapeAdapterFactory)
+ * .create();
+`
* + * Like `GsonBuilder`, this API supports chaining:
   `RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ * .registerSubtype(Rectangle.class)
+ * .registerSubtype(Circle.class)
+ * .registerSubtype(Diamond.class);
+`
* + * + *

Serialization and deserialization

+ * In order to serialize and deserialize a polymorphic object, + * you must specify the base type explicitly. + *
   `Diamond diamond = new Diamond();
+ * String json = gson.toJson(diamond, Shape.class);
+`
* + * And then: + *
   `Shape shape = gson.fromJson(json, Shape.class);
+`
* + */ +class RuntimeTypeAdapterFactory( + baseType: Class<*>?, + typeFieldName: String?, + maintainType: Boolean +) : TypeAdapterFactory { + + private val baseType: Class<*> + private val typeFieldName: String + private val labelToSubtype = mutableMapOf>() + private val subtypeToLabel = mutableMapOf, String>() + private val maintainType: Boolean + + init { + if (typeFieldName == null || baseType == null) { + throw NullPointerException() + } + this.baseType = baseType + this.typeFieldName = typeFieldName + this.maintainType = maintainType + } + + /** + * Registers `type` identified by `label`. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either `type` or `label` + * have already been registered on this type adapter. + */ + fun registerSubtype(type: Class?, label: String?): RuntimeTypeAdapterFactory { + if (type == null || label == null) { + throw NullPointerException() + } + require(!(subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label))) { + "types and labels must be unique" + } + + labelToSubtype[label] = type + subtypeToLabel[type] = label + return this + } + + /** + * Registers `type` identified by its [simple][Class.getSimpleName]. Labels are case sensitive. + * + * @throws IllegalArgumentException if either `type` or its simple name + * have already been registered on this type adapter. + */ + fun registerSubtype(type: Class): RuntimeTypeAdapterFactory { + return registerSubtype(type, type.simpleName) + } + + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + if (type.rawType != baseType) { + return null + } + + val labelToDelegate = mutableMapOf>() + val subtypeToDelegate = mutableMapOf, TypeAdapter<*>>() + for ((key, value) in labelToSubtype) { + val delegate = gson.getDelegateAdapter( + this, TypeToken.get( + value + ) + ) + labelToDelegate[key] = delegate + subtypeToDelegate[value] = delegate + } + + return object : TypeAdapter() { + @Throws(IOException::class) + override fun read(reader: JsonReader): R? { + val jsonElement = Streams.parse(reader) + val labelJsonElement = if (maintainType) { + jsonElement.asJsonObject[typeFieldName] + } else { + jsonElement.asJsonObject.remove(typeFieldName) + } + + if (labelJsonElement == null) { + throw JsonParseException( + "cannot deserialize $baseType because it does not define a field named $typeFieldName" + ) + } + val label = labelJsonElement.asString + val delegate = labelToDelegate[label] as TypeAdapter? + if (delegate == null) { + Timber.tag("RuntimeTypeAdapter").e( + "cannot deserialize $baseType subtype named $label; did you forget to register a subtype? $jsonElement" + ) + return null + } + return delegate.fromJsonTree(jsonElement) + } + + @Throws(IOException::class) + override fun write(out: JsonWriter, value: R) { + val srcType: Class<*> = value::class.java.javaClass + val delegate = + subtypeToDelegate[srcType] as TypeAdapter? ?: throw JsonParseException( + "cannot serialize ${srcType.name}; did you forget to register a subtype?" + ) + + val jsonObject = delegate.toJsonTree(value).asJsonObject + if (maintainType) { + Streams.write(jsonObject, out) + return + } + + if (jsonObject.has(typeFieldName)) { + throw JsonParseException( + "cannot serialize ${srcType.name} because it already defines a field named $typeFieldName" + ) + } + val clone = JsonObject() + val label = subtypeToLabel[srcType] + clone.add(typeFieldName, JsonPrimitive(label)) + for ((key, value1) in jsonObject.entrySet()) { + clone.add(key, value1) + } + Streams.write(clone, out) + } + }.nullSafe() + } + + companion object { + /** + * Creates a new runtime type adapter using for `baseType` using `typeFieldName` as the type field name. Type field names are case sensitive. + * `maintainType` flag decide if the type will be stored in pojo or not. + */ + fun of( + baseType: Class, + typeFieldName: String, + maintainType: Boolean + ): RuntimeTypeAdapterFactory = + RuntimeTypeAdapterFactory(baseType, typeFieldName, maintainType) + + /** + * Creates a new runtime type adapter using for `baseType` using `typeFieldName` as the type field name. Type field names are case sensitive. + */ + fun of(baseType: Class, typeFieldName: String): RuntimeTypeAdapterFactory = + RuntimeTypeAdapterFactory(baseType, typeFieldName, false) + + /** + * Creates a new runtime type adapter for `baseType` using `"type"` as + * the type field name. + */ + fun of(baseType: Class): RuntimeTypeAdapterFactory = + RuntimeTypeAdapterFactory(baseType, "type", false) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.java deleted file mode 100644 index 069e02f32..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.java +++ /dev/null @@ -1,22 +0,0 @@ -package fr.free.nrw.commons.wikidata.json; - -import android.net.Uri; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import java.io.IOException; - -public class UriTypeAdapter extends TypeAdapter { - @Override - public void write(JsonWriter out, Uri value) throws IOException { - out.value(value.toString()); - } - - @Override - public Uri read(JsonReader in) throws IOException { - String url = in.nextString(); - return Uri.parse(url); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.kt new file mode 100644 index 000000000..305cfa28a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.kt @@ -0,0 +1,19 @@ +package fr.free.nrw.commons.wikidata.json + +import android.net.Uri +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import java.io.IOException + +class UriTypeAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter, value: Uri) { + out.value(value.toString()) + } + + @Throws(IOException::class) + override fun read(reader: JsonReader): Uri { + return Uri.parse(reader.nextString()) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.java deleted file mode 100644 index c268d1e73..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.java +++ /dev/null @@ -1,63 +0,0 @@ -package fr.free.nrw.commons.wikidata.json; - -import android.net.Uri; - -import com.google.gson.JsonParseException; -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; - -import fr.free.nrw.commons.wikidata.model.WikiSite; - -import java.io.IOException; - -public class WikiSiteTypeAdapter extends TypeAdapter { - private static final String DOMAIN = "domain"; - private static final String LANGUAGE_CODE = "languageCode"; - - @Override public void write(JsonWriter out, WikiSite value) throws IOException { - out.beginObject(); - out.name(DOMAIN); - out.value(value.url()); - - out.name(LANGUAGE_CODE); - out.value(value.languageCode()); - out.endObject(); - } - - @Override public WikiSite read(JsonReader in) throws IOException { - // todo: legacy; remove in June 2018 - if (in.peek() == JsonToken.STRING) { - return new WikiSite(Uri.parse(in.nextString())); - } - - String domain = null; - String languageCode = null; - in.beginObject(); - while (in.hasNext()) { - String field = in.nextName(); - String val = in.nextString(); - switch (field) { - case DOMAIN: - domain = val; - break; - case LANGUAGE_CODE: - languageCode = val; - break; - default: break; - } - } - in.endObject(); - - if (domain == null) { - throw new JsonParseException("Missing domain"); - } - - // todo: legacy; remove in June 2018 - if (languageCode == null) { - return new WikiSite(domain); - } - return new WikiSite(domain, languageCode); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.kt new file mode 100644 index 000000000..da5cb0802 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.kt @@ -0,0 +1,61 @@ +package fr.free.nrw.commons.wikidata.json + +import android.net.Uri +import com.google.gson.JsonParseException +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import fr.free.nrw.commons.wikidata.model.WikiSite +import java.io.IOException + +class WikiSiteTypeAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter, value: WikiSite) { + out.beginObject() + out.name(DOMAIN) + out.value(value.url()) + + out.name(LANGUAGE_CODE) + out.value(value.languageCode()) + out.endObject() + } + + @Throws(IOException::class) + override fun read(reader: JsonReader): WikiSite { + // todo: legacy; remove reader June 2018 + if (reader.peek() == JsonToken.STRING) { + return WikiSite(Uri.parse(reader.nextString())) + } + + var domain: String? = null + var languageCode: String? = null + reader.beginObject() + while (reader.hasNext()) { + val field = reader.nextName() + val value = reader.nextString() + when (field) { + DOMAIN -> domain = value + LANGUAGE_CODE -> languageCode = value + else -> {} + } + } + reader.endObject() + + if (domain == null) { + throw JsonParseException("Missing domain") + } + + // todo: legacy; remove reader June 2018 + return if (languageCode == null) { + WikiSite(domain) + } else { + WikiSite(domain, languageCode) + } + } + + companion object { + private const val DOMAIN = "domain" + private const val LANGUAGE_CODE = "languageCode" + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.java deleted file mode 100644 index 98e12745b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.java +++ /dev/null @@ -1,21 +0,0 @@ -package fr.free.nrw.commons.wikidata.json.annotations; - - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; - -/** - * Annotate fields in Retrofit POJO classes with this to enforce their presence in order to return - * an instantiated object. - * - * E.g.: @NonNull @Required private String title; - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target(FIELD) -public @interface Required { -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.kt new file mode 100644 index 000000000..189a3a42c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.kt @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.wikidata.json.annotations + + +/** + * Annotate fields in Retrofit POJO classes with this to enforce their presence in order to return + * an instantiated object. + * + * E.g.: @NonNull @Required private String title; + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD) +annotation class Required diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java index 2d1dbdf28..929fe0d13 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java @@ -148,7 +148,7 @@ public class Notification { return null; } if (primaryLink == null && primary instanceof JsonObject) { - primaryLink = GsonUtil.getDefaultGson().fromJson(primary, Link.class); + primaryLink = GsonUtil.INSTANCE.getDefaultGson().fromJson(primary, Link.class); } return primaryLink; } diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.java b/app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.java deleted file mode 100644 index 3ac9e3915..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.java +++ /dev/null @@ -1,34 +0,0 @@ -package fr.free.nrw.commons.wikidata.mwapi; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import java.util.Map; - - -public class UserInfo { - @NonNull private String name; - @NonNull private int id; - - //Block information - private int blockid; - private String blockedby; - private int blockedbyid; - private String blockreason; - private String blocktimestamp; - private String blockexpiry; - - // Object type is any JSON type. - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") - @Nullable private Map options; - - public int id() { - return id; - } - - @NonNull - public String blockexpiry() { - if (blockexpiry != null) - return blockexpiry; - else return ""; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.kt new file mode 100644 index 000000000..c9182a821 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.kt @@ -0,0 +1,21 @@ +package fr.free.nrw.commons.wikidata.mwapi + +data class UserInfo( + val name: String = "", + val id: Int = 0, + + //Block information + val blockid: Int = 0, + val blockedby: String? = null, + val blockedbyid: Int = 0, + val blockreason: String? = null, + val blocktimestamp: String? = null, + val blockexpiry: String? = null, + + // Object type is any JSON type. + val options: Map? = null +) { + fun id(): Int = id + + fun blockexpiry(): String = blockexpiry ?: "" +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java b/app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java index fd940c12f..d9c8ad4fb 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java +++ b/app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java @@ -69,7 +69,7 @@ public abstract class MockWebServerTest { .baseUrl(url) .callbackExecutor(new ImmediateExecutor()) .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson())) + .addConverterFactory(GsonConverterFactory.create(GsonUtil.INSTANCE.getDefaultGson())) .build() .create(clazz); } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt index ec3ad82f1..f876916b6 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt @@ -49,13 +49,13 @@ class CampaignsPresenterTest { campaignsSingle = Single.just(campaignResponseDTO) campaignsPresenter = CampaignsPresenter(okHttpJsonApiClient, testScheduler, testScheduler) campaignsPresenter.onAttachView(view) - Mockito.`when`(okHttpJsonApiClient.campaigns).thenReturn(campaignsSingle) + Mockito.`when`(okHttpJsonApiClient.getCampaigns()).thenReturn(campaignsSingle) } @Test fun getCampaignsTestNoCampaigns() { campaignsPresenter.getCampaigns() - verify(okHttpJsonApiClient).campaigns + verify(okHttpJsonApiClient).getCampaigns() testScheduler.triggerActions() verify(view).showCampaigns(null) } @@ -77,7 +77,7 @@ class CampaignsPresenterTest { Mockito.`when`(campaign.endDate).thenReturn(endDateString) Mockito.`when`(campaign.startDate).thenReturn(startDateString) Mockito.`when`(campaignResponseDTO.campaigns).thenReturn(campaigns) - verify(okHttpJsonApiClient).campaigns + verify(okHttpJsonApiClient).getCampaigns() testScheduler.triggerActions() verify(view).showCampaigns(campaign) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/UserClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/UserClientTest.kt index 52c7953ec..926678308 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/UserClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/UserClientTest.kt @@ -30,8 +30,7 @@ class UserClientTest { @Test fun isUserBlockedFromCommonsForInfinitelyBlockedUser() { - val userInfo = Mockito.mock(UserInfo::class.java) - Mockito.`when`(userInfo.blockexpiry()).thenReturn("infinite") + val userInfo = UserInfo(blockexpiry = "infinite") val mwQueryResult = Mockito.mock(MwQueryResult::class.java) Mockito.`when`(mwQueryResult.userInfo()).thenReturn(userInfo) val mockResponse = Mockito.mock(MwQueryResponse::class.java) @@ -49,8 +48,7 @@ class UserClientTest { val currentDate = Date() val expiredDate = Date(currentDate.time + 10000) - val userInfo = Mockito.mock(UserInfo::class.java) - Mockito.`when`(userInfo.blockexpiry()).thenReturn(DateUtil.iso8601DateFormat(expiredDate)) + val userInfo = UserInfo(blockexpiry = DateUtil.iso8601DateFormat(expiredDate)) val mwQueryResult = Mockito.mock(MwQueryResult::class.java) Mockito.`when`(mwQueryResult.userInfo()).thenReturn(userInfo) val mockResponse = Mockito.mock(MwQueryResponse::class.java) @@ -65,8 +63,7 @@ class UserClientTest { @Test fun isUserBlockedFromCommonsForNeverBlockedUser() { - val userInfo = Mockito.mock(UserInfo::class.java) - Mockito.`when`(userInfo.blockexpiry()).thenReturn("") + val userInfo = UserInfo(blockexpiry = "") val mwQueryResult = Mockito.mock(MwQueryResult::class.java) Mockito.`when`(mwQueryResult.userInfo()).thenReturn(userInfo) val mockResponse = Mockito.mock(MwQueryResponse::class.java) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentUnitTest.kt index 6584550b0..7fb3ba8bd 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentUnitTest.kt @@ -325,7 +325,7 @@ class NearbyParentFragmentUnitTest { @Throws(Exception::class) fun testOnDestroy() { fragment.onDestroy() - verify(wikidataEditListener).setAuthenticationStateListener(null) + verify(wikidataEditListener).authenticationStateListener = null } @Test @Ignore diff --git a/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt index e9451cd75..7d7c668a8 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt @@ -120,26 +120,16 @@ class NotificationClientTest { ) = Notification().apply { setId(notificationId) - setTimestamp( - Notification.Timestamp().apply { - setUtciso8601(timestamp) - }, - ) + setTimestamp(Notification.Timestamp().apply { setUtciso8601(timestamp) }) - contents = - Notification.Contents().apply { - setCompactHeader(compactHeader) + contents = Notification.Contents().apply { + setCompactHeader(compactHeader) - links = - Notification.Links().apply { - setPrimary( - GsonUtil.getDefaultGson().toJsonTree( - Notification.Link().apply { - setUrl(primaryUrl) - }, - ), - ) - } + links = Notification.Links().apply { + setPrimary(GsonUtil.defaultGson.toJsonTree( + Notification.Link().apply { setUrl(primaryUrl) } + )) } + } } } From f8d519e8eb04f3de5897d1b05b08a9cb94c2614e Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Fri, 6 Dec 2024 14:01:40 +0530 Subject: [PATCH 55/74] Migrated filepicker from Java to Kotlin (#5997) * Rename .java to .kt * Migrated filepicker module from Java to Kotlin * Rename .java to .kt * Migrated filepicker module from Java to Kotlin * fix: test cases --- .../nrw/commons/filepicker/Constants.java | 23 - .../free/nrw/commons/filepicker/Costants.kt | 29 ++ .../commons/filepicker/DefaultCallback.java | 16 - .../nrw/commons/filepicker/DefaultCallback.kt | 12 + .../filepicker/ExtendedFileProvider.java | 7 - .../filepicker/ExtendedFileProvider.kt | 5 + .../nrw/commons/filepicker/FilePicker.java | 355 -------------- .../free/nrw/commons/filepicker/FilePicker.kt | 441 ++++++++++++++++++ .../filepicker/FilePickerConfiguration.java | 44 -- .../filepicker/FilePickerConfiguration.kt | 46 ++ .../filepicker/MimeTypeMapWrapper.java | 26 -- .../commons/filepicker/MimeTypeMapWrapper.kt | 24 + .../nrw/commons/filepicker/PickedFiles.java | 208 --------- .../nrw/commons/filepicker/PickedFiles.kt | 195 ++++++++ .../commons/filepicker/UploadableFile.java | 213 --------- .../nrw/commons/filepicker/UploadableFile.kt | 168 +++++++ .../nrw/commons/settings/SettingsFragment.kt | 15 +- .../nrw/commons/utils/CustomSelectorUtils.kt | 2 +- .../filepicker/ShadowFileProvider.java | 32 -- .../commons/filepicker/ShadowFileProvider.kt | 36 ++ .../nrw/commons/upload/UploadPresenterTest.kt | 2 +- 21 files changed, 970 insertions(+), 929 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/Costants.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.kt delete mode 100644 app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.java create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.kt diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java deleted file mode 100644 index 97a16acc3..000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java +++ /dev/null @@ -1,23 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -public interface Constants { - String DEFAULT_FOLDER_NAME = "CommonsContributions"; - - /** - * Provides the request codes for permission handling - */ - interface RequestCodes { - int LOCATION = 1; - int STORAGE = 2; - } - - /** - * Provides locations as string for corresponding operations - */ - interface BundleKeys { - String FOLDER_NAME = "fr.free.nrw.commons.folder_name"; - String ALLOW_MULTIPLE = "fr.free.nrw.commons.allow_multiple"; - String COPY_TAKEN_PHOTOS = "fr.free.nrw.commons.copy_taken_photos"; - String COPY_PICKED_IMAGES = "fr.free.nrw.commons.copy_picked_images"; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/Costants.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/Costants.kt new file mode 100644 index 000000000..e405a6d52 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/Costants.kt @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.filepicker + +interface Constants { + companion object { + const val DEFAULT_FOLDER_NAME = "CommonsContributions" + } + + /** + * Provides the request codes for permission handling + */ + interface RequestCodes { + companion object { + const val LOCATION = 1 + const val STORAGE = 2 + } + } + + /** + * Provides locations as string for corresponding operations + */ + interface BundleKeys { + companion object { + const val FOLDER_NAME = "fr.free.nrw.commons.folder_name" + const val ALLOW_MULTIPLE = "fr.free.nrw.commons.allow_multiple" + const val COPY_TAKEN_PHOTOS = "fr.free.nrw.commons.copy_taken_photos" + const val COPY_PICKED_IMAGES = "fr.free.nrw.commons.copy_picked_images" + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.java b/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.java deleted file mode 100644 index e8373dc6f..000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.java +++ /dev/null @@ -1,16 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -/** - * Provides abstract methods which are overridden while handling Contribution Results - * inside the ContributionsController - */ -public abstract class DefaultCallback implements FilePicker.Callbacks { - - @Override - public void onImagePickerError(Exception e, FilePicker.ImageSource source, int type) { - } - - @Override - public void onCanceled(FilePicker.ImageSource source, int type) { - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.kt new file mode 100644 index 000000000..baaba67b5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.kt @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.filepicker + +/** + * Provides abstract methods which are overridden while handling Contribution Results + * inside the ContributionsController + */ +abstract class DefaultCallback: FilePicker.Callbacks { + + override fun onImagePickerError(e: Exception, source: FilePicker.ImageSource, type: Int) {} + + override fun onCanceled(source: FilePicker.ImageSource, type: Int) {} +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.java b/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.java deleted file mode 100644 index af3dc8622..000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.java +++ /dev/null @@ -1,7 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import androidx.core.content.FileProvider; - -public class ExtendedFileProvider extends FileProvider { - -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.kt new file mode 100644 index 000000000..746058fc4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.kt @@ -0,0 +1,5 @@ +package fr.free.nrw.commons.filepicker + +import androidx.core.content.FileProvider + +class ExtendedFileProvider: FileProvider() {} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java deleted file mode 100644 index b64db24c5..000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java +++ /dev/null @@ -1,355 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import static fr.free.nrw.commons.filepicker.PickedFiles.singleFileList; - -import android.app.Activity; -import android.content.ClipData; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import android.provider.MediaStore; -import android.text.TextUtils; -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultLauncher; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.PreferenceManager; -import fr.free.nrw.commons.customselector.model.Image; -import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity; -import java.io.File; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; - -public class FilePicker implements Constants { - - private static final String KEY_PHOTO_URI = "photo_uri"; - private static final String KEY_VIDEO_URI = "video_uri"; - private static final String KEY_LAST_CAMERA_PHOTO = "last_photo"; - private static final String KEY_LAST_CAMERA_VIDEO = "last_video"; - private static final String KEY_TYPE = "type"; - - /** - * Returns the uri of the clicked image so that it can be put in MediaStore - */ - private static Uri createCameraPictureFile(@NonNull Context context) throws IOException { - File imagePath = PickedFiles.getCameraPicturesLocation(context); - Uri uri = PickedFiles.getUriToFile(context, imagePath); - SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - editor.putString(KEY_PHOTO_URI, uri.toString()); - editor.putString(KEY_LAST_CAMERA_PHOTO, imagePath.toString()); - editor.apply(); - return uri; - } - - private static Intent createGalleryIntent(@NonNull Context context, int type, - boolean openDocumentIntentPreferred) { - // storing picked image type to shared preferences - storeType(context, type); - //Supported types are SVG, PNG and JPEG,GIF, TIFF, WebP, XCF - final String[] mimeTypes = { "image/jpg","image/png","image/jpeg", "image/gif", "image/tiff", "image/webp", "image/xcf", "image/svg+xml", "image/webp"}; - return plainGalleryPickerIntent(openDocumentIntentPreferred) - .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, configuration(context).allowsMultiplePickingInGallery()) - .putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); - } - - /** - * CreateCustomSectorIntent, creates intent for custom selector activity. - * @param context - * @param type - * @return Custom selector intent - */ - private static Intent createCustomSelectorIntent(@NonNull Context context, int type) { - storeType(context, type); - return new Intent(context, CustomSelectorActivity.class); - } - - private static Intent createCameraForImageIntent(@NonNull Context context, int type) { - storeType(context, type); - - Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - try { - Uri capturedImageUri = createCameraPictureFile(context); - //We have to explicitly grant the write permission since Intent.setFlag works only on API Level >=20 - grantWritePermission(context, intent, capturedImageUri); - intent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri); - } catch (Exception e) { - e.printStackTrace(); - } - - return intent; - } - - private static void revokeWritePermission(@NonNull Context context, Uri uri) { - context.revokeUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - - private static void grantWritePermission(@NonNull Context context, Intent intent, Uri uri) { - List resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resInfoList) { - String packageName = resolveInfo.activityInfo.packageName; - context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } - - private static void storeType(@NonNull Context context, int type) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(KEY_TYPE, type).apply(); - } - - private static int restoreType(@NonNull Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getInt(KEY_TYPE, 0); - } - - /** - * Opens default galery or a available galleries picker if there is no default - * - * @param type Custom type of your choice, which will be returned with the images - */ - public static void openGallery(Activity activity, ActivityResultLauncher resultLauncher, int type, boolean openDocumentIntentPreferred) { - Intent intent = createGalleryIntent(activity, type, openDocumentIntentPreferred); - resultLauncher.launch(intent); - } - - /** - * Opens Custom Selector - */ - public static void openCustomSelector(Activity activity, ActivityResultLauncher resultLauncher, int type) { - Intent intent = createCustomSelectorIntent(activity, type); - resultLauncher.launch(intent); - } - - /** - * Opens the camera app to pick image clicked by user - */ - public static void openCameraForImage(Activity activity, ActivityResultLauncher resultLauncher, int type) { - Intent intent = createCameraForImageIntent(activity, type); - resultLauncher.launch(intent); - } - - @Nullable - private static UploadableFile takenCameraPicture(Context context) throws URISyntaxException { - String lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_LAST_CAMERA_PHOTO, null); - if (lastCameraPhoto != null) { - return new UploadableFile(new File(lastCameraPhoto)); - } else { - return null; - } - } - - @Nullable - private static UploadableFile takenCameraVideo(Context context) throws URISyntaxException { - String lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_LAST_CAMERA_VIDEO, null); - if (lastCameraPhoto != null) { - return new UploadableFile(new File(lastCameraPhoto)); - } else { - return null; - } - } - - public static List handleExternalImagesPicked(Intent data, Activity activity) { - try { - return getFilesFromGalleryPictures(data, activity); - } catch (IOException | SecurityException e) { - e.printStackTrace(); - } - return new ArrayList<>(); - } - - private static boolean isPhoto(Intent data) { - return data == null || (data.getData() == null && data.getClipData() == null); - } - - private static Intent plainGalleryPickerIntent(boolean openDocumentIntentPreferred) { - /* - * Asking for ACCESS_MEDIA_LOCATION at runtime solved the location-loss issue - * in the custom selector in Contributions fragment. - * Detailed discussion: https://github.com/commons-app/apps-android-commons/issues/5015 - * - * This permission check, however, was insufficient to fix location-loss in - * the regular selector in Contributions fragment and Nearby fragment, - * especially on some devices running Android 13 that use the new Photo Picker by default. - * - * New Photo Picker: https://developer.android.com/training/data-storage/shared/photopicker - * - * The new Photo Picker introduced by Android redacts location tags from EXIF metadata. - * Reported on the Google Issue Tracker: https://issuetracker.google.com/issues/243294058 - * Status: Won't fix (Intended behaviour) - * - * Switched intent from ACTION_GET_CONTENT to ACTION_OPEN_DOCUMENT (by default; can - * be changed through the Setting page) as: - * - * ACTION_GET_CONTENT opens the 'best application' for choosing that kind of data - * The best application is the new Photo Picker that redacts the location tags - * - * ACTION_OPEN_DOCUMENT, however, displays the various DocumentsProvider instances - * installed on the device, letting the user interactively navigate through them. - * - * So, this allows us to use the traditional file picker that does not redact location tags - * from EXIF. - * - */ - Intent intent; - if (openDocumentIntentPreferred) { - intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - } else { - intent = new Intent(Intent.ACTION_GET_CONTENT); - } - intent.setType("image/*"); - return intent; - } - - public static void onPictureReturnedFromDocuments(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { - if(result.getResultCode() == Activity.RESULT_OK && !isPhoto(result.getData())){ - try { - Uri photoPath = result.getData().getData(); - UploadableFile photoFile = PickedFiles.pickedExistingPicture(activity, photoPath); - callbacks.onImagesPicked(singleFileList(photoFile), FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); - - if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { - PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); - } - } catch (Exception e) { - e.printStackTrace(); - callbacks.onImagePickerError(e, FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); - } - } else { - callbacks.onCanceled(FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); - } - } - - /** - * onPictureReturnedFromCustomSelector. - * Retrieve and forward the images to upload wizard through callback. - */ - public static void onPictureReturnedFromCustomSelector(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { - if(result.getResultCode() == Activity.RESULT_OK){ - try { - List files = getFilesFromCustomSelector(result.getData(), activity); - callbacks.onImagesPicked(files, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); - } catch (Exception e) { - e.printStackTrace(); - callbacks.onImagePickerError(e, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); - } - } else { - callbacks.onCanceled(ImageSource.CUSTOM_SELECTOR, restoreType(activity)); - } - } - - /** - * Get files from custom selector - * Retrieve and process the selected images from the custom selector. - */ - private static List getFilesFromCustomSelector(Intent data, Activity activity) throws IOException, SecurityException { - List files = new ArrayList<>(); - ArrayList images = data.getParcelableArrayListExtra("Images"); - for(Image image : images) { - Uri uri = image.getUri(); - UploadableFile file = PickedFiles.pickedExistingPicture(activity, uri); - files.add(file); - } - - if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { - PickedFiles.copyFilesInSeparateThread(activity, files); - } - - return files; - } - - public static void onPictureReturnedFromGallery(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { - if(result.getResultCode() == Activity.RESULT_OK && !isPhoto(result.getData())){ - try { - List files = getFilesFromGalleryPictures(result.getData(), activity); - callbacks.onImagesPicked(files, FilePicker.ImageSource.GALLERY, restoreType(activity)); - } catch (Exception e) { - e.printStackTrace(); - callbacks.onImagePickerError(e, FilePicker.ImageSource.GALLERY, restoreType(activity)); - } - } else{ - callbacks.onCanceled(FilePicker.ImageSource.GALLERY, restoreType(activity)); - } - } - - private static List getFilesFromGalleryPictures(Intent data, Activity activity) throws IOException, SecurityException { - List files = new ArrayList<>(); - ClipData clipData = data.getClipData(); - if (clipData == null) { - Uri uri = data.getData(); - UploadableFile file = PickedFiles.pickedExistingPicture(activity, uri); - files.add(file); - } else { - for (int i = 0; i < clipData.getItemCount(); i++) { - Uri uri = clipData.getItemAt(i).getUri(); - UploadableFile file = PickedFiles.pickedExistingPicture(activity, uri); - files.add(file); - } - } - - if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { - PickedFiles.copyFilesInSeparateThread(activity, files); - } - - return files; - } - - public static void onPictureReturnedFromCamera(ActivityResult activityResult, Activity activity, @NonNull FilePicker.Callbacks callbacks) { - if(activityResult.getResultCode() == Activity.RESULT_OK){ - try { - String lastImageUri = PreferenceManager.getDefaultSharedPreferences(activity).getString(KEY_PHOTO_URI, null); - if (!TextUtils.isEmpty(lastImageUri)) { - revokeWritePermission(activity, Uri.parse(lastImageUri)); - } - - UploadableFile photoFile = FilePicker.takenCameraPicture(activity); - List files = new ArrayList<>(); - files.add(photoFile); - - if (photoFile == null) { - Exception e = new IllegalStateException("Unable to get the picture returned from camera"); - callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); - } else { - if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) { - PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); - } - - callbacks.onImagesPicked(files, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); - } - - PreferenceManager.getDefaultSharedPreferences(activity) - .edit() - .remove(KEY_LAST_CAMERA_PHOTO) - .remove(KEY_PHOTO_URI) - .apply(); - } catch (Exception e) { - e.printStackTrace(); - callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); - } - } else { - callbacks.onCanceled(FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); - } - } - - public static FilePickerConfiguration configuration(@NonNull Context context) { - return new FilePickerConfiguration(context); - } - - - public enum ImageSource { - GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO, CUSTOM_SELECTOR - } - - public interface Callbacks { - void onImagePickerError(Exception e, FilePicker.ImageSource source, int type); - - void onImagesPicked(@NonNull List imageFiles, FilePicker.ImageSource source, int type); - - void onCanceled(FilePicker.ImageSource source, int type); - } - - public interface HandleActivityResult{ - void onHandleActivityResult(FilePicker.Callbacks callbacks); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt new file mode 100644 index 000000000..6bf8a1061 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt @@ -0,0 +1,441 @@ +package fr.free.nrw.commons.filepicker + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.provider.MediaStore +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.preference.PreferenceManager +import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity +import fr.free.nrw.commons.filepicker.PickedFiles.singleFileList +import java.io.File +import java.io.IOException +import java.net.URISyntaxException + + +object FilePicker : Constants { + + private const val KEY_PHOTO_URI = "photo_uri" + private const val KEY_VIDEO_URI = "video_uri" + private const val KEY_LAST_CAMERA_PHOTO = "last_photo" + private const val KEY_LAST_CAMERA_VIDEO = "last_video" + private const val KEY_TYPE = "type" + + /** + * Returns the uri of the clicked image so that it can be put in MediaStore + */ + @Throws(IOException::class) + @JvmStatic + private fun createCameraPictureFile(context: Context): Uri { + val imagePath = PickedFiles.getCameraPicturesLocation(context) + val uri = PickedFiles.getUriToFile(context, imagePath) + val editor = PreferenceManager.getDefaultSharedPreferences(context).edit() + editor.putString(KEY_PHOTO_URI, uri.toString()) + editor.putString(KEY_LAST_CAMERA_PHOTO, imagePath.toString()) + editor.apply() + return uri + } + + + @JvmStatic + private fun createGalleryIntent( + context: Context, + type: Int, + openDocumentIntentPreferred: Boolean + ): Intent { + // storing picked image type to shared preferences + storeType(context, type) + // Supported types are SVG, PNG and JPEG, GIF, TIFF, WebP, XCF + val mimeTypes = arrayOf( + "image/jpg", + "image/png", + "image/jpeg", + "image/gif", + "image/tiff", + "image/webp", + "image/xcf", + "image/svg+xml", + "image/webp" + ) + return plainGalleryPickerIntent(openDocumentIntentPreferred) + .putExtra( + Intent.EXTRA_ALLOW_MULTIPLE, + configuration(context).allowsMultiplePickingInGallery() + ) + .putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) + } + + /** + * CreateCustomSectorIntent, creates intent for custom selector activity. + * @param context + * @param type + * @return Custom selector intent + */ + @JvmStatic + private fun createCustomSelectorIntent(context: Context, type: Int): Intent { + storeType(context, type) + return Intent(context, CustomSelectorActivity::class.java) + } + + @JvmStatic + private fun createCameraForImageIntent(context: Context, type: Int): Intent { + storeType(context, type) + + val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + try { + val capturedImageUri = createCameraPictureFile(context) + // We have to explicitly grant the write permission since Intent.setFlag works only on API Level >=20 + grantWritePermission(context, intent, capturedImageUri) + intent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri) + } catch (e: Exception) { + e.printStackTrace() + } + + return intent + } + + @JvmStatic + private fun revokeWritePermission(context: Context, uri: Uri) { + context.revokeUriPermission( + uri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } + + @JvmStatic + private fun grantWritePermission(context: Context, intent: Intent, uri: Uri) { + val resInfoList = + context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) + for (resolveInfo in resInfoList) { + val packageName = resolveInfo.activityInfo.packageName + context.grantUriPermission( + packageName, + uri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } + } + + @JvmStatic + private fun storeType(context: Context, type: Int) { + PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(KEY_TYPE, type).apply() + } + + @JvmStatic + private fun restoreType(context: Context): Int { + return PreferenceManager.getDefaultSharedPreferences(context).getInt(KEY_TYPE, 0) + } + + /** + * Opens default gallery or available galleries picker if there is no default + * + * @param type Custom type of your choice, which will be returned with the images + */ + @JvmStatic + fun openGallery( + activity: Activity, + resultLauncher: ActivityResultLauncher, + type: Int, + openDocumentIntentPreferred: Boolean + ) { + val intent = createGalleryIntent(activity, type, openDocumentIntentPreferred) + resultLauncher.launch(intent) + } + + /** + * Opens Custom Selector + */ + @JvmStatic + fun openCustomSelector( + activity: Activity, + resultLauncher: ActivityResultLauncher, + type: Int + ) { + val intent = createCustomSelectorIntent(activity, type) + resultLauncher.launch(intent) + } + + /** + * Opens the camera app to pick image clicked by user + */ + @JvmStatic + fun openCameraForImage( + activity: Activity, + resultLauncher: ActivityResultLauncher, + type: Int + ) { + val intent = createCameraForImageIntent(activity, type) + resultLauncher.launch(intent) + } + + @Throws(URISyntaxException::class) + @JvmStatic + private fun takenCameraPicture(context: Context): UploadableFile? { + val lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context) + .getString(KEY_LAST_CAMERA_PHOTO, null) + return if (lastCameraPhoto != null) { + UploadableFile(File(lastCameraPhoto)) + } else { + null + } + } + + @Throws(URISyntaxException::class) + @JvmStatic + private fun takenCameraVideo(context: Context): UploadableFile? { + val lastCameraVideo = PreferenceManager.getDefaultSharedPreferences(context) + .getString(KEY_LAST_CAMERA_VIDEO, null) + return if (lastCameraVideo != null) { + UploadableFile(File(lastCameraVideo)) + } else { + null + } + } + + @JvmStatic + fun handleExternalImagesPicked(data: Intent?, activity: Activity): List { + return try { + getFilesFromGalleryPictures(data, activity) + } catch (e: IOException) { + e.printStackTrace() + emptyList() + } catch (e: SecurityException) { + e.printStackTrace() + emptyList() + } + } + + @JvmStatic + private fun isPhoto(data: Intent?): Boolean { + return data == null || (data.data == null && data.clipData == null) + } + + @JvmStatic + private fun plainGalleryPickerIntent( + openDocumentIntentPreferred: Boolean + ): Intent { + /* + * Asking for ACCESS_MEDIA_LOCATION at runtime solved the location-loss issue + * in the custom selector in Contributions fragment. + * Detailed discussion: https://github.com/commons-app/apps-android-commons/issues/5015 + * + * This permission check, however, was insufficient to fix location-loss in + * the regular selector in Contributions fragment and Nearby fragment, + * especially on some devices running Android 13 that use the new Photo Picker by default. + * + * New Photo Picker: https://developer.android.com/training/data-storage/shared/photopicker + * + * The new Photo Picker introduced by Android redacts location tags from EXIF metadata. + * Reported on the Google Issue Tracker: https://issuetracker.google.com/issues/243294058 + * Status: Won't fix (Intended behaviour) + * + * Switched intent from ACTION_GET_CONTENT to ACTION_OPEN_DOCUMENT (by default; can + * be changed through the Setting page) as: + * + * ACTION_GET_CONTENT opens the 'best application' for choosing that kind of data + * The best application is the new Photo Picker that redacts the location tags + * + * ACTION_OPEN_DOCUMENT, however, displays the various DocumentsProvider instances + * installed on the device, letting the user interactively navigate through them. + * + * So, this allows us to use the traditional file picker that does not redact location tags + * from EXIF. + * + */ + val intent = if (openDocumentIntentPreferred) { + Intent(Intent.ACTION_OPEN_DOCUMENT) + } else { + Intent(Intent.ACTION_GET_CONTENT) + } + intent.type = "image/*" + return intent + } + + @JvmStatic + fun onPictureReturnedFromDocuments( + result: ActivityResult, + activity: Activity, + callbacks: Callbacks + ) { + if (result.resultCode == Activity.RESULT_OK && !isPhoto(result.data)) { + try { + val photoPath = result.data?.data + val photoFile = PickedFiles.pickedExistingPicture(activity, photoPath!!) + callbacks.onImagesPicked( + singleFileList(photoFile), + ImageSource.DOCUMENTS, + restoreType(activity) + ) + + if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { + PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)) + } + } catch (e: Exception) { + e.printStackTrace() + callbacks.onImagePickerError(e, ImageSource.DOCUMENTS, restoreType(activity)) + } + } else { + callbacks.onCanceled(ImageSource.DOCUMENTS, restoreType(activity)) + } + } + + /** + * onPictureReturnedFromCustomSelector. + * Retrieve and forward the images to upload wizard through callback. + */ + @JvmStatic + fun onPictureReturnedFromCustomSelector( + result: ActivityResult, + activity: Activity, + callbacks: Callbacks + ) { + if (result.resultCode == Activity.RESULT_OK) { + try { + val files = getFilesFromCustomSelector(result.data, activity) + callbacks.onImagesPicked(files, ImageSource.CUSTOM_SELECTOR, restoreType(activity)) + } catch (e: Exception) { + e.printStackTrace() + callbacks.onImagePickerError(e, ImageSource.CUSTOM_SELECTOR, restoreType(activity)) + } + } else { + callbacks.onCanceled(ImageSource.CUSTOM_SELECTOR, restoreType(activity)) + } + } + + /** + * Get files from custom selector + * Retrieve and process the selected images from the custom selector. + */ + @Throws(IOException::class, SecurityException::class) + @JvmStatic + private fun getFilesFromCustomSelector( + data: Intent?, + activity: Activity + ): List { + val files = mutableListOf() + val images = data?.getParcelableArrayListExtra("Images") + images?.forEach { image -> + val uri = image.uri + val file = PickedFiles.pickedExistingPicture(activity, uri) + files.add(file) + } + + if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { + PickedFiles.copyFilesInSeparateThread(activity, files) + } + + return files + } + + @JvmStatic + fun onPictureReturnedFromGallery( + result: ActivityResult, + activity: Activity, + callbacks: Callbacks + ) { + if (result.resultCode == Activity.RESULT_OK && !isPhoto(result.data)) { + try { + val files = getFilesFromGalleryPictures(result.data, activity) + callbacks.onImagesPicked(files, ImageSource.GALLERY, restoreType(activity)) + } catch (e: Exception) { + e.printStackTrace() + callbacks.onImagePickerError(e, ImageSource.GALLERY, restoreType(activity)) + } + } else { + callbacks.onCanceled(ImageSource.GALLERY, restoreType(activity)) + } + } + + @Throws(IOException::class, SecurityException::class) + @JvmStatic + private fun getFilesFromGalleryPictures( + data: Intent?, + activity: Activity + ): List { + val files = mutableListOf() + val clipData = data?.clipData + if (clipData == null) { + val uri = data?.data + val file = PickedFiles.pickedExistingPicture(activity, uri!!) + files.add(file) + } else { + for (i in 0 until clipData.itemCount) { + val uri = clipData.getItemAt(i).uri + val file = PickedFiles.pickedExistingPicture(activity, uri) + files.add(file) + } + } + + if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { + PickedFiles.copyFilesInSeparateThread(activity, files) + } + + return files + } + + @JvmStatic + fun onPictureReturnedFromCamera( + activityResult: ActivityResult, + activity: Activity, + callbacks: Callbacks + ) { + if (activityResult.resultCode == Activity.RESULT_OK) { + try { + val lastImageUri = PreferenceManager.getDefaultSharedPreferences(activity) + .getString(KEY_PHOTO_URI, null) + if (!lastImageUri.isNullOrEmpty()) { + revokeWritePermission(activity, Uri.parse(lastImageUri)) + } + + val photoFile = takenCameraPicture(activity) + val files = mutableListOf() + photoFile?.let { files.add(it) } + + if (photoFile == null) { + val e = IllegalStateException("Unable to get the picture returned from camera") + callbacks.onImagePickerError(e, ImageSource.CAMERA_IMAGE, restoreType(activity)) + } else { + if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) { + PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)) + } + callbacks.onImagesPicked(files, ImageSource.CAMERA_IMAGE, restoreType(activity)) + } + + PreferenceManager.getDefaultSharedPreferences(activity).edit() + .remove(KEY_LAST_CAMERA_PHOTO) + .remove(KEY_PHOTO_URI) + .apply() + } catch (e: Exception) { + e.printStackTrace() + callbacks.onImagePickerError(e, ImageSource.CAMERA_IMAGE, restoreType(activity)) + } + } else { + callbacks.onCanceled(ImageSource.CAMERA_IMAGE, restoreType(activity)) + } + } + + @JvmStatic + fun configuration(context: Context): FilePickerConfiguration { + return FilePickerConfiguration(context) + } + + enum class ImageSource { + GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO, CUSTOM_SELECTOR + } + + interface Callbacks { + fun onImagePickerError(e: Exception, source: ImageSource, type: Int) + + fun onImagesPicked(imageFiles: List, source: ImageSource, type: Int) + + fun onCanceled(source: ImageSource, type: Int) + } + + interface HandleActivityResult { + fun onHandleActivityResult(callbacks: Callbacks) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.java b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.java deleted file mode 100644 index 08a204e8b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.java +++ /dev/null @@ -1,44 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import android.content.Context; -import androidx.preference.PreferenceManager; - -public class FilePickerConfiguration implements Constants { - - private Context context; - - FilePickerConfiguration(Context context) { - this.context = context; - } - - public FilePickerConfiguration setAllowMultiplePickInGallery(boolean allowMultiple) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putBoolean(BundleKeys.ALLOW_MULTIPLE, allowMultiple) - .apply(); - return this; - } - - public FilePickerConfiguration setCopyTakenPhotosToPublicGalleryAppFolder(boolean copy) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putBoolean(BundleKeys.COPY_TAKEN_PHOTOS, copy) - .apply(); - return this; - } - - public String getFolderName() { - return PreferenceManager.getDefaultSharedPreferences(context).getString(BundleKeys.FOLDER_NAME, DEFAULT_FOLDER_NAME); - } - - public boolean allowsMultiplePickingInGallery() { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BundleKeys.ALLOW_MULTIPLE, false); - } - - public boolean shouldCopyTakenPhotosToPublicGalleryAppFolder() { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BundleKeys.COPY_TAKEN_PHOTOS, false); - } - - public boolean shouldCopyPickedImagesToPublicGalleryAppFolder() { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BundleKeys.COPY_PICKED_IMAGES, false); - } - -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.kt new file mode 100644 index 000000000..db025a544 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.kt @@ -0,0 +1,46 @@ +package fr.free.nrw.commons.filepicker + +import android.content.Context +import androidx.preference.PreferenceManager + +class FilePickerConfiguration( + private val context: Context +): Constants { + + fun setAllowMultiplePickInGallery(allowMultiple: Boolean): FilePickerConfiguration { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putBoolean(Constants.BundleKeys.ALLOW_MULTIPLE, allowMultiple) + .apply() + return this + } + + fun setCopyTakenPhotosToPublicGalleryAppFolder(copy: Boolean): FilePickerConfiguration { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putBoolean(Constants.BundleKeys.COPY_TAKEN_PHOTOS, copy) + .apply() + return this + } + + fun getFolderName(): String { + return PreferenceManager.getDefaultSharedPreferences(context) + .getString( + Constants.BundleKeys.FOLDER_NAME, + Constants.DEFAULT_FOLDER_NAME + ) ?: Constants.DEFAULT_FOLDER_NAME + } + + fun allowsMultiplePickingInGallery(): Boolean { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(Constants.BundleKeys.ALLOW_MULTIPLE, false) + } + + fun shouldCopyTakenPhotosToPublicGalleryAppFolder(): Boolean { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(Constants.BundleKeys.COPY_TAKEN_PHOTOS, false) + } + + fun shouldCopyPickedImagesToPublicGalleryAppFolder(): Boolean { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(Constants.BundleKeys.COPY_PICKED_IMAGES, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.java b/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.java deleted file mode 100644 index e6c82f5c1..000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.java +++ /dev/null @@ -1,26 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import android.webkit.MimeTypeMap; - -import com.facebook.common.internal.ImmutableMap; - -import java.util.Map; - -public class MimeTypeMapWrapper { - - private static final MimeTypeMap sMimeTypeMap = MimeTypeMap.getSingleton(); - - private static final Map sMimeTypeToExtensionMap = - ImmutableMap.of( - "image/heif", "heif", - "image/heic", "heic"); - - public static String getExtensionFromMimeType(String mimeType) { - String result = sMimeTypeToExtensionMap.get(mimeType); - if (result != null) { - return result; - } - return sMimeTypeMap.getExtensionFromMimeType(mimeType); - } - -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.kt new file mode 100644 index 000000000..0cf21cc02 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.kt @@ -0,0 +1,24 @@ +package fr.free.nrw.commons.filepicker + +import android.webkit.MimeTypeMap + +class MimeTypeMapWrapper { + + companion object { + private val sMimeTypeMap = MimeTypeMap.getSingleton() + + private val sMimeTypeToExtensionMap = mapOf( + "image/heif" to "heif", + "image/heic" to "heic" + ) + + @JvmStatic + fun getExtensionFromMimeType(mimeType: String): String? { + val result = sMimeTypeToExtensionMap[mimeType] + if (result != null) { + return result + } + return sMimeTypeMap.getExtensionFromMimeType(mimeType) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java deleted file mode 100644 index ca1abba62..000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java +++ /dev/null @@ -1,208 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import android.content.ContentResolver; -import android.content.Context; -import android.media.MediaScannerConnection; -import android.net.Uri; -import android.os.Environment; -import android.webkit.MimeTypeMap; - -import androidx.annotation.NonNull; -import androidx.core.content.FileProvider; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.List; -import java.util.UUID; - -import timber.log.Timber; - -/** - * PickedFiles. - * Process the upload items. - */ -public class PickedFiles implements Constants { - - /** - * Get Folder Name - * @param context - * @return default application folder name. - */ - private static String getFolderName(@NonNull Context context) { - return FilePicker.configuration(context).getFolderName(); - } - - /** - * tempImageDirectory - * @param context - * @return temporary image directory to copy and perform exif changes. - */ - private static File tempImageDirectory(@NonNull Context context) { - File privateTempDir = new File(context.getCacheDir(), DEFAULT_FOLDER_NAME); - if (!privateTempDir.exists()) privateTempDir.mkdirs(); - return privateTempDir; - } - - /** - * writeToFile - * writes inputStream data to the destination file. - * @param in input stream of source file. - * @param file destination file - */ - private static void writeToFile(InputStream in, File file) throws IOException { - try (OutputStream out = new FileOutputStream(file)) { - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - } - } - - /** - * Copy file function. - * Copies source file to destination file. - * @param src source file - * @param dst destination file - * @throws IOException (File input stream exception) - */ - private static void copyFile(File src, File dst) throws IOException { - try (InputStream in = new FileInputStream(src)) { - writeToFile(in, dst); - } - } - - /** - * Copy files in separate thread. - * Copies all the uploadable files to the temp image folder on background thread. - * @param context - * @param filesToCopy uploadable file list to be copied. - */ - static void copyFilesInSeparateThread(final Context context, final List filesToCopy) { - new Thread(() -> { - List copiedFiles = new ArrayList<>(); - int i = 1; - for (UploadableFile uploadableFile : filesToCopy) { - File fileToCopy = uploadableFile.getFile(); - File dstDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), getFolderName(context)); - if (!dstDir.exists()) { - dstDir.mkdirs(); - } - - String[] filenameSplit = fileToCopy.getName().split("\\."); - String extension = "." + filenameSplit[filenameSplit.length - 1]; - String filename = String.format("IMG_%s_%d.%s", new SimpleDateFormat("yyyyMMdd_HHmmss").format(Calendar.getInstance().getTime()), i, extension); - - File dstFile = new File(dstDir, filename); - try { - dstFile.createNewFile(); - copyFile(fileToCopy, dstFile); - copiedFiles.add(dstFile); - } catch (IOException e) { - e.printStackTrace(); - } - i++; - } - scanCopiedImages(context, copiedFiles); - }).run(); - } - - /** - * singleFileList. - * converts a single uploadableFile to list of uploadableFile. - * @param file uploadable file - * @return - */ - static List singleFileList(UploadableFile file) { - List list = new ArrayList<>(); - list.add(file); - return list; - } - - /** - * ScanCopiedImages - * Scan copied images metadata using media scanner. - * @param context - * @param copiedImages copied images list. - */ - static void scanCopiedImages(Context context, List copiedImages) { - String[] paths = new String[copiedImages.size()]; - for (int i = 0; i < copiedImages.size(); i++) { - paths[i] = copiedImages.get(i).toString(); - } - - MediaScannerConnection.scanFile(context, - paths, null, - (path, uri) -> { - Timber.d("Scanned " + path + ":"); - Timber.d("-> uri=%s", uri); - }); - } - - /** - * pickedExistingPicture - * convert the image into uploadable file. - * @param photoUri Uri of the image. - * @return Uploadable file ready for tag redaction. - */ - public static UploadableFile pickedExistingPicture(@NonNull Context context, Uri photoUri) throws IOException, SecurityException {// SecurityException for those file providers who share URI but forget to grant necessary permissions - File directory = tempImageDirectory(context); - File photoFile = new File(directory, UUID.randomUUID().toString() + "." + getMimeType(context, photoUri)); - if (photoFile.createNewFile()) { - try (InputStream pictureInputStream = context.getContentResolver().openInputStream(photoUri)) { - writeToFile(pictureInputStream, photoFile); - } - } else { - throw new IOException("could not create photoFile to write upon"); - } - return new UploadableFile(photoUri, photoFile); - } - - /** - * getCameraPictureLocation - */ - static File getCameraPicturesLocation(@NonNull Context context) throws IOException { - File dir = tempImageDirectory(context); - return File.createTempFile(UUID.randomUUID().toString(), ".jpg", dir); - } - - /** - * To find out the extension of required object in given uri - * Solution by http://stackoverflow.com/a/36514823/1171484 - */ - private static String getMimeType(@NonNull Context context, @NonNull Uri uri) { - String extension; - - //Check uri format to avoid null - if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { - //If scheme is a content - extension = MimeTypeMapWrapper.getExtensionFromMimeType(context.getContentResolver().getType(uri)); - } else { - //If scheme is a File - //This will replace white spaces with %20 and also other special characters. This will avoid returning null values on file name with spaces and special characters. - extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(new File(uri.getPath())).toString()); - - } - - return extension; - } - - /** - * GetUriToFile - * @param file get uri of file - * @return uri of requested file. - */ - static Uri getUriToFile(@NonNull Context context, @NonNull File file) { - String packageName = context.getApplicationContext().getPackageName(); - String authority = packageName + ".provider"; - return FileProvider.getUriForFile(context, authority, file); - } - -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.kt new file mode 100644 index 000000000..9694dedb5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.kt @@ -0,0 +1,195 @@ +package fr.free.nrw.commons.filepicker + +import android.content.ContentResolver +import android.content.Context +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Environment +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider +import fr.free.nrw.commons.filepicker.Constants.Companion.DEFAULT_FOLDER_NAME +import timber.log.Timber +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.UUID + + +/** + * PickedFiles. + * Process the upload items. + */ +object PickedFiles : Constants { + + /** + * Get Folder Name + * @return default application folder name. + */ + @JvmStatic + private fun getFolderName(context: Context): String { + return FilePicker.configuration(context).getFolderName() + } + + /** + * tempImageDirectory + * @return temporary image directory to copy and perform exif changes. + */ + @JvmStatic + private fun tempImageDirectory(context: Context): File { + val privateTempDir = File(context.cacheDir, DEFAULT_FOLDER_NAME) + if (!privateTempDir.exists()) privateTempDir.mkdirs() + return privateTempDir + } + + /** + * writeToFile + * Writes inputStream data to the destination file. + */ + @JvmStatic + @Throws(IOException::class) + private fun writeToFile(inputStream: InputStream, file: File) { + inputStream.use { input -> + FileOutputStream(file).use { output -> + val buffer = ByteArray(1024) + var length: Int + while (input.read(buffer).also { length = it } > 0) { + output.write(buffer, 0, length) + } + } + } + } + + /** + * Copy file function. + * Copies source file to destination file. + */ + @Throws(IOException::class) + @JvmStatic + private fun copyFile(src: File, dst: File) { + FileInputStream(src).use { inputStream -> + writeToFile(inputStream, dst) + } + } + + /** + * Copy files in separate thread. + * Copies all the uploadable files to the temp image folder on background thread. + */ + @JvmStatic + fun copyFilesInSeparateThread(context: Context, filesToCopy: List) { + Thread { + val copiedFiles = mutableListOf() + var index = 1 + filesToCopy.forEach { uploadableFile -> + val fileToCopy = uploadableFile.file + val dstDir = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + getFolderName(context) + ) + if (!dstDir.exists()) dstDir.mkdirs() + + val filenameSplit = fileToCopy.name.split(".") + val extension = ".${filenameSplit.last()}" + val filename = "IMG_${SimpleDateFormat( + "yyyyMMdd_HHmmss", + Locale.getDefault()).format(Date())}_$index$extension" + val dstFile = File(dstDir, filename) + + try { + dstFile.createNewFile() + copyFile(fileToCopy, dstFile) + copiedFiles.add(dstFile) + } catch (e: IOException) { + e.printStackTrace() + } + index++ + } + scanCopiedImages(context, copiedFiles) + }.start() + } + + /** + * singleFileList + * Converts a single uploadableFile to list of uploadableFile. + */ + @JvmStatic + fun singleFileList(file: UploadableFile): List { + return listOf(file) + } + + /** + * ScanCopiedImages + * Scans copied images metadata using media scanner. + */ + @JvmStatic + fun scanCopiedImages(context: Context, copiedImages: List) { + val paths = copiedImages.map { it.toString() }.toTypedArray() + MediaScannerConnection.scanFile(context, paths, null) { path, uri -> + Timber.d("Scanned $path:") + Timber.d("-> uri=$uri") + } + } + + /** + * pickedExistingPicture + * Convert the image into uploadable file. + */ + @Throws(IOException::class, SecurityException::class) + @JvmStatic + fun pickedExistingPicture(context: Context, photoUri: Uri): UploadableFile { + val directory = tempImageDirectory(context) + val mimeType = getMimeType(context, photoUri) + val photoFile = File(directory, "${UUID.randomUUID()}.$mimeType") + + if (photoFile.createNewFile()) { + context.contentResolver.openInputStream(photoUri)?.use { inputStream -> + writeToFile(inputStream, photoFile) + } + } else { + throw IOException("Could not create photoFile to write upon") + } + return UploadableFile(photoUri, photoFile) + } + + /** + * getCameraPictureLocation + */ + @Throws(IOException::class) + @JvmStatic + fun getCameraPicturesLocation(context: Context): File { + val dir = tempImageDirectory(context) + return File.createTempFile(UUID.randomUUID().toString(), ".jpg", dir) + } + + /** + * To find out the extension of the required object in a given uri + */ + @JvmStatic + private fun getMimeType(context: Context, uri: Uri): String { + return if (uri.scheme == ContentResolver.SCHEME_CONTENT) { + context.contentResolver.getType(uri) + ?.let { MimeTypeMapWrapper.getExtensionFromMimeType(it) } + } else { + MimeTypeMap.getFileExtensionFromUrl( + Uri.fromFile(uri.path?.let { File(it) }).toString() + ) + } ?: "jpg" // Default to jpg if unable to determine type + } + + /** + * GetUriToFile + * @param file get uri of file + * @return uri of requested file. + */ + @JvmStatic + fun getUriToFile(context: Context, file: File): Uri { + val packageName = context.applicationContext.packageName + val authority = "$packageName.provider" + return FileProvider.getUriForFile(context, authority, file) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java b/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java deleted file mode 100644 index 1fe306a8b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java +++ /dev/null @@ -1,213 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; - -import androidx.annotation.Nullable; -import androidx.exifinterface.media.ExifInterface; - -import fr.free.nrw.commons.upload.FileUtils; -import java.io.File; -import java.io.IOException; -import java.util.Date; -import timber.log.Timber; - -public class UploadableFile implements Parcelable { - public static final Creator CREATOR = new Creator() { - @Override - public UploadableFile createFromParcel(Parcel in) { - return new UploadableFile(in); - } - - @Override - public UploadableFile[] newArray(int size) { - return new UploadableFile[size]; - } - }; - - private final Uri contentUri; - private final File file; - - public UploadableFile(Uri contentUri, File file) { - this.contentUri = contentUri; - this.file = file; - } - - public UploadableFile(File file) { - this.file = file; - this.contentUri = Uri.fromFile(new File(file.getPath())); - } - - public UploadableFile(Parcel in) { - this.contentUri = in.readParcelable(Uri.class.getClassLoader()); - file = (File) in.readSerializable(); - } - - public Uri getContentUri() { - return contentUri; - } - - public File getFile() { - return file; - } - - public String getFilePath() { - return file.getPath(); - } - - public Uri getMediaUri() { - return Uri.parse(getFilePath()); - } - - public String getMimeType(Context context) { - return FileUtils.getMimeType(context, getMediaUri()); - } - - @Override - public int describeContents() { - return 0; - } - - /** - * First try to get the file creation date from EXIF else fall back to CP - * @param context - * @return - */ - @Nullable - public DateTimeWithSource getFileCreatedDate(Context context) { - DateTimeWithSource dateTimeFromExif = getDateTimeFromExif(); - if (dateTimeFromExif == null) { - return getFileCreatedDateFromCP(context); - } else { - return dateTimeFromExif; - } - } - - /** - * Get filePath creation date from uri from all possible content providers - * - * @return - */ - private DateTimeWithSource getFileCreatedDateFromCP(Context context) { - try { - Cursor cursor = context.getContentResolver().query(contentUri, null, null, null, null); - if (cursor == null) { - return null;//Could not fetch last_modified - } - //Content provider contracts for opening gallery from the app and that by sharing from gallery from outside are different and we need to handle both the cases - int lastModifiedColumnIndex = cursor.getColumnIndex("last_modified");//If gallery is opened from in app - if (lastModifiedColumnIndex == -1) { - lastModifiedColumnIndex = cursor.getColumnIndex("datetaken"); - } - //If both the content providers do not give the data, lets leave it to Jesus - if (lastModifiedColumnIndex == -1) { - cursor.close(); - return null; - } - cursor.moveToFirst(); - return new DateTimeWithSource(cursor.getLong(lastModifiedColumnIndex), DateTimeWithSource.CP_SOURCE); - } catch (Exception e) { - return null;////Could not fetch last_modified - } - } - - /** - * Indicate whether the EXIF contains the location (both latitude and longitude). - * - * @return whether the location exists for the file's EXIF - */ - public boolean hasLocation() { - try { - ExifInterface exif = new ExifInterface(file.getAbsolutePath()); - final String latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); - final String longitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); - return latitude != null && longitude != null; - } catch (IOException | NumberFormatException | IndexOutOfBoundsException e) { - Timber.tag("UploadableFile"); - Timber.d(e); - } - return false; - } - - /** - * Get filePath creation date from uri from EXIF - * - * @return - */ - private DateTimeWithSource getDateTimeFromExif() { - try { - ExifInterface exif = new ExifInterface(file.getAbsolutePath()); - // TAG_DATETIME returns the last edited date, we need TAG_DATETIME_ORIGINAL for creation date - // See issue https://github.com/commons-app/apps-android-commons/issues/1971 - String dateTimeSubString = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL); - if (dateTimeSubString!=null) { //getAttribute may return null - String year = dateTimeSubString.substring(0,4); - String month = dateTimeSubString.substring(5,7); - String day = dateTimeSubString.substring(8,10); - // This date is stored as a string (not as a date), the rason is we don't want to include timezones - String dateCreatedString = String.format("%04d-%02d-%02d", Integer.parseInt(year), Integer.parseInt(month), Integer.parseInt(day)); - if (dateCreatedString.length() == 10) { //yyyy-MM-dd format of date is expected - @SuppressLint("RestrictedApi") Long dateTime = exif.getDateTimeOriginal(); - if(dateTime != null){ - Date date = new Date(dateTime); - return new DateTimeWithSource(date, dateCreatedString, DateTimeWithSource.EXIF_SOURCE); - } - } - } - } catch (IOException | NumberFormatException | IndexOutOfBoundsException e) { - Timber.tag("UploadableFile"); - Timber.d(e); - } - return null; - } - - @Override - public void writeToParcel(Parcel parcel, int i) { - parcel.writeParcelable(contentUri, 0); - parcel.writeSerializable(file); - } - - /** - * This class contains the epochDate along with the source from which it was extracted - */ - public class DateTimeWithSource { - public static final String CP_SOURCE = "contentProvider"; - public static final String EXIF_SOURCE = "exif"; - - private final long epochDate; - private String dateString; // this does not includes timezone information - private final String source; - - public DateTimeWithSource(long epochDate, String source) { - this.epochDate = epochDate; - this.source = source; - } - - public DateTimeWithSource(Date date, String source) { - this.epochDate = date.getTime(); - this.source = source; - } - - public DateTimeWithSource(Date date, String dateString, String source) { - this.epochDate = date.getTime(); - this.dateString = dateString; - this.source = source; - } - - public long getEpochDate() { - return epochDate; - } - - public String getDateString() { - return dateString; - } - - public String getSource() { - return source; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.kt new file mode 100644 index 000000000..1398e7785 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.kt @@ -0,0 +1,168 @@ +package fr.free.nrw.commons.filepicker + +import android.annotation.SuppressLint +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable + +import androidx.exifinterface.media.ExifInterface + +import fr.free.nrw.commons.upload.FileUtils +import java.io.File +import java.io.IOException +import java.util.Date +import timber.log.Timber + +class UploadableFile : Parcelable { + + val contentUri: Uri + val file: File + + constructor(contentUri: Uri, file: File) { + this.contentUri = contentUri + this.file = file + } + + constructor(file: File) { + this.file = file + this.contentUri = Uri.fromFile(File(file.path)) + } + + private constructor(parcel: Parcel) { + contentUri = parcel.readParcelable(Uri::class.java.classLoader)!! + file = parcel.readSerializable() as File + } + + fun getFilePath(): String { + return file.path + } + + fun getMediaUri(): Uri { + return Uri.parse(getFilePath()) + } + + fun getMimeType(context: Context): String? { + return FileUtils.getMimeType(context, getMediaUri()) + } + + override fun describeContents(): Int = 0 + + /** + * First try to get the file creation date from EXIF, else fall back to Content Provider (CP) + */ + fun getFileCreatedDate(context: Context): DateTimeWithSource? { + return getDateTimeFromExif() ?: getFileCreatedDateFromCP(context) + } + + /** + * Get filePath creation date from URI using all possible content providers + */ + private fun getFileCreatedDateFromCP(context: Context): DateTimeWithSource? { + return try { + val cursor: Cursor? = context.contentResolver.query(contentUri, null, null, null, null) + cursor?.use { + val lastModifiedColumnIndex = cursor + .getColumnIndex( + "last_modified" + ).takeIf { it != -1 } + ?: cursor.getColumnIndex("datetaken") + if (lastModifiedColumnIndex == -1) return null // No valid column found + cursor.moveToFirst() + DateTimeWithSource( + cursor.getLong( + lastModifiedColumnIndex + ), DateTimeWithSource.CP_SOURCE) + } + } catch (e: Exception) { + Timber.tag("UploadableFile").d(e) + null + } + } + + /** + * Indicates whether the EXIF contains the location (both latitude and longitude). + */ + fun hasLocation(): Boolean { + return try { + val exif = ExifInterface(file.absolutePath) + val latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) + val longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE) + latitude != null && longitude != null + } catch (e: IOException) { + Timber.tag("UploadableFile").d(e) + false + } + } + + /** + * Get filePath creation date from URI using EXIF data + */ + private fun getDateTimeFromExif(): DateTimeWithSource? { + return try { + val exif = ExifInterface(file.absolutePath) + val dateTimeSubString = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL) + if (dateTimeSubString != null) { + val year = dateTimeSubString.substring(0, 4).toInt() + val month = dateTimeSubString.substring(5, 7).toInt() + val day = dateTimeSubString.substring(8, 10).toInt() + val dateCreatedString = "%04d-%02d-%02d".format(year, month, day) + if (dateCreatedString.length == 10) { + @SuppressLint("RestrictedApi") + val dateTime = exif.dateTimeOriginal + if (dateTime != null) { + val date = Date(dateTime) + return DateTimeWithSource(date, dateCreatedString, DateTimeWithSource.EXIF_SOURCE) + } + } + } + null + } catch (e: Exception) { + Timber.tag("UploadableFile").d(e) + null + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(contentUri, flags) + parcel.writeSerializable(file) + } + + class DateTimeWithSource { + companion object { + const val CP_SOURCE = "contentProvider" + const val EXIF_SOURCE = "exif" + } + + val epochDate: Long + var dateString: String? = null + val source: String + + constructor(epochDate: Long, source: String) { + this.epochDate = epochDate + this.source = source + } + + constructor(date: Date, source: String) { + epochDate = date.time + this.source = source + } + + constructor(date: Date, dateString: String, source: String) { + epochDate = date.time + this.dateString = dateString + this.source = source + } + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): UploadableFile { + return UploadableFile(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt index 86ee5c4fe..91146059d 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt @@ -38,6 +38,7 @@ import fr.free.nrw.commons.campaigns.CampaignView import fr.free.nrw.commons.contributions.ContributionController import fr.free.nrw.commons.contributions.MainActivity import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.filepicker.FilePicker import fr.free.nrw.commons.kvstore.JsonKvStore import fr.free.nrw.commons.location.LocationServiceManager import fr.free.nrw.commons.logging.CommonsLogSender @@ -83,9 +84,17 @@ class SettingsFragment : PreferenceFragmentCompat() { private val cameraPickLauncherForResult: ActivityResultLauncher = registerForActivityResult(StartActivityForResult()) { result -> - contributionController.handleActivityResultWithCallback(requireActivity()) { callbacks -> - contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks) - } + contributionController.handleActivityResultWithCallback( + requireActivity(), + object: FilePicker.HandleActivityResult { + override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) { + contributionController.onPictureReturnedFromCamera( + result, + requireActivity(), + callbacks + ) + } + }) } /** diff --git a/app/src/main/java/fr/free/nrw/commons/utils/CustomSelectorUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/CustomSelectorUtils.kt index fc80252fc..62bd3f1a9 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/CustomSelectorUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/CustomSelectorUtils.kt @@ -63,7 +63,7 @@ class CustomSelectorUtils { fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact()) val sha1 = fileUtilsWrapper.getSHA1( - fileUtilsWrapper.getFileInputStream(uploadableFile.filePath), + fileUtilsWrapper.getFileInputStream(uploadableFile.getFilePath()), ) uploadableFile.file.delete() sha1 diff --git a/app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.java b/app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.java deleted file mode 100644 index 4da9e2690..000000000 --- a/app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.java +++ /dev/null @@ -1,32 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import android.database.Cursor; -import android.database.MatrixCursor; -import android.net.Uri; -import android.provider.OpenableColumns; -import androidx.core.content.FileProvider; -import org.robolectric.annotation.Implementation; -import org.robolectric.annotation.Implements; - -@Implements(FileProvider.class) -public class ShadowFileProvider { - - @Implementation - public Cursor query(final Uri uri, final String[] projection, final String selection, - final String[] selectionArgs, - final String sortOrder) { - - if (uri == null) { - return null; - } - - final String[] columns = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}; - final Object[] values = {"dummy", 500}; - final MatrixCursor cursor = new MatrixCursor(columns, 1); - - if (!uri.equals(Uri.EMPTY)) { - cursor.addRow(values); - } - return cursor; - } -} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.kt b/app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.kt new file mode 100644 index 000000000..fc9d20cf6 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.kt @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.filepicker + +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import android.provider.OpenableColumns +import androidx.core.content.FileProvider +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements + +@Implements(FileProvider::class) +class ShadowFileProvider { + + @Implementation + fun query( + uri: Uri?, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + + if (uri == null) { + return null + } + + val columns = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE) + val values = arrayOf("dummy", 500) + val cursor = MatrixCursor(columns, 1) + + if (uri != Uri.EMPTY) { + cursor.addRow(values) + } + return cursor + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt index 29a35c1e5..861d1a6a4 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt @@ -62,7 +62,7 @@ class UploadPresenterTest { `when`(repository.buildContributions()).thenReturn(Observable.just(contribution)) uploadableFiles.add(uploadableFile) `when`(view.uploadableFiles).thenReturn(uploadableFiles) - `when`(uploadableFile.filePath).thenReturn("data://test") + `when`(uploadableFile.getFilePath()).thenReturn("data://test") } /** From ae52267a277976474eb2fa7aa2826f3873765800 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Fri, 6 Dec 2024 02:50:29 -0600 Subject: [PATCH 56/74] Convert wikidata/mwapi to kotlin (part 2) (#5999) * Convert DepictSearchResponse to kotlin * Convert Entities to kotlin * Convert WikiSite to kotlin --------- Co-authored-by: Nicolas Raoul --- .../fr/free/nrw/commons/AboutActivityTest.kt | 2 +- .../free/nrw/commons/di/NetworkingModule.kt | 6 +- .../wikidata/model/DepictSearchResponse.java | 24 -- .../wikidata/model/DepictSearchResponse.kt | 12 + .../nrw/commons/wikidata/model/Entities.java | 106 ------- .../nrw/commons/wikidata/model/Entities.kt | 64 ++++ .../nrw/commons/wikidata/model/WikiSite.java | 292 ------------------ .../nrw/commons/wikidata/model/WikiSite.kt | 269 ++++++++++++++++ 8 files changed, 348 insertions(+), 427 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.kt diff --git a/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt index 45ff9e49d..50dfe8e7f 100644 --- a/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt +++ b/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt @@ -105,7 +105,7 @@ class AboutActivityTest { fun testLaunchTranslate() { Espresso.onView(ViewMatchers.withId(R.id.about_translate)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click()) - val langCode = CommonsApplication.instance.languageLookUpTable!!.codes[0] + val langCode = CommonsApplication.instance.languageLookUpTable!!.getCodes()[0] Intents.intended( CoreMatchers.allOf( IntentMatchers.hasAction(Intent.ACTION_VIEW), diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt index 7ca3b4fd0..0e9d83478 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt @@ -44,7 +44,6 @@ import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor.Level import timber.log.Timber import java.io.File -import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Named import javax.inject.Singleton @@ -293,9 +292,8 @@ class NetworkingModule { @Provides @Singleton @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) - fun provideLanguageWikipediaSite(): WikiSite { - return WikiSite.forLanguageCode(Locale.getDefault().language) - } + fun provideLanguageWikipediaSite(): WikiSite = + WikiSite.forDefaultLocaleLanguageCode() companion object { private const val WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql" diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.java deleted file mode 100644 index 8ea2fa1ed..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package fr.free.nrw.commons.wikidata.model; - -import java.util.List; - -/** - * Model class for API response obtained from search for depictions - */ -public class DepictSearchResponse { - private final List search; - - /** - * Constructor to initialise value of the search object - */ - public DepictSearchResponse(List search) { - this.search = search; - } - - /** - * @return List for the DepictSearchResponse - */ - public List getSearch() { - return search; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.kt new file mode 100644 index 000000000..5a0ed8c49 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.kt @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.wikidata.model + +/** + * Model class for API response obtained from search for depictions + */ +class DepictSearchResponse( + /** + * @return List for the DepictSearchResponse + + */ + val search: List +) diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.java deleted file mode 100644 index 9dab836cf..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.java +++ /dev/null @@ -1,106 +0,0 @@ -package fr.free.nrw.commons.wikidata.model; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.gson.annotations.SerializedName; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; -import fr.free.nrw.commons.wikidata.mwapi.MwResponse; - - -public class Entities extends MwResponse { - @Nullable private Map entities; - private int success; - - @NotNull - public Map entities() { - return entities != null ? entities : Collections.emptyMap(); - } - - public int getSuccess() { - return success; - } - - @Nullable public Entity getFirst() { - if (entities == null) { - return null; - } - return entities.values().iterator().next(); - } - - @Override - public void postProcess() { - if (getFirst() != null && getFirst().isMissing()) { - throw new RuntimeException("The requested entity was not found."); - } - } - - public static class Entity { - @Nullable private String type; - @Nullable private String id; - @Nullable private Map labels; - @Nullable private Map descriptions; - @Nullable private Map sitelinks; - @Nullable @SerializedName(value = "statements", alternate = "claims") private Map> statements; - @Nullable private String missing; - - @NonNull public String id() { - return StringUtils.defaultString(id); - } - - @NonNull public Map labels() { - return labels != null ? labels : Collections.emptyMap(); - } - - @NonNull public Map descriptions() { - return descriptions != null ? descriptions : Collections.emptyMap(); - } - - @NonNull public Map sitelinks() { - return sitelinks != null ? sitelinks : Collections.emptyMap(); - } - - @Nullable - public Map> getStatements() { - return statements; - } - - boolean isMissing() { - return "-1".equals(id) && missing != null; - } - } - - public static class Label { - @Nullable private String language; - @Nullable private String value; - - public Label(@Nullable final String language, @Nullable final String value) { - this.language = language; - this.value = value; - } - - @NonNull public String language() { - return StringUtils.defaultString(language); - } - - @NonNull public String value() { - return StringUtils.defaultString(value); - } - } - - public static class SiteLink { - @Nullable private String site; - @Nullable private String title; - - @NonNull public String getSite() { - return StringUtils.defaultString(site); - } - - @NonNull public String getTitle() { - return StringUtils.defaultString(title); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.kt new file mode 100644 index 000000000..588dbd262 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.kt @@ -0,0 +1,64 @@ +package fr.free.nrw.commons.wikidata.model + +import com.google.gson.annotations.SerializedName +import fr.free.nrw.commons.wikidata.mwapi.MwResponse +import org.apache.commons.lang3.StringUtils + +class Entities : MwResponse() { + private val entities: Map? = null + val success: Int = 0 + + fun entities(): Map = entities ?: emptyMap() + + private val first : Entity? + get() = entities?.values?.iterator()?.next() + + override fun postProcess() { + first?.let { + if (it.isMissing()) throw RuntimeException("The requested entity was not found.") + } + } + + class Entity { + private val type: String? = null + private val id: String? = null + private val labels: Map? = null + private val descriptions: Map? = null + private val sitelinks: Map? = null + + @SerializedName(value = "statements", alternate = ["claims"]) + val statements: Map>? = null + private val missing: String? = null + + fun id(): String = + StringUtils.defaultString(id) + + fun labels(): Map = + labels ?: emptyMap() + + fun descriptions(): Map = + descriptions ?: emptyMap() + + fun sitelinks(): Map = + sitelinks ?: emptyMap() + + fun isMissing(): Boolean = + "-1" == id && missing != null + } + + class Label(private val language: String?, private val value: String?) { + fun language(): String = + StringUtils.defaultString(language) + + fun value(): String = + StringUtils.defaultString(value) + } + + class SiteLink { + val site: String? = null + get() = StringUtils.defaultString(field) + + private val title: String? = null + get() = StringUtils.defaultString(field) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.java deleted file mode 100644 index 204ea0ab4..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.java +++ /dev/null @@ -1,292 +0,0 @@ -package fr.free.nrw.commons.wikidata.model; - -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; -import android.text.TextUtils; - -import androidx.annotation.NonNull; - -import com.google.gson.annotations.SerializedName; - -import org.apache.commons.lang3.StringUtils; -import fr.free.nrw.commons.language.AppLanguageLookUpTable; - -/** - * The base URL and Wikipedia language code for a MediaWiki site. Examples: - * - *
    - * Name: scheme / authority / language code - *
  • English Wikipedia: HTTPS / en.wikipedia.org / en
  • - *
  • Chinese Wikipedia: HTTPS / zh.wikipedia.org / zh-hans or zh-hant
  • - *
  • Meta-Wiki: HTTPS / meta.wikimedia.org / (none)
  • - *
  • Test Wikipedia: HTTPS / test.wikipedia.org / test
  • - *
  • Võro Wikipedia: HTTPS / fiu-vro.wikipedia.org / fiu-vro
  • - *
  • Simple English Wikipedia: HTTPS / simple.wikipedia.org / simple
  • - *
  • Simple English Wikipedia (beta cluster mirror): HTTP / simple.wikipedia.beta.wmflabs.org / simple
  • - *
  • Development: HTTP / 192.168.1.11:8080 / (none)
  • - *
- * - * As shown above, the language code or mapping is part of the authority: - *
    - * Validity: authority / language code - *
  • Correct: "test.wikipedia.org" / "test"
  • - *
  • Correct: "wikipedia.org", ""
  • - *
  • Correct: "no.wikipedia.org", "nb"
  • - *
  • Incorrect: "wikipedia.org", "test"
  • - *
- */ -public class WikiSite implements Parcelable { - private static String WIKIPEDIA_URL = "https://wikipedia.org/"; - - public static final String DEFAULT_SCHEME = "https"; - private static String DEFAULT_BASE_URL = WIKIPEDIA_URL; - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public WikiSite createFromParcel(Parcel in) { - return new WikiSite(in); - } - - @Override - public WikiSite[] newArray(int size) { - return new WikiSite[size]; - } - }; - - // todo: remove @SerializedName. this is now in the TypeAdapter and a "uri" case may be added - @SerializedName("domain") @NonNull private final Uri uri; - @NonNull private String languageCode; - - public static WikiSite forLanguageCode(@NonNull String languageCode) { - Uri uri = ensureScheme(Uri.parse(DEFAULT_BASE_URL)); - return new WikiSite((languageCode.isEmpty() - ? "" : (languageCodeToSubdomain(languageCode) + ".")) + uri.getAuthority(), - languageCode); - } - - public WikiSite(@NonNull Uri uri) { - Uri tempUri = ensureScheme(uri); - String authority = tempUri.getAuthority(); - if (("wikipedia.org".equals(authority) || "www.wikipedia.org".equals(authority)) - && tempUri.getPath() != null && tempUri.getPath().startsWith("/wiki")) { - // Special case for Wikipedia only: assume English subdomain when none given. - authority = "en.wikipedia.org"; - } - String langVariant = getLanguageVariantFromUri(tempUri); - if (!TextUtils.isEmpty(langVariant)) { - languageCode = langVariant; - } else { - languageCode = authorityToLanguageCode(authority); - } - this.uri = new Uri.Builder() - .scheme(tempUri.getScheme()) - .encodedAuthority(authority) - .build(); - } - - /** Get language variant code from a Uri, e.g. "zh-*", otherwise returns empty string. */ - @NonNull - private String getLanguageVariantFromUri(@NonNull Uri uri) { - if (TextUtils.isEmpty(uri.getPath())) { - return ""; - } - String[] parts = StringUtils.split(StringUtils.defaultString(uri.getPath()), '/'); - return parts.length > 1 && !parts[0].equals("wiki") ? parts[0] : ""; - } - - public WikiSite(@NonNull String url) { - this(url.startsWith("http") ? Uri.parse(url) : url.startsWith("//") - ? Uri.parse(DEFAULT_SCHEME + ":" + url) : Uri.parse(DEFAULT_SCHEME + "://" + url)); - } - - public WikiSite(@NonNull String authority, @NonNull String languageCode) { - this(authority); - this.languageCode = languageCode; - } - - @NonNull - public String scheme() { - return TextUtils.isEmpty(uri.getScheme()) ? DEFAULT_SCHEME : uri.getScheme(); - } - - /** - * @return The complete wiki authority including language subdomain but not including scheme, - * authentication, port, nor trailing slash. - * - * @see URL syntax - */ - @NonNull - public String authority() { - return uri.getAuthority(); - } - - /** - * Like {@link #authority()} but with a "m." between the language subdomain and the rest of the host. - * Examples: - * - *
    - *
  • English Wikipedia: en.m.wikipedia.org
  • - *
  • Chinese Wikipedia: zh.m.wikipedia.org
  • - *
  • Meta-Wiki: meta.m.wikimedia.org
  • - *
  • Test Wikipedia: test.m.wikipedia.org
  • - *
  • Võro Wikipedia: fiu-vro.m.wikipedia.org
  • - *
  • Simple English Wikipedia: simple.m.wikipedia.org
  • - *
  • Simple English Wikipedia (beta cluster mirror): simple.m.wikipedia.beta.wmflabs.org
  • - *
  • Development: m.192.168.1.11
  • - *
- */ - @NonNull - public String mobileAuthority() { - return authorityToMobile(authority()); - } - - /** - * Get wiki's mobile URL - * Eg. https://en.m.wikipedia.org - * @return - */ - public String mobileUrl() { - return String.format("%1$s://%2$s", scheme(), mobileAuthority()); - } - - @NonNull - public String subdomain() { - return languageCodeToSubdomain(languageCode); - } - - /** - * @return A path without an authority for the segment including a leading "/". - */ - @NonNull - public String path(@NonNull String segment) { - return "/w/" + segment; - } - - - @NonNull public Uri uri() { - return uri; - } - - /** - * @return The canonical URL. e.g., https://en.wikipedia.org. - */ - @NonNull public String url() { - return uri.toString(); - } - - /** - * @return The canonical URL for segment. e.g., https://en.wikipedia.org/w/foo. - */ - @NonNull public String url(@NonNull String segment) { - return url() + path(segment); - } - - /** - * @return The wiki language code which may differ from the language subdomain. Empty if - * language code is unknown. Ex: "en", "zh-hans", "" - * - * @see AppLanguageLookUpTable - */ - @NonNull - public String languageCode() { - return languageCode; - } - - // Auto-generated - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - WikiSite wiki = (WikiSite) o; - - if (!uri.equals(wiki.uri)) { - return false; - } - return languageCode.equals(wiki.languageCode); - } - - // Auto-generated - @Override - public int hashCode() { - int result = uri.hashCode(); - result = 31 * result + languageCode.hashCode(); - return result; - } - - // Auto-generated - @Override - public String toString() { - return "WikiSite{" - + "uri=" + uri - + ", languageCode='" + languageCode + '\'' - + '}'; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeParcelable(uri, 0); - dest.writeString(languageCode); - } - - protected WikiSite(@NonNull Parcel in) { - this.uri = in.readParcelable(Uri.class.getClassLoader()); - this.languageCode = in.readString(); - } - - @NonNull - private static String languageCodeToSubdomain(@NonNull String languageCode) { - switch (languageCode) { - case AppLanguageLookUpTable.SIMPLIFIED_CHINESE_LANGUAGE_CODE: - case AppLanguageLookUpTable.TRADITIONAL_CHINESE_LANGUAGE_CODE: - case AppLanguageLookUpTable.CHINESE_CN_LANGUAGE_CODE: - case AppLanguageLookUpTable.CHINESE_HK_LANGUAGE_CODE: - case AppLanguageLookUpTable.CHINESE_MO_LANGUAGE_CODE: - case AppLanguageLookUpTable.CHINESE_SG_LANGUAGE_CODE: - case AppLanguageLookUpTable.CHINESE_TW_LANGUAGE_CODE: - return AppLanguageLookUpTable.CHINESE_LANGUAGE_CODE; - case AppLanguageLookUpTable.NORWEGIAN_BOKMAL_LANGUAGE_CODE: - return AppLanguageLookUpTable.NORWEGIAN_LEGACY_LANGUAGE_CODE; // T114042 - default: - return languageCode; - } - } - - @NonNull private static String authorityToLanguageCode(@NonNull String authority) { - String[] parts = authority.split("\\."); - final int minLengthForSubdomain = 3; - if (parts.length < minLengthForSubdomain - || parts.length == minLengthForSubdomain && parts[0].equals("m")) { - // "" - // wikipedia.org - // m.wikipedia.org - return ""; - } - return parts[0]; - } - - @NonNull private static Uri ensureScheme(@NonNull Uri uri) { - if (TextUtils.isEmpty(uri.getScheme())) { - return uri.buildUpon().scheme(DEFAULT_SCHEME).build(); - } - return uri; - } - - /** @param authority Host and optional port. */ - @NonNull private String authorityToMobile(@NonNull String authority) { - if (authority.startsWith("m.") || authority.contains(".m.")) { - return authority; - } - return authority.replaceFirst("^" + subdomain() + "\\.?", "$0m."); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.kt new file mode 100644 index 000000000..1cd0bb858 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.kt @@ -0,0 +1,269 @@ +package fr.free.nrw.commons.wikidata.model + +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import android.text.TextUtils +import com.google.gson.annotations.SerializedName +import fr.free.nrw.commons.language.AppLanguageLookUpTable +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.CHINESE_CN_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.CHINESE_HK_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.CHINESE_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.CHINESE_MO_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.CHINESE_SG_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.CHINESE_TW_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.NORWEGIAN_BOKMAL_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.NORWEGIAN_LEGACY_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.SIMPLIFIED_CHINESE_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.TRADITIONAL_CHINESE_LANGUAGE_CODE +import org.apache.commons.lang3.StringUtils +import java.util.Locale + +/** + * The base URL and Wikipedia language code for a MediaWiki site. Examples: + * + * + * Name: scheme / authority / language code + * * English Wikipedia: HTTPS / en.wikipedia.org / en + * * Chinese Wikipedia: HTTPS / zh.wikipedia.org / zh-hans or zh-hant + * * Meta-Wiki: HTTPS / meta.wikimedia.org / (none) + * * Test Wikipedia: HTTPS / test.wikipedia.org / test + * * Võro Wikipedia: HTTPS / fiu-vro.wikipedia.org / fiu-vro + * * Simple English Wikipedia: HTTPS / simple.wikipedia.org / simple + * * Simple English Wikipedia (beta cluster mirror): HTTP / simple.wikipedia.beta.wmflabs.org / simple + * * Development: HTTP / 192.168.1.11:8080 / (none) + * + * + * **As shown above, the language code or mapping is part of the authority:** + * + * Validity: authority / language code + * * Correct: "test.wikipedia.org" / "test" + * * Correct: "wikipedia.org", "" + * * Correct: "no.wikipedia.org", "nb" + * * Incorrect: "wikipedia.org", "test" + * + */ +class WikiSite : Parcelable { + //TODO: remove @SerializedName. this is now in the TypeAdapter and a "uri" case may be added + @SerializedName("domain") + private val uri: Uri + + private var languageCode: String? = null + + constructor(uri: Uri) { + val tempUri = ensureScheme(uri) + var authority = tempUri.authority + + if (authority.isWikipedia && tempUri.path?.startsWith("/wiki") == true) { + // Special case for Wikipedia only: assume English subdomain when none given. + authority = "en.wikipedia.org" + } + + val langVariant = getLanguageVariantFromUri(tempUri) + languageCode = if (!TextUtils.isEmpty(langVariant)) { + langVariant + } else { + authorityToLanguageCode(authority!!) + } + + this.uri = Uri.Builder() + .scheme(tempUri.scheme) + .encodedAuthority(authority) + .build() + } + + private val String?.isWikipedia: Boolean get() = + (this == "wikipedia.org" || this == "www.wikipedia.org") + + /** Get language variant code from a Uri, e.g. "zh-*", otherwise returns empty string. */ + private fun getLanguageVariantFromUri(uri: Uri): String { + if (TextUtils.isEmpty(uri.path)) { + return "" + } + val parts = StringUtils.split(StringUtils.defaultString(uri.path), '/') + return if (parts.size > 1 && parts[0] != "wiki") parts[0] else "" + } + + constructor(url: String) : this( + if (url.startsWith("http")) Uri.parse(url) else if (url.startsWith("//")) + Uri.parse("$DEFAULT_SCHEME:$url") + else + Uri.parse("$DEFAULT_SCHEME://$url") + ) + + constructor(authority: String, languageCode: String) : this(authority) { + this.languageCode = languageCode + } + + fun scheme(): String = + if (TextUtils.isEmpty(uri.scheme)) DEFAULT_SCHEME else uri.scheme!! + + /** + * @return The complete wiki authority including language subdomain but not including scheme, + * authentication, port, nor trailing slash. + * + * @see [URL syntax](https://en.wikipedia.org/wiki/Uniform_Resource_Locator.Syntax) + */ + fun authority(): String = uri.authority!! + + /** + * Like [.authority] but with a "m." between the language subdomain and the rest of the host. + * Examples: + * + * + * * English Wikipedia: en.m.wikipedia.org + * * Chinese Wikipedia: zh.m.wikipedia.org + * * Meta-Wiki: meta.m.wikimedia.org + * * Test Wikipedia: test.m.wikipedia.org + * * Võro Wikipedia: fiu-vro.m.wikipedia.org + * * Simple English Wikipedia: simple.m.wikipedia.org + * * Simple English Wikipedia (beta cluster mirror): simple.m.wikipedia.beta.wmflabs.org + * * Development: m.192.168.1.11 + * + */ + fun mobileAuthority(): String = authorityToMobile(authority()) + + /** + * Get wiki's mobile URL + * Eg. https://en.m.wikipedia.org + * @return + */ + fun mobileUrl(): String = String.format("%1\$s://%2\$s", scheme(), mobileAuthority()) + + fun subdomain(): String = languageCodeToSubdomain(languageCode!!) + + /** + * @return A path without an authority for the segment including a leading "/". + */ + fun path(segment: String): String = "/w/$segment" + + + fun uri(): Uri = uri + + /** + * @return The canonical URL. e.g., https://en.wikipedia.org. + */ + fun url(): String = uri.toString() + + /** + * @return The canonical URL for segment. e.g., https://en.wikipedia.org/w/foo. + */ + fun url(segment: String): String = url() + path(segment) + + /** + * @return The wiki language code which may differ from the language subdomain. Empty if + * language code is unknown. Ex: "en", "zh-hans", "" + * + * @see AppLanguageLookUpTable + */ + fun languageCode(): String = languageCode!! + + // Auto-generated + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o == null || javaClass != o.javaClass) { + return false + } + + val wiki = o as WikiSite + + if (uri != wiki.uri) { + return false + } + return languageCode == wiki.languageCode + } + + // Auto-generated + override fun hashCode(): Int { + var result = uri.hashCode() + result = 31 * result + languageCode.hashCode() + return result + } + + // Auto-generated + override fun toString(): String { + return ("WikiSite{" + + "uri=" + uri + + ", languageCode='" + languageCode + '\'' + + '}') + } + + override fun describeContents(): Int = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeParcelable(uri, 0) + dest.writeString(languageCode) + } + + protected constructor(`in`: Parcel) { + uri = `in`.readParcelable(Uri::class.java.classLoader)!! + languageCode = `in`.readString() + } + + /** @param authority Host and optional port. + */ + private fun authorityToMobile(authority: String): String { + if (authority.startsWith("m.") || authority.contains(".m.")) { + return authority + } + return authority.replaceFirst(("^" + subdomain() + "\\.?").toRegex(), "$0m.") + } + + companion object { + const val WIKIPEDIA_URL = "https://wikipedia.org/" + const val DEFAULT_SCHEME: String = "https" + + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): WikiSite { + return WikiSite(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + + fun forDefaultLocaleLanguageCode(): WikiSite { + val languageCode: String = Locale.getDefault().language + val subdomain = if (languageCode.isEmpty()) "" else languageCodeToSubdomain(languageCode) + "." + val uri = ensureScheme(Uri.parse(WIKIPEDIA_URL)) + return WikiSite(subdomain + uri.authority, languageCode) + } + + private fun languageCodeToSubdomain(languageCode: String): String = when (languageCode) { + SIMPLIFIED_CHINESE_LANGUAGE_CODE, + TRADITIONAL_CHINESE_LANGUAGE_CODE, + CHINESE_CN_LANGUAGE_CODE, + CHINESE_HK_LANGUAGE_CODE, + CHINESE_MO_LANGUAGE_CODE, + CHINESE_SG_LANGUAGE_CODE, + CHINESE_TW_LANGUAGE_CODE -> CHINESE_LANGUAGE_CODE + + NORWEGIAN_BOKMAL_LANGUAGE_CODE -> NORWEGIAN_LEGACY_LANGUAGE_CODE // T114042 + + else -> languageCode + } + + private fun authorityToLanguageCode(authority: String): String { + val parts = authority.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val minLengthForSubdomain = 3 + if (parts.size < minLengthForSubdomain || parts.size == minLengthForSubdomain && parts[0] == "m") { + // "" + // wikipedia.org + // m.wikipedia.org + return "" + } + return parts[0] + } + + private fun ensureScheme(uri: Uri): Uri { + if (TextUtils.isEmpty(uri.scheme)) { + return uri.buildUpon().scheme(DEFAULT_SCHEME).build() + } + return uri + } + } +} From a8387f01c9e7d54b4f6cd7b7da342c64f38de352 Mon Sep 17 00:00:00 2001 From: Neel Doshi <60827173+neeldoshii@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:45:47 +0530 Subject: [PATCH 57/74] Bug Fixs & Enhancement of Achievement Screen (#5666) * Rename AchievementFragment from `.java` to `.kt` * Migrated AchievementFragment to kotlin * Revamped Achievement Screen * fixed AchievementFragment Unit Test * fixed Level on MoreBottomSheetFragment * Implemented Badge and Minor Code Refactor * Fixed the badge issue & made the badge clickable * Removed Redundant XML Code & Converted badges to green color and added values inside it * Fixed : showSnackBarWithRetry Test * Fixed : Theme issues on Light Mode --------- Co-authored-by: Nicolas Raoul --- app/build.gradle | 2 +- .../commons/navtab/MoreBottomSheetFragment.kt | 14 +- .../nrw/commons/profile/ProfileActivity.java | 1 - .../achievements/AchievementsFragment.java | 492 --------- .../achievements/AchievementsFragment.kt | 566 ++++++++++ .../main/res/layout/fragment_achievements.xml | 986 +++++++----------- app/src/main/res/values/strings.xml | 4 +- app/src/main/res/values/styles.xml | 5 +- .../AchievementsFragmentUnitTests.kt | 2 +- 9 files changed, 942 insertions(+), 1130 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt diff --git a/app/build.gradle b/app/build.gradle index 468255d38..b83f2b01b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -47,7 +47,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' - implementation "com.google.android.material:material:1.9.0" + implementation "com.google.android.material:material:1.12.0" implementation 'com.karumi:dexter:5.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt index 857e18ec3..cbdf5f087 100644 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt @@ -111,10 +111,18 @@ class MoreBottomSheetFragment : BottomSheetDialogFragment() { private fun setUserName() { val store = BasicKvStore(requireContext(), getUserName()) val level = store.getString("userAchievementsLevel", "0") - binding?.moreProfile?.text = if (level == "0") { - "${getUserName()} (${getString(R.string.see_your_achievements)})" + if (level == "0"){ + binding?.moreProfile?.text = getString( + R.string.profileLevel, + getUserName(), + getString(R.string.see_your_achievements) // Second argument + ) } else { - "${getUserName()} (${getString(R.string.level)} $level)" + binding?.moreProfile?.text = getString( + R.string.profileLevel, + getUserName(), + level + ) } } diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java index 60a0f47a1..390768416 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java @@ -16,7 +16,6 @@ import androidx.annotation.NonNull; import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; -import com.google.android.material.tabs.TabLayout; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.ViewPagerAdapter; diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java deleted file mode 100644 index ef6a323b2..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java +++ /dev/null @@ -1,492 +0,0 @@ -package fr.free.nrw.commons.profile.achievements; - -import android.accounts.Account; -import android.content.Context; -import android.net.Uri; -import android.os.Bundle; -import android.util.DisplayMetrics; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; -import androidx.annotation.Nullable; -import androidx.appcompat.view.ContextThemeWrapper; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.databinding.FragmentAchievementsBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.kvstore.BasicKvStore; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.ViewUtil; -import fr.free.nrw.commons.profile.ProfileActivity; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.util.Locale; -import java.util.Objects; -import javax.inject.Inject; -import org.apache.commons.lang3.StringUtils; -import timber.log.Timber; - -/** - * fragment for sharing feedback on uploaded activity - */ -public class AchievementsFragment extends CommonsDaggerSupportFragment { - - private static final double BADGE_IMAGE_WIDTH_RATIO = 0.4; - private static final double BADGE_IMAGE_HEIGHT_RATIO = 0.3; - - /** - * Help link URLs - */ - private static final String IMAGES_UPLOADED_URL = "https://commons.wikimedia.org/wiki/Commons:Project_scope"; - private static final String IMAGES_REVERT_URL = "https://commons.wikimedia.org/wiki/Commons:Deletion_policy#Reasons_for_deletion"; - private static final String IMAGES_USED_URL = "https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images"; - private static final String IMAGES_NEARBY_PLACES_URL = "https://www.wikidata.org/wiki/Property:P18"; - private static final String IMAGES_FEATURED_URL = "https://commons.wikimedia.org/wiki/Commons:Featured_pictures"; - private static final String QUALITY_IMAGE_URL = "https://commons.wikimedia.org/wiki/Commons:Quality_images"; - private static final String THANKS_URL = "https://www.mediawiki.org/wiki/Extension:Thanks"; - - private LevelController.LevelInfo levelInfo; - - @Inject - SessionManager sessionManager; - - @Inject - OkHttpJsonApiClient okHttpJsonApiClient; - - private FragmentAchievementsBinding binding; - - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - - // To keep track of the number of wiki edits made by a user - private int numberOfEdits = 0; - - private String userName; - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (getArguments() != null) { - userName = getArguments().getString(ProfileActivity.KEY_USERNAME); - } - } - - /** - * This method helps in the creation Achievement screen and - * dynamically set the size of imageView - * - * @param savedInstanceState Data bundle - */ - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentAchievementsBinding.inflate(inflater, container, false); - View rootView = binding.getRoot(); - - binding.achievementInfo.setOnClickListener(view -> showInfoDialog()); - binding.imagesUploadInfo.setOnClickListener(view -> showUploadInfo()); - binding.imagesRevertedInfo.setOnClickListener(view -> showRevertedInfo()); - binding.imagesUsedByWikiInfo.setOnClickListener(view -> showUsedByWikiInfo()); - binding.imagesNearbyInfo.setOnClickListener(view -> showImagesViaNearbyInfo()); - binding.imagesFeaturedInfo.setOnClickListener(view -> showFeaturedImagesInfo()); - binding.thanksReceivedInfo.setOnClickListener(view -> showThanksReceivedInfo()); - binding.qualityImagesInfo.setOnClickListener(view -> showQualityImagesInfo()); - - // DisplayMetrics used to fetch the size of the screen - DisplayMetrics displayMetrics = new DisplayMetrics(); - getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - int height = displayMetrics.heightPixels; - int width = displayMetrics.widthPixels; - - // Used for the setting the size of imageView at runtime - ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) - binding.achievementBadgeImage.getLayoutParams(); - params.height = (int) (height * BADGE_IMAGE_HEIGHT_RATIO); - params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO); - binding.achievementBadgeImage.requestLayout(); - binding.progressBar.setVisibility(View.VISIBLE); - - setHasOptionsMenu(true); - - // Set the initial value of WikiData edits to 0 - binding.wikidataEdits.setText("0"); - if(sessionManager.getUserName() == null || sessionManager.getUserName().equals(userName)){ - binding.tvAchievementsOfUser.setVisibility(View.GONE); - }else{ - binding.tvAchievementsOfUser.setVisibility(View.VISIBLE); - binding.tvAchievementsOfUser.setText(getString(R.string.achievements_of_user,userName)); - } - - // Achievements currently unimplemented in Beta flavor. Skip all API calls. - if(ConfigUtils.isBetaFlavour()) { - binding.progressBar.setVisibility(View.GONE); - binding.imagesUsedByWikiText.setText(R.string.no_image); - binding.imagesRevertedText.setText(R.string.no_image_reverted); - binding.imagesUploadTextParam.setText(R.string.no_image_uploaded); - binding.wikidataEdits.setText("0"); - binding.imageFeatured.setText("0"); - binding.qualityImages.setText("0"); - binding.achievementLevel.setText("0"); - setMenuVisibility(true); - return rootView; - } - setWikidataEditCount(); - setAchievements(); - return rootView; - } - - @Override - public void onDestroyView() { - binding = null; - super.onDestroyView(); - } - - @Override - public void setMenuVisibility(boolean visible) { - super.setMenuVisibility(visible); - - // Whenever this fragment is revealed in a menu, - // notify Beta users the page data is unavailable - if(ConfigUtils.isBetaFlavour() && visible) { - Context ctx = null; - if(getContext() != null) { - ctx = getContext(); - } else if(getView() != null && getView().getContext() != null) { - ctx = getView().getContext(); - } - if(ctx != null) { - Toast.makeText(ctx, - R.string.achievements_unavailable_beta, - Toast.LENGTH_LONG).show(); - } - } - } - - /** - * To invoke the AlertDialog on clicking info button - */ - protected void showInfoDialog(){ - launchAlert( - getResources().getString(R.string.Achievements), - getResources().getString(R.string.achievements_info_message)); - } - - /** - * To call the API to get results in form Single - * which then calls parseJson when results are fetched - */ - private void setAchievements() { - binding.progressBar.setVisibility(View.VISIBLE); - if (checkAccount()) { - try{ - - compositeDisposable.add(okHttpJsonApiClient - .getAchievements(Objects.requireNonNull(userName)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null) { - setUploadCount(Achievements.from(response)); - } else { - Timber.d("success"); - binding.layoutImageReverts.setVisibility(View.INVISIBLE); - binding.achievementBadgeImage.setVisibility(View.INVISIBLE); - // If the number of edits made by the user are more than 150,000 - // in some cases such high number of wiki edit counts cause the - // achievements calculator to fail in some cases, for more details - // refer Issue: #3295 - if (numberOfEdits <= 150000) { - showSnackBarWithRetry(false); - } else { - showSnackBarWithRetry(true); - } - } - }, - t -> { - Timber.e(t, "Fetching achievements statistics failed"); - if (numberOfEdits <= 150000) { - showSnackBarWithRetry(false); - } else { - showSnackBarWithRetry(true); - } - } - )); - } - catch (Exception e){ - Timber.d(e+"success"); - } - } - } - - /** - * To call the API to fetch the count of wiki data edits - * in the form of JavaRx Single object - */ - private void setWikidataEditCount() { - if (StringUtils.isBlank(userName)) { - return; - } - compositeDisposable.add(okHttpJsonApiClient - .getWikidataEdits(userName) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(edits -> { - numberOfEdits = edits; - binding.wikidataEdits.setText(String.valueOf(edits)); - }, e -> { - Timber.e("Error:" + e); - })); - } - - /** - * Shows a snack bar which has an action button which on click dismisses the snackbar and invokes the - * listener passed - * @param tooManyAchievements if this value is true it means that the number of achievements of the - * user are so high that it wrecks havoc with the Achievements calculator due to which request may time - * out. Well this is the Ultimate Achievement - */ - private void showSnackBarWithRetry(boolean tooManyAchievements) { - if (tooManyAchievements) { - binding.progressBar.setVisibility(View.GONE); - ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content), - R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry, view -> setAchievements()); - } else { - binding.progressBar.setVisibility(View.GONE); - ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content), - R.string.achievements_fetch_failed, R.string.retry, view -> setAchievements()); - } - } - - /** - * Shows a generic error toast when error occurs while loading achievements or uploads - */ - private void onError() { - ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.error_occurred)); - binding.progressBar.setVisibility(View.GONE); - } - - /** - * used to the count of images uploaded by user - */ - private void setUploadCount(Achievements achievements) { - if (checkAccount()) { - compositeDisposable.add(okHttpJsonApiClient - .getUploadCount(Objects.requireNonNull(userName)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - uploadCount -> setAchievementsUploadCount(achievements, uploadCount), - t -> { - Timber.e(t, "Fetching upload count failed"); - onError(); - } - )); - } - } - - /** - * used to set achievements upload count and call hideProgressbar - * @param uploadCount - */ - private void setAchievementsUploadCount(Achievements achievements, int uploadCount) { - // Create a new instance of Achievements with updated imagesUploaded - Achievements updatedAchievements = new Achievements( - achievements.getUniqueUsedImages(), - achievements.getArticlesUsingImages(), - achievements.getThanksReceived(), - achievements.getFeaturedImages(), - achievements.getQualityImages(), - uploadCount, // Update imagesUploaded with new value - achievements.getRevertCount() - ); - - hideProgressBar(updatedAchievements); - } - - /** - * used to the uploaded images progressbar - * @param uploadCount - */ - private void setUploadProgress(int uploadCount){ - if (uploadCount==0){ - setZeroAchievements(); - }else { - binding.imagesUploadedProgressbar.setVisibility(View.VISIBLE); - binding.imagesUploadedProgressbar.setProgress - (100*uploadCount/levelInfo.getMaxUploadCount()); - binding.tvUploadedImages.setText - (uploadCount + "/" + levelInfo.getMaxUploadCount()); - } - - } - - private void setZeroAchievements() { - String message = !Objects.equals(sessionManager.getUserName(), userName) ? - getString(R.string.no_achievements_yet, userName) : - getString(R.string.you_have_no_achievements_yet); - DialogUtil.showAlertDialog(getActivity(), - null, - message, - getString(R.string.ok), - () -> {}, - true); -// binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE); -// binding.imageRevertsProgressbar.setVisibility(View.INVISIBLE); -// binding.imagesUsedByWikiProgressBar.setVisibility(View.INVISIBLE); - binding.achievementBadgeImage.setVisibility(View.INVISIBLE); - binding.imagesUsedByWikiText.setText(R.string.no_image); - binding.imagesRevertedText.setText(R.string.no_image_reverted); - binding.imagesUploadTextParam.setText(R.string.no_image_uploaded); - binding.achievementBadgeImage.setVisibility(View.INVISIBLE); - } - - /** - * used to set the non revert image percentage - * @param notRevertPercentage - */ - private void setImageRevertPercentage(int notRevertPercentage){ - binding.imageRevertsProgressbar.setVisibility(View.VISIBLE); - binding.imageRevertsProgressbar.setProgress(notRevertPercentage); - final String revertPercentage = Integer.toString(notRevertPercentage); - binding.tvRevertedImages.setText(revertPercentage + "%"); - binding.imagesRevertLimitText.setText(getResources().getString(R.string.achievements_revert_limit_message)+ levelInfo.getMinNonRevertPercentage() + "%"); - } - - /** - * Used the inflate the fetched statistics of the images uploaded by user - * and assign badge and level. Also stores the achievements level of the user in BasicKvStore to display in menu - * @param achievements - */ - private void inflateAchievements(Achievements achievements) { -// binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE); - binding.thanksReceived.setText(String.valueOf(achievements.getThanksReceived())); - binding.imagesUsedByWikiProgressBar.setProgress - (100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages()); - binding.tvWikiPb.setText(achievements.getUniqueUsedImages() + "/" - + levelInfo.getMaxUniqueImages()); - binding.imageFeatured.setText(String.valueOf(achievements.getFeaturedImages())); - binding.qualityImages.setText(String.valueOf(achievements.getQualityImages())); - String levelUpInfoString = getString(R.string.level).toUpperCase(Locale.ROOT); - levelUpInfoString += " " + levelInfo.getLevelNumber(); - binding.achievementLevel.setText(levelUpInfoString); - binding.achievementBadgeImage.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge, - new ContextThemeWrapper(getActivity(), levelInfo.getLevelStyle()).getTheme())); - binding.achievementBadgeText.setText(Integer.toString(levelInfo.getLevelNumber())); - BasicKvStore store = new BasicKvStore(this.getContext(), userName); - store.putString("userAchievementsLevel", Integer.toString(levelInfo.getLevelNumber())); - } - - /** - * to hide progressbar - */ - private void hideProgressBar(Achievements achievements) { - if (binding.progressBar != null) { - levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(), - achievements.getUniqueUsedImages(), - achievements.getNotRevertPercentage()); - inflateAchievements(achievements); - setUploadProgress(achievements.getImagesUploaded()); - setImageRevertPercentage(achievements.getNotRevertPercentage()); - binding.progressBar.setVisibility(View.GONE); - } - } - - protected void showUploadInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.images_uploaded), - getResources().getString(R.string.images_uploaded_explanation), - IMAGES_UPLOADED_URL); - } - - protected void showRevertedInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.image_reverts), - getResources().getString(R.string.images_reverted_explanation), - IMAGES_REVERT_URL); - } - - protected void showUsedByWikiInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.images_used_by_wiki), - getResources().getString(R.string.images_used_explanation), - IMAGES_USED_URL); - } - - protected void showImagesViaNearbyInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.statistics_wikidata_edits), - getResources().getString(R.string.images_via_nearby_explanation), - IMAGES_NEARBY_PLACES_URL); - } - - protected void showFeaturedImagesInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.statistics_featured), - getResources().getString(R.string.images_featured_explanation), - IMAGES_FEATURED_URL); - } - - protected void showThanksReceivedInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.statistics_thanks), - getResources().getString(R.string.thanks_received_explanation), - THANKS_URL); - } - - public void showQualityImagesInfo() { - launchAlertWithHelpLink( - getResources().getString(R.string.statistics_quality), - getResources().getString(R.string.quality_images_info), - QUALITY_IMAGE_URL); - } - - /** - * takes title and message as input to display alerts - * @param title - * @param message - */ - private void launchAlert(String title, String message){ - DialogUtil.showAlertDialog(getActivity(), - title, - message, - getString(R.string.ok), - () -> {}, - true); - } - - /** - * Launch Alert with a READ MORE button and clicking it open a custom webpage - */ - private void launchAlertWithHelpLink(String title, String message, String helpLinkUrl) { - DialogUtil.showAlertDialog(getActivity(), - title, - message, - getString(R.string.ok), - getString(R.string.read_help_link), - () -> {}, - () -> Utils.handleWebUrl(requireContext(), Uri.parse(helpLinkUrl)), - null, - true); - } - - /** - * check to ensure that user is logged in - * @return - */ - private boolean checkAccount(){ - Account currentAccount = sessionManager.getCurrentAccount(); - if (currentAccount == null) { - Timber.d("Current account is null"); - ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.user_not_logged_in)); - sessionManager.forceLogin(getActivity()); - return false; - } - return true; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt new file mode 100644 index 000000000..020a67f24 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt @@ -0,0 +1,566 @@ +package fr.free.nrw.commons.profile.achievements + +import android.net.Uri +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.widget.Toast +import androidx.appcompat.view.ContextThemeWrapper +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat +import com.google.android.material.badge.BadgeDrawable +import com.google.android.material.badge.BadgeUtils +import com.google.android.material.badge.ExperimentalBadgeUtils +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.databinding.FragmentAchievementsBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.kvstore.BasicKvStore +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.profile.ProfileActivity +import fr.free.nrw.commons.profile.achievements.LevelController.LevelInfo.Companion.from +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog +import fr.free.nrw.commons.utils.ViewUtil.showDismissibleSnackBar +import fr.free.nrw.commons.utils.ViewUtil.showLongToast +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import org.apache.commons.lang3.StringUtils +import timber.log.Timber +import java.util.Objects +import javax.inject.Inject + +class AchievementsFragment : CommonsDaggerSupportFragment(){ + private lateinit var levelInfo: LevelController.LevelInfo + + @Inject + lateinit var sessionManager: SessionManager + + @Inject + lateinit var okHttpJsonApiClient: OkHttpJsonApiClient + + private var _binding: FragmentAchievementsBinding? = null + private val binding get() = _binding!! + // To keep track of the number of wiki edits made by a user + private var numberOfEdits: Int = 0 + + private var userName: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + userName = it.getString(ProfileActivity.KEY_USERNAME) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAchievementsBinding.inflate(inflater, container, false) + + binding.achievementInfo.setOnClickListener { showInfoDialog() } + binding.imagesUploadInfoIcon.setOnClickListener { showUploadInfo() } + binding.imagesRevertedInfoIcon.setOnClickListener { showRevertedInfo() } + binding.imagesUsedByWikiInfoIcon.setOnClickListener { showUsedByWikiInfo() } + binding.wikidataEditsIcon.setOnClickListener { showImagesViaNearbyInfo() } + binding.featuredImageIcon.setOnClickListener { showFeaturedImagesInfo() } + binding.thanksImageIcon.setOnClickListener { showThanksReceivedInfo() } + binding.qualityImageIcon.setOnClickListener { showQualityImagesInfo() } + + // DisplayMetrics used to fetch the size of the screen + val displayMetrics = DisplayMetrics() + requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) + val height = displayMetrics.heightPixels + val width = displayMetrics.widthPixels + + // Used for the setting the size of imageView at runtime + // TODO REMOVE + val params = binding.achievementBadgeImage.layoutParams as ConstraintLayout.LayoutParams + params.height = (height * BADGE_IMAGE_HEIGHT_RATIO).toInt() + params.width = (width * BADGE_IMAGE_WIDTH_RATIO).toInt() + binding.achievementBadgeImage.requestLayout() + binding.progressBar.visibility = View.VISIBLE + + setHasOptionsMenu(true) + if (sessionManager.userName == null || sessionManager.userName == userName) { + binding.tvAchievementsOfUser.visibility = View.GONE + } else { + binding.tvAchievementsOfUser.visibility = View.VISIBLE + binding.tvAchievementsOfUser.text = getString(R.string.achievements_of_user, userName) + } + if (isBetaFlavour) { + binding.layout.visibility = View.GONE + setMenuVisibility(true) + return binding.root + } + + + setWikidataEditCount() + setAchievements() + return binding.root + + } + + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + + override fun setMenuVisibility(visible: Boolean) { + super.setMenuVisibility(visible) + + // Whenever this fragment is revealed in a menu, + // notify Beta users the page data is unavailable + if (isBetaFlavour && visible) { + val ctx = context ?: view?.context + ctx?.let { + Toast.makeText(it, R.string.achievements_unavailable_beta, Toast.LENGTH_LONG).show() + } + } + } + + /** + * To invoke the AlertDialog on clicking info button + */ + fun showInfoDialog() { + launchAlert( + resources.getString(R.string.Achievements), + resources.getString(R.string.achievements_info_message) + ) + } + + + + + /** + * To call the API to get results in form Single + * which then calls parseJson when results are fetched + */ + + private fun setAchievements() { + binding.progressBar.visibility = View.VISIBLE + if (checkAccount()) { + try { + compositeDisposable.add( + okHttpJsonApiClient + .getAchievements(userName ?: return) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response -> + if (response != null) { + setUploadCount(Achievements.from(response)) + } else { + Timber.d("Success") + // TODO Create a Method to Hide all the Statistics +// binding.layoutImageReverts.visibility = View.INVISIBLE +// binding.achievementBadgeImage.visibility = View.INVISIBLE + // If the number of edits made by the user are more than 150,000 + // in some cases such high number of wiki edit counts cause the + // achievements calculator to fail in some cases, for more details + // refer Issue: #3295 + if (numberOfEdits <= 150_000) { + showSnackBarWithRetry(false) + } else { + showSnackBarWithRetry(true) + } + } + }, + { throwable -> + Timber.e(throwable, "Fetching achievements statistics failed") + if (numberOfEdits <= 150_000) { + showSnackBarWithRetry(false) + } else { + showSnackBarWithRetry(true) + } + } + ) + ) + } catch (e: Exception) { + Timber.d("Exception: ${e.message}") + } + } + } + + /** + * To call the API to fetch the count of wiki data edits + * in the form of JavaRx Single object + */ + + private fun setWikidataEditCount() { + if (StringUtils.isBlank(userName)) { + return + } + compositeDisposable.add( + okHttpJsonApiClient + .getWikidataEdits(userName) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ edits: Int -> + numberOfEdits = edits + showBadgesWithCount(view = binding.wikidataEditsIcon, count = edits) + }, { e: Throwable -> + Timber.e("Error:$e") + }) + ) + } + + /** + * Shows a snack bar which has an action button which on click dismisses the snackbar and invokes the + * listener passed + * @param tooManyAchievements if this value is true it means that the number of achievements of the + * user are so high that it wrecks havoc with the Achievements calculator due to which request may time + * out. Well this is the Ultimate Achievement + */ + private fun showSnackBarWithRetry(tooManyAchievements: Boolean) { + if (tooManyAchievements) { + if (view == null) { + return + } + else { + binding.progressBar.visibility = View.GONE + showDismissibleSnackBar( + requireView().findViewById(android.R.id.content), + R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry + ) { setAchievements() } + } + + } else { + if (view == null) { + return + } + binding.progressBar.visibility = View.GONE + showDismissibleSnackBar( + requireView().findViewById(android.R.id.content), + R.string.achievements_fetch_failed, R.string.retry + ) { setAchievements() } + } + } + + /** + * Shows a generic error toast when error occurs while loading achievements or uploads + */ + private fun onError() { + showLongToast(requireActivity(), resources.getString(R.string.error_occurred)) + binding.progressBar.visibility = View.GONE + } + + /** + * used to the count of images uploaded by user + */ + + private fun setUploadCount(achievements: Achievements) { + if (checkAccount()) { + compositeDisposable.add(okHttpJsonApiClient + .getUploadCount(Objects.requireNonNull(userName)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { uploadCount: Int? -> + setAchievementsUploadCount( + achievements, + uploadCount ?:0 + ) + }, + { t: Throwable? -> + Timber.e(t, "Fetching upload count failed") + onError() + } + )) + } + } + + /** + * used to set achievements upload count and call hideProgressbar + * @param uploadCount + */ + private fun setAchievementsUploadCount(achievements: Achievements, uploadCount: Int) { + // Create a new instance of Achievements with updated imagesUploaded + val updatedAchievements = Achievements( + achievements.uniqueUsedImages, + achievements.articlesUsingImages, + achievements.thanksReceived, + achievements.featuredImages, + achievements.qualityImages, + uploadCount, // Update imagesUploaded with new value + achievements.revertCount + ) + + hideProgressBar(updatedAchievements) + } + + /** + * used to the uploaded images progressbar + * @param uploadCount + */ + private fun setUploadProgress(uploadCount: Int) { + if (uploadCount == 0) { + setZeroAchievements() + } else { + binding.imagesUploadedProgressbar.visibility = View.VISIBLE + binding.imagesUploadedProgressbar.progress = + 100 * uploadCount / levelInfo.maxUploadCount + binding.imageUploadedTVCount.text = uploadCount.toString() + "/" + levelInfo.maxUploadCount + } + } + + private fun setZeroAchievements() { + val message = if (sessionManager.userName != userName) { + getString(R.string.no_achievements_yet, userName ) + } else { + getString(R.string.you_have_no_achievements_yet) + } + showAlertDialog( + requireActivity(), + null, + message, + getString(R.string.ok), + {}, + true + ) + +// binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE); +// binding.imageRevertsProgressbar.setVisibility(View.INVISIBLE); +// binding.imagesUsedByWikiProgressBar.setVisibility(View.INVISIBLE); + //binding.achievementBadgeImage.visibility = View.INVISIBLE // TODO + binding.imagesUsedCount.setText(R.string.no_image) + binding.imagesRevertedText.setText(R.string.no_image_reverted) + binding.imagesUploadTextParam.setText(R.string.no_image_uploaded) + } + + /** + * used to set the non revert image percentage + * @param notRevertPercentage + */ + private fun setImageRevertPercentage(notRevertPercentage: Int) { + binding.imageRevertsProgressbar.visibility = View.VISIBLE + binding.imageRevertsProgressbar.progress = notRevertPercentage + val revertPercentage = notRevertPercentage.toString() + binding.imageRevertTVCount.text = "$revertPercentage%" + binding.imagesRevertLimitText.text = + resources.getString(R.string.achievements_revert_limit_message) + levelInfo.minNonRevertPercentage + "%" + } + + /** + * Used the inflate the fetched statistics of the images uploaded by user + * and assign badge and level. Also stores the achievements level of the user in BasicKvStore to display in menu + * @param achievements + */ + private fun inflateAchievements(achievements: Achievements) { + + // Thanks Received Badge + showBadgesWithCount(view = binding.thanksImageIcon, count = achievements.thanksReceived) + + // Featured Images Badge + showBadgesWithCount(view = binding.featuredImageIcon, count = achievements.featuredImages) + + // Quality Images Badge + showBadgesWithCount(view = binding.qualityImageIcon, count = achievements.qualityImages) + + binding.imagesUsedByWikiProgressBar.progress = + 100 * achievements.uniqueUsedImages / levelInfo.maxUniqueImages + binding.imagesUsedCount.text = (achievements.uniqueUsedImages.toString() + "/" + + levelInfo.maxUniqueImages) + + binding.achievementLevel.text = getString(R.string.level,levelInfo.levelNumber) + binding.achievementBadgeImage.setImageDrawable( + VectorDrawableCompat.create( + resources, R.drawable.badge, + ContextThemeWrapper(activity, levelInfo.levelStyle).theme + ) + ) + binding.achievementBadgeText.text = levelInfo.levelNumber.toString() + val store = BasicKvStore(requireContext(), userName) + store.putString("userAchievementsLevel", levelInfo.levelNumber.toString()) + } + + /** + * This function is used to show badge on any view (button, imageView, etc) + * @param view The View on which the badge will be displayed eg (button, imageView, etc) + * @param count The number to be displayed inside the badge. + * @param backgroundColor The badge background color. Default is R.attr.colorPrimary + * @param badgeTextColor The badge text color. Default is R.attr.colorPrimary + * @param badgeGravity The position of the badge [TOP_END,TOP_START,BOTTOM_END,BOTTOM_START]. Default is TOP_END + * @return if the number is 0, then it will not create badge for it and hide the view + * @see https://developer.android.com/reference/com/google/android/material/badge/BadgeDrawable + */ + + private fun showBadgesWithCount( + view: View, + count: Int, + backgroundColor: Int = R.attr.colorPrimary, + badgeTextColor: Int = R.attr.textEnabled, + badgeGravity: Int = BadgeDrawable.TOP_END + ) { + //https://stackoverflow.com/a/67742035 + if (count == 0) { + view.visibility = View.GONE + return + } + + view.viewTreeObserver.addOnGlobalLayoutListener(object : + ViewTreeObserver.OnGlobalLayoutListener { + /** + * Callback method to be invoked when the global layout state or the visibility of views + * within the view tree changes + */ + @ExperimentalBadgeUtils + override fun onGlobalLayout() { + view.visibility = View.VISIBLE + val badgeDrawable = BadgeDrawable.create(requireActivity()) + badgeDrawable.number = count + badgeDrawable.badgeGravity = badgeGravity + badgeDrawable.badgeTextColor = badgeTextColor + badgeDrawable.backgroundColor = backgroundColor + BadgeUtils.attachBadgeDrawable(badgeDrawable, view) + view.getViewTreeObserver().removeOnGlobalLayoutListener(this) + } + }) + } + + /** + * to hide progressbar + */ + private fun hideProgressBar(achievements: Achievements) { + if (binding.progressBar != null) { + levelInfo = from( + achievements.imagesUploaded, + achievements.uniqueUsedImages, + achievements.notRevertPercentage + ) + inflateAchievements(achievements) + setUploadProgress(achievements.imagesUploaded) + setImageRevertPercentage(achievements.notRevertPercentage) + binding.progressBar.visibility = View.GONE + } + } + + fun showUploadInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.images_uploaded), + resources.getString(R.string.images_uploaded_explanation), + IMAGES_UPLOADED_URL + ) + } + + fun showRevertedInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.image_reverts), + resources.getString(R.string.images_reverted_explanation), + IMAGES_REVERT_URL + ) + } + + fun showUsedByWikiInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.images_used_by_wiki), + resources.getString(R.string.images_used_explanation), + IMAGES_USED_URL + ) + } + + fun showImagesViaNearbyInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.statistics_wikidata_edits), + resources.getString(R.string.images_via_nearby_explanation), + IMAGES_NEARBY_PLACES_URL + ) + } + + fun showFeaturedImagesInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.statistics_featured), + resources.getString(R.string.images_featured_explanation), + IMAGES_FEATURED_URL + ) + } + + fun showThanksReceivedInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.statistics_thanks), + resources.getString(R.string.thanks_received_explanation), + THANKS_URL + ) + } + + fun showQualityImagesInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.statistics_quality), + resources.getString(R.string.quality_images_info), + QUALITY_IMAGE_URL + ) + } + + /** + * takes title and message as input to display alerts + * @param title + * @param message + */ + private fun launchAlert(title: String, message: String) { + showAlertDialog( + requireActivity(), + title, + message, + getString(R.string.ok), + {}, + true + ) + } + + /** + * Launch Alert with a READ MORE button and clicking it open a custom webpage + */ + private fun launchAlertWithHelpLink(title: String, message: String, helpLinkUrl: String) { + showAlertDialog( + requireActivity(), + title, + message, + getString(R.string.ok), + getString(R.string.read_help_link), + {}, + { Utils.handleWebUrl(requireContext(), Uri.parse(helpLinkUrl)) }, + null, + true + ) + } + /** + * check to ensure that user is logged in + * @return + */ + private fun checkAccount(): Boolean { + val currentAccount = sessionManager.currentAccount + if (currentAccount == null) { + Timber.d("Current account is null") + showLongToast(requireActivity(), resources.getString(R.string.user_not_logged_in)) + sessionManager.forceLogin(activity) + return false + } + return true + } + + + + companion object{ + private const val BADGE_IMAGE_WIDTH_RATIO = 0.4 + private const val BADGE_IMAGE_HEIGHT_RATIO = 0.3 + + /** + * Help link URLs + */ + private const val IMAGES_UPLOADED_URL = "https://commons.wikimedia.org/wiki/Commons:Project_scope" + private const val IMAGES_REVERT_URL = "https://commons.wikimedia.org/wiki/Commons:Deletion_policy#Reasons_for_deletion" + private const val IMAGES_USED_URL = "https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images" + private const val IMAGES_NEARBY_PLACES_URL = "https://www.wikidata.org/wiki/Property:P18" + private const val IMAGES_FEATURED_URL = "https://commons.wikimedia.org/wiki/Commons:Featured_pictures" + private const val QUALITY_IMAGE_URL = "https://commons.wikimedia.org/wiki/Commons:Quality_images" + private const val THANKS_URL = "https://www.mediawiki.org/wiki/Extension:Thanks" + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_achievements.xml b/app/src/main/res/layout/fragment_achievements.xml index e0dddcf5b..00c18b323 100644 --- a/app/src/main/res/layout/fragment_achievements.xml +++ b/app/src/main/res/layout/fragment_achievements.xml @@ -1,640 +1,368 @@ - + android:background="?attr/achievementBackground" + android:fillViewport="true" + tools:ignore="ContentDescription" > - + + + + + + + + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/activity_margin_horizontal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/tv_achievements_of_user" + app:srcCompat="@drawable/ic_info_outline_24dp" + app:tint="@color/black" + tools:ignore="ContentDescription" /> - + - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_centerInParent="true" + android:progressDrawable="@android:drawable/progress_horizontal" + android:progressBackgroundTintMode="multiply" + android:progressTint="#5ce65c" + tools:progress="50" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 187c5fc96..fa2173699 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -371,11 +371,13 @@ Delete Achievements Profile + Badges Statistics Thanks Received Featured Images Images via \"Nearby Places\" - Level + Level %d + %s (Level %s) Images Uploaded Images Not Reverted Images Used diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 94856e4eb..67b5eae0f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,6 +1,6 @@ - -