From c06756b89d7878d58562fcc7d2614b8cd2685ad4 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 7 Jun 2021 14:45:46 +0200 Subject: [PATCH 01/78] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 3 +++ app/src/main/res/values-ia/strings.xml | 2 ++ app/src/main/res/values-iw/strings.xml | 1 + app/src/main/res/values-jv/strings.xml | 5 +++-- app/src/main/res/values-kn/strings.xml | 15 ++++++++++++++- app/src/main/res/values-mk/strings.xml | 2 ++ app/src/main/res/values-pms/strings.xml | 2 ++ app/src/main/res/values-pt-rBR/strings.xml | 2 ++ app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sk/strings.xml | 2 ++ app/src/main/res/values-tr/strings.xml | 2 ++ 12 files changed, 35 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d09c93a39..a403f5853 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -246,6 +246,7 @@ Dieses Bild wurde an einem anderen Ort aufgenommen. Lade bitte nur Bilder hoch, die du selber erstellt hast. Lade keine Bilder hoch, die du auf Facebook-Konten anderer Personen gefunden hast. Möchtest du immer noch dieses Bild hochladen? + Verbindungsfehler Im Bild gefundene Probleme Lade bitte nur Bilder hoch, die du selber aufgenommen hast. Lade keine Bilder hoch, die du aus dem Internet heruntergeladen hast. Speichern von In-App-Aufnahmen diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 24c42ef2e..436fc07d4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -10,6 +10,7 @@ * Gomoko * Happy13241 * Jean-Frédéric +* JenyxGym * KATRINE1992 * Melissadeba95 * Metroitendo @@ -252,6 +253,8 @@ Cette image a été prise à un emplacement différent. Veuillez ne téléverser que des images que vous avez prises vous-même. Ne téléversez aucune image prise sur les comptes Facebook d’autres personnes. Voulez-vous tout de même téléverser cette image ? + Erreur de connexion + Le processus de téléchargement nécessite un accès internet\n actif. Veuillez vérifier votre connexion internet. Problèmes trouvés dans l’image Veuillez ne téléverser que des images que vous avez prises vous-même. Ne téléversez aucune image que vous avez téléchargée depuis Internet. Enregistrer les prises de vue dans l’application diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml index ea9ce67f5..c8b6c0fa1 100644 --- a/app/src/main/res/values-ia/strings.xml +++ b/app/src/main/res/values-ia/strings.xml @@ -35,6 +35,8 @@ Mi incargamentos Divider Permission necessari: Scriber sur immagazinage externe. Le app non pote acceder a tu camera/galeria sin isto. + Error de connexion + Le processo de incargamento require un accesso a internet active. Per favor verifica tu connexion al rete. Salveguardar photos prendite in app Salveguardar le photos prendite con le camera del application sur le immagazinage de tu apparato \"A proximitate\" poterea non functionar, perque le position geographic non es disponibile. diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index aa6427ab2..7e7f3be90 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -257,6 +257,7 @@ התמונה צולמה במקום אחר. נא להעלות רק תמונות שהעלית בעצמך. לא להעלות תמונות שמצאת בחשבונות של כל מיני אנשים בפייסבוק. האם ברצונך עדיין להעלות את התמונה הזאת? + שגיאת חיבור נמצאו בעיות בתמונה נא להעלות רק תמונות שצילמת בעצמך. לא להעלות תמונות שהורדת מהאינטרנט. שמירת צילומים מתוך היישומון diff --git a/app/src/main/res/values-jv/strings.xml b/app/src/main/res/values-jv/strings.xml index 64ca4e9da..de8bfdf02 100644 --- a/app/src/main/res/values-jv/strings.xml +++ b/app/src/main/res/values-jv/strings.xml @@ -1,5 +1,6 @@ + + Commons kantun usak + Mimih. Wénten sané iwang! + Bangyang iraga nawang napi sané ragané margiang, lantas wedar saking rerepél majeng iraga. Pacang ngawantu iraga ngabecikin! + Matur suksma! + diff --git a/app/src/main/res/values-ban/strings.xml b/app/src/main/res/values-ban/strings.xml index b8cc3ea17..a427f3f6c 100644 --- a/app/src/main/res/values-ban/strings.xml +++ b/app/src/main/res/values-ban/strings.xml @@ -87,6 +87,8 @@ Lis (Durung kaunggah) Nénten wénten kategori sané patut sareng %1$s + %1$s ten ngelah kelas turunan + %1$s ten ngelah kelas rerama Kategori Setélan Daptar @@ -94,9 +96,13 @@ Kategori Ulasan Peer Indik + Kardi <a href=\"%1$s\">isu GitHub</a> anyar antuk parihindik miwah panikayan kakutu. Parikrama paragaan Krédit Indik + Kirim umpan walik (liwat Rerepél) + Kategori sané mangkin kaanggén + Ngantosang sinkronisasi kapertama… Indayang malih Wangdé Unduh @@ -110,8 +116,17 @@ CC BY 3.0 CC BY-SA 4.0 CC BY 4.0 + Objék palemahan (sekar, baburon, gunung) + Objék mawiguna (sepeda, stasiun sepur) + Jadma kasub (bupati ragané, atlét Olimpiade sané ragané tepukin) Durus SAMPUNANG ngunggah: + Potrék sélfi utawi potrék timpal ragané + Gambar sané kaunduh saking internét + Tangkepan layar satunggil aplikasi Conto unggahan: + Murda: Wangunan Opera Sydney + Pidarta: Wangunan Opera Sydney kacingak saking sebrang celuk + Kategori: Wangunan Opera Sydney saking kauh, pacingakan Wangunan Opera Sydney Napiké Ida ngartos? Inggih! Pidarta lianan @@ -119,6 +134,10 @@ Ngamuat… Nénten wénten kapilih Tanpa sasirah + Tanpa pidarta + Tanpa pabligbagan + Lisénsi nénten kauningin + Segerang OK Paingetan Katemuin Gambar Kaduplikat @@ -153,16 +172,35 @@ Setélan Umpan walik Medal log + Panuntun + Turéksa + Kaca berkas Commons Suratan Wikipédia + Gambar bes peteng. + Gambar burem. + Gambar sampun wénten ring Commons + Gamble kaambil ring genah tiosan. + Durus wantah unggaj gambar sané ragané ambil ngaraga. Sampunang unggah gambar sané ragané temuin ring akun Facebook jadma liyanan. + Napiké ragané kantunjagi ngunggah gambar puniki? + Galat sambungan + Prosés pangunggahan perlu aksés internét urip. Durus turéksa sambungan jaringan ragané. + Pikobet katemuin ring gambar + Manjing log nuju akun ragané Kirim berkas log Liwatin Manjing log + Paarah Wikidata Wikipédia + Commons + Rating iraga + Liwatin Tutorial Pangalih basa Basa + Kamargiang Wangdé Indayang malih + Gambar ten katemu! Kaunggah olih: %1$s Gambar rahina mangkin Gambar rahina mangkin @@ -174,24 +212,65 @@ Média Kategori Kaunggah saking sélulér + Dadosang wallpaper + Wallpaper sampun kapasang! + Napiké gambar puniki OK antuk kaunggah? + Pitakén + Asil Lanturang Cawisan sané becik + Pisaur Iwang + Napiké Ida jagi ngaresikin lelintihan parerehan Ida? + Napiké ragané jagi ngusap parerehan puniki? + Lelintihan parerehan kausap + Usulan Pangusapan Usap + Panghargaan + Profil Statistik Haturan Suksma Katampi + Gambar Pilihan Tingkat Gambar Kaupload + Gambar Kaanggén Pituut Nampek Lis + Langkat %1$d dari %2$d: %3$s + Selanturné + Sadurungné + Kumpulang Gambar Genah Puput + Kirim Suksma majeng %1$s + Gambar salanturnyané + Nggih, ngujang ten + Cingak wacén + Cingak durung kawacén + Jantos dumun… + Katurun + Piranti lunak Pangunggahan Kawangdé %1$s kaunggah olih: %2$s Logo MÉDIA + Pangaturan + Muat luwih akéh + Patunjuk + Senengan + Ngawarsa + Ngawuku + Makejang kala Unggah + Nampek + Kaanggén + Paringkat Titiang + Télémétri Mapbox + Kualitas Gambar Ngalanturang unggahan... Lisénsi Média + Ring samian basa + Pilihin genah + Pilih Genah diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 11c3ee618..e99236569 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -616,6 +616,6 @@ Descendre et zoomer pour ajuster Sortir du sélecteur d’emplacement Sélectionner un emplacement - Affichage de l’image du sélecteur d’emplacement - Ombre de l’affichage de l’image du sélecteur d’emplacement + L’affichage d’image du sélecteur d’emplacement + L’ombre de l’affichage d’image du sélecteur d’événement diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f69794ad4..821c9035c 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1,5 +1,6 @@ @@ -62,4 +63,6 @@ Categoriae Depromens… Nulla selecta + Mutare locus + Locus imaginis diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 73b93383b..c3be6ee2f 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -594,6 +594,10 @@ Panorera och zooma för att justera Avsluta platsväljaren Välj plats + Visa i kartappen + Redigera plats Platsväljarens bildvisare Skuggan för platsväljarens bildvisare + Bildplats + Kontrollera om platsen är korrekt diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ceb1e4d06..01676ca8d 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -4,6 +4,7 @@ * Alexander Yukal * Andriykopanytsia * Base +* DDPAT * Movses * Mykola Swarnyk * Piramidion @@ -148,7 +149,7 @@ Усталена ліцензія Використати попередні назву й опис Тема - Attribution-ShareAlike 4.0 + Attribution-ShareAlike 4.0 Attribution 4.0 Attribution-ShareAlike 3.0 CC Attribution 3.0 @@ -193,7 +194,7 @@ Гаразд Попередження Виявлено дублікат зображення - Вивантажити + Завантажити Так Ні Підпис @@ -207,7 +208,7 @@ Координати Не передбачено Станьте бета-тестером - Підпишіться на наш бета-канал на Google Play і отримайте ранній доступ до нових функцій та виправлень багів + Підпишіться на наш бета-канал на Google Play і отримайте ранній доступ до нових функцій та виправлень баґів Код 2FA Ви справді хочете вийти із системи? Логотип Вікісховища @@ -234,7 +235,7 @@ Налаштування Зворотний зв\'язок Вийти - Посібник + Керівництво Сповіщення Перевірка опис не знайдено @@ -269,8 +270,8 @@ Ви дійсно бажаєте пропустити автентифікацію? Вам треба буде увійти в систему для завантаження зображень у майбутньому. Увійдіть, щоб використати цю функцію - Скопіювати вікітекст у буфер обміну - Вікітекст скопійовано у буфер обміну + Скопіювати вікі-текст у буфер обміну + Вікі-текст скопійовано у буфер обміну Функція «Поблизу» може працювати некоректно, «Розташування» недоступне. Потрібний дозвіл для показу списку місць поблизу Напрямки @@ -321,10 +322,10 @@ Чи можна завантажувати це зображення? Запитання Результат - Якщо ви продовжите завантажувати зображення, які підлягають вилученню, ваш обліковий запис можуть забанити. Ви впевнені, що хочете закінчити тест? - Понад %1$s завантажених вами зображень було вилучено. Якщо ви продовжите завантажувати зображення, які підлягають вилучення, ваш обліковий запис можуть забанити. \n\nХочете переглянути посібник ще раз і тоді пройти тест, що допоможе дізнатися, які зображення можна, а які не можна завантажувати? - Селфі не мають значної енциклопедичної цінності. Будь ласка, не завантажуйте фото самих себе, якщо про вас нема статті у Вікіпедії. - Зображення пам\'яток і пейзажів можна завантажувати у більшості країн. Зважте, будь ласка, що сучасні мистецькі інсталяції на вулиці часто захищені авторськими правами і їх НЕ можна завантажувати. + Якщо ви продовжите завантажувати зображення, які підлягають вилученню, ваш обліковий запис можуть заблокувати. Ви впевнені, що хочете закінчити тест? + Понад %1$s завантажених вами зображень було вилучено. Якщо ви продовжите завантажувати зображення, які підлягають вилучення, ваш обліковий запис можуть заблокувати. \n\nХочете переглянути посібник ще раз і тоді пройти тест, що допоможе дізнатися, які зображення можна, а які не можна завантажувати? + Селфі не мають значної енциклопедичної цінності. Будь ласка, не завантажуйте фото самих себе, якщо про вас немає статті у Вікіпедії. + Зображення пам\'яток і пейзажів можна завантажувати у більшості країн. Зауважте, будь ласка, що сучасні мистецькі інсталяції на вулиці часто захищені авторськими правами і їх НЕ можна завантажувати. Знімки екрану веб-сайтів вважаються похідними роботами і є таким же об\'єктом авторських прав, як і сам веб-сайт. Їх можна використовувати з дозволу авторів сайту. Без такого дозволу, будь-який твір, який ви створите на основі їхньої роботи, юридично вважається неліцензованою копією, яка належить оригінальному автору. Одна з цілей Вікісховища — зібрати якісні зображення. Тому розмиті зображення завантажувати не треба. Завжди старайтеся зробити гарні знімки при хорошому освітленні. Зображення технологій чи культури дуже бажані для Вікісховища. @@ -335,7 +336,7 @@ Продовжити Правильна відповідь Хибна відповідь - Чи можна завантажувати цей скріншот? + Чи можна завантажувати цей знімок? Поширити програму Помилка отримання місць поблизу. Історія пошуку порожня @@ -360,7 +361,7 @@ Кількість зображень, які ви завантажили у Вікісховище будь-яким методом Відсоток завантажених вами у Вікісховище зображень, що не були вилучені Кількість завантажених вами у Вікісховище зображень, що використані у статтях Вікімедіа - Відбулася помилка! + Сталася помилка! Сповіщення Вікісховища Використати альтернативне ім\'я автора При завантаженні фото використовувати альтернативне ім\'я для зазначення авторства замість власного імені користувача @@ -411,7 +412,7 @@ Ви зробили так багато, що наша система підрахунку досягнень не може впоратись зі своїм завданням. Це — абсолютне досягнення. Завершується: Показати кампанії - Чинні кампанії + Чинні компанії Ви більше не бачитимете кампаній. Однак Ви можете увімкнути це сповіщення повторно в своїх налаштуваннях, якщо забажаєте. Ця функція вимагає доступу до інтернету. Будь ласка, перевірте своє з\'єднання. Сталася помилка при обробці зображення. Будь ласка, спробуйте ще раз! @@ -456,7 +457,7 @@ Сталася помилка при завантаженні зображень Будь ласка, зачекайте… Вибрані зображення — це зображення від вправних фотографів та ілюстраторів, які спільнота Вікісховища визначила як такі, що мають найкращу якість на сайті. - Зображення, завантажені через «Поблизу», — це зображення, завантажені через дослідження місць на карті. + Зображення, завантажені через «Поблизу», — це зображення, завантажені через дослідження місць на мапі. Ця функція дозволяє редакторам надіслати «дякую» користувачам, які роблять корисні редагування, — скориставшись невеличким посиланням на сторінці історії або порівняння версій. Копіювати до наступних медіафайлів Скопійовано @@ -513,7 +514,7 @@ Не вдалося додати координати. Не вдалося отримати координати. Поширити зображення через - Поки що від вас немає вкладу + Ви ще не зробили жодного внеску Обліковий запис створено! Текст скопійовано до буферу обміну Сповіщення позначено прочитаним @@ -553,12 +554,12 @@ Ви хочете додати це зображення у статтю Вікіпедії мовою %1$s? Підтвердити Інструкції - 1. Використовуйте такий вікітекст: + 1. Використовуйте такий вікі-текст: 2. Натискання на «Підтвердити» відкриє статтю Вікіпедії 3. Знайдіть розділ статті, до якого пасуватиме ваше зображення 4. Натисніть на іконку «Редагувати» (у вигляді олівця) біля цього розділу. - 5. Вставте вікітекст у підхожому місці. - 6. Відредагуйте вікітекст за потреби, вказавши потрібне розміщення. Детальнішу інформацію знайдете <a href=\"https://uk.wikipedia.org/wiki/Довідка:Розширений_синтаксис_зображень\">тут</a>. + 5. Вставте вікі-текст у відповідне місце. + 6. Відредагуйте вікі-текст за потреби, вказавши потрібне розміщення. Детальну інформацію знайдете <a href=\"https://uk.wikipedia.org/wiki/Довідка:Розширений_синтаксис_зображень\">тут</a>. 7. Опублікуйте статтю Скопіювати вікікод у буфер обміну пауза @@ -587,16 +588,16 @@ Мій ранг Телеметрія Mapbox Надсилати анонімізоване розташування та дані щодо користування до Mapbox при використанні функції Поблизу - &#169; <a href=\"https://www.mapbox.com/about/maps/\">Mapbox</a> &#169; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> <a href=\"https://www.mapbox.com/map-feedback/\">Покращити цю карту</a> + &#169; <a href=\"https://www.mapbox.com/about/maps/\">Mapbox</a> &#169; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> <a href=\"https://www.mapbox.com/map-feedback/\">Покращити цю мапу</a> Увімкнено режим обмеженого з\'єднання! Вимкнено режим обмеженого з\'єднання. Завантаження в процесі очікування тепер відновляться. Режим обмеженого з\'єднання Якісні зображення - Якісні зображення — це діаграми чи фотографії, що відповідають певним стандартам якості (переважно технічним за природою) та цінні для проектів Вікімедіа + Якісні зображення — це діаграми чи фотографії, що відповідають певним стандартам якості (переважно технічним за природою) та цінні для проєктів Вікімедіа Продовження завантаження… Призупинення завантаження… - Ви ввімкнули режим обмеженого з\'єднання. Усі завантаження призупинено та буде відновлено коли Ви вимкнете цей режим. - Режим обмеженого з\'єднання ввімкнено. + Ви увімкнули режим обмеженого з\'єднання. Усі завантаження призупинено та буде відновлено коли Ви вимкнете цей режим. + Режим обмеженого з\'єднання увімкнено. Будь ласка, напишіть короткий опис, що розповідає що представлено на зображенні. В описі розкажіть що робить це зображення цікавим, типовим або рідкісним, опишіть контекст — видимий чи ні. Старайтесь максимально використовувати точну термінологію. Будь ласка, знайдіть та оберіть всі концепти, що це зображення показує. Будьте настільки точними, наскільки можливо. Якщо зображення зображує декілька речей, у межах здорового глузду оберіть їх всі. Не обирайте загальніші теґи, коли є доступні точніші. Будь ласка, оберіть відповідні категорії. На відміну від описів, назви категорій лише англійською. @@ -614,6 +615,10 @@ Панорамуйте і збільшуйте, щоб підлаштувати Вийти з вікна вибору розташування Вибрати розташування + Показати в додатку на мапі + Редагувати підпис Вигляд вибору розташування як зображення Вигляд вибору розташування як тіні зображення + Підпис зображення + Перевірте правильність розташування From 2233fc2e98f4c35a7d406de20a3fc2af62d00864 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 8 Jul 2021 13:01:13 +0200 Subject: [PATCH 41/78] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-es/strings.xml | 9 +++++++++ app/src/main/res/values-ko/strings.xml | 3 +++ 2 files changed, 12 insertions(+) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 22aa501ab..2b06ead76 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -13,6 +13,7 @@ * Hasley * Ihojose * JO777 +* Jackiezelaya * Jduranboger * Jelou * Johnny243 @@ -253,6 +254,7 @@ Esta imagen fue tomada en una ubicación diferente. Por favor sube solo fotografías que tu mismo hayas tomado. No subas imágenes o fotografías que hayas encontrado en las cuentas de Facebook de otros. ¿Todavía quieres cargar esta imagen? + El cargar el proceso requiere acceso de internet activo. Complacer comprobar vuestra conexión de red. Problemas encontrados en la imagen Carga únicamente fotografías que hayas creado tú. No cargues imágenes o fotografías que hayas descargado de Internet. Guardar tomas en la aplicación @@ -540,8 +542,15 @@ Se activó el modo de conexión limitada. Se desactivó el modo de conexión limitada. Las cargas pendientes se reanudarán ahora. Conexión limitada + En más idiomas Escoge una ubicación Haz una panorámica y acércate para ajustar Salir del seleccionador de ubicación Seleccionar ubicación + Mostrar en la aplicación de mapa + Editar ubicación + La vista de imagen de la ubicación picker + La sombra de la vista de imagen de la ubicación picker + Posición de imagen + Compruebe si la ubicación es correcta diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 821c9035c..b5f14eac8 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -456,7 +456,9 @@ 성공 분류를 추가하지 못했습니다. 분류 업데이트 + 좌표 업데이트 성공 + 좌표를 가져올 수 없습니다. 다음을 통해 이미지 공유 아직 기여가 없습니다 계정을 만들었습니다! @@ -519,6 +521,7 @@ 앱 사용자 인터페이스 언어 캡션과 설명을 제거합니다 더 읽어보기 + 모든 언어 장소 선택하기 장소 선택 From a09da2d59e54413b93435268b4f8036f362799ed Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 12 Jul 2021 13:01:15 +0200 Subject: [PATCH 42/78] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-bg/strings.xml | 3 +++ app/src/main/res/values-ru/strings.xml | 2 ++ app/src/main/res/values-tr/strings.xml | 5 +++++ app/src/main/res/values-xmf/strings.xml | 5 +++-- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 4f9b0bda2..8b42615f6 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -236,7 +236,10 @@ Това място вече не съществува. Не са открити изображения! Търсене + Търсене в Общомедия Търсене + Скорошни търсения: + Скорошни заявки за търсене Грешка при зареждането на категориите. Грешка при зареждането на описанията. Мултимедия diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 06bc48994..618d3f3cc 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -633,4 +633,6 @@ Панорамируйте и масштабируйте для настройки Выйти из окна выбора местоположения Выберите местоположение + Показать в приложении карты + Проверьте правильность местоположения diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 152f216cd..e148e5dfd 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -604,10 +604,15 @@ Uygulama kullanıcı arayüzü dili Bir başlığı ve açıklamayı kaldırır Devamını oku + Tüm dillerde Bir konum seçin Ayarlamak için kaydırma ve yakınlaştırma Konum seçiciden çık Konum seçin + Harita uygulamasında göster + Konumu düzenle Konum seçicinin resim görünümü Konum seçicinin resim görünümünün gölgesi + Görüntü Konumu + Konumun doğru olup olmadığını kontrol edin diff --git a/app/src/main/res/values-xmf/strings.xml b/app/src/main/res/values-xmf/strings.xml index 5abd96563..4f3d8bbd9 100644 --- a/app/src/main/res/values-xmf/strings.xml +++ b/app/src/main/res/values-xmf/strings.xml @@ -32,7 +32,7 @@ მიოჯინი თარი წიმოხრსხუ - კონფიდენციალურობა + კონფიდენციალურალა ვიკიოწკარუე პარამეტრეფი ვიკიოწკარუეშა ეხარგუა @@ -69,6 +69,7 @@ გობჟინაფა ფაილიშ ხასჷლაშ ძირაფა მუკნაჭარა (უციო) + ქორთხინთ, ქემიოწურეთ თე ფაილიშ ეჭარუა ეჭარუა მუკნაჭარა მიშულაქ ვემიხუჯინუ - რშვილიშ ჩილათა @@ -98,7 +99,7 @@ <a href=\"https://github.com/commons-app/apps-android-commons\">წყუ</a> და <a href=\"https://commons-app.gოthub.io/\">ვებ-ხასჷლა</a> GitHub-ის. ჩილათაშ ოგინაფალო ვარ-და ზიტყვასქვილშო გაჭყით ახალი <a href=\"%1$s\">მოთხირი GitHub-ის</a>. კონფიდენციალურალაშ პოლიტიკა <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">მარდეფი</a> - პრგრამაშ გეშა + პროგრამაშ გეშა წჷმიხონარეფიშ ჯღონუა (ელ.ფოსტათ) ელ-ფოსტაშ კლიენტი ვა რე გერინაფილი ასერდე გჷმორინაფილი კატეგორიეფი From 8793d45f303d41bc0e75dbd802202dfceb9cd7c9 Mon Sep 17 00:00:00 2001 From: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Date: Wed, 14 Jul 2021 11:49:43 +0530 Subject: [PATCH 43/78] Remove duplicate code which was added during merge (#4504) --- .../java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index ce6585b52..52f8b7385 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -102,7 +102,6 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple pager.addOnPageChangeListener(this); adapter = new MediaDetailAdapter(getChildFragmentManager()); - ((BaseActivity)getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true); if (getActivity() != null) { final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); From ec89a3377a0aceef5f5a4aa20efcc63f44b52066 Mon Sep 17 00:00:00 2001 From: neslihanturan Date: Wed, 14 Jul 2021 18:05:04 +0300 Subject: [PATCH 44/78] Add an else case where crash logs are null means there is no crash (#4506) --- app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java | 4 ++++ 1 file changed, 4 insertions(+) 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 index 9490d9bcf..68f7bd78c 100644 --- a/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java +++ b/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java @@ -1,5 +1,7 @@ package fr.free.nrw.commons.logging; +import static org.acra.ACRA.getErrorReporter; + import android.content.Context; import android.content.Intent; import android.net.Uri; @@ -62,6 +64,8 @@ public abstract class LogsSender implements ReportSender { final Uri logFileUri = getZippedLogFileUri(context, report); if (logFileUri != null) { sendEmail(context, logFileUri); + } else { + getErrorReporter().handleSilentException(null); } } From 9643d8d0b87f63d232ed3d8f77d631a5737c001b Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 15 Jul 2021 13:01:22 +0200 Subject: [PATCH 45/78] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ja/strings.xml | 5 + app/src/main/res/values-nl/strings.xml | 336 ++++++++++++++++++++++++- 2 files changed, 332 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 5c0616a4f..a7e5e5e90 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -9,6 +9,7 @@ * Hinaloe * Kkairri * LaMagiaaa +* MathXplore * Nicolas Raoul * Omotecho * Otokoume @@ -583,4 +584,8 @@ メディアのライセンス メディアの詳細 カテゴリのページを表示 + すべての言語 + 地図アプリに表示 + 位置を編集 + 場所が正しいかどうかを確認する diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 2a44daab2..1350c3792 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -13,6 +13,7 @@ * Optilete * Patio * Robin van der Linde +* Romaine * Rots61 * Siebrand * Sjoerddebruin @@ -24,20 +25,31 @@ %1$d bestand aan het uploaden %1$d bestanden aan het uploaden - - \@string/contributions_subtitle_zero - %1$d upload - %1$d uploads + + (%1$d) + (%1$d) - + Uploaden starten + Bezig met 1 upload Bezig met %1$d uploads - + %1$d upload %1$d uploads - Deze afbeelding wordt gelicenseerd onder %1$s + + Deze afbeelding wordt gelicenseerd onder %1$s + Deze afbeeldingen worden gelicenseerd onder %1$s + + + %1$d upload + %1$d uploads + + + Gedeelde inhoud wordt ontvangen. Het verwerken van de afbeelding kan enige tijd duren, afhankelijk van de grootte van de afbeelding en uw apparaat + Gedeelde inhoud wordt ontvangen. Het verwerken van de afbeeldingen kan enige tijd duren, afhankelijk van de grootte van de afbeeldingen en uw apparaat + Verkennen Uiterlijk Algemeen @@ -60,6 +72,7 @@ Bestand niet gevonden. Probeer een ander bestand. Verifiëring is mislukt. Meld je opnieuw aan. Uploaden is begonnen + Uploaden in wachtrij (beperkte verbindingsmodus ingeschakeld) %1$s geüpload Wijs aan om uw upload te bekijken Bestand wordt geüpload: %s @@ -81,31 +94,38 @@ Delen Bestandspagina bekijken Titel (verplicht) + Geef een bijschrift op voor dit bestand Beschrijving + Bijschrift Aanmelden niet mogelijk. Er is een probleem met het netwerk U hebt te vaak geprobeerd aan te melden. Probeer het over een aantal minuten opnieuw. Deze gebruiker is helaas geblokkeerd op Wikimedia Commons + U moet uw tweefactorauthenticatiecode opgeven. Aanmelden mislukt Uploaden Geef deze verzameling een naam Wijzigingen Uploaden Categorieën zoeken + Zoek naar items die uw media weergeven (berg, Taj Mahal, enz.) Opslaan Vernieuwen Lijst (Nog geen uploads) Er zijn geen categorieën met \"%1$s\" gevonden - Voeg categorieën toe om uw afbeeldingen makkelijker te vinden te maken op Wikimedia Commons.\n\nBegin met het toevoegen van categorieën.\n\nKlik op dit bericht, of ga terug, om deze stap over te slaan. + Geen Wikidata-items gevonden die overeenkomen met %1$s + Voeg categorieën toe om uw afbeeldingen makkelijker te vinden te maken op Wikimedia Commons.\nBegin met typen om categorieën toe te voegen. Categorieën Instellingen Registreren Uitgelichte afbeelding Categorie + Peer review Over - Opensourcesoftware vrijgegeven onder de <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache License v2</a>. Wikimedia Commons en haar logo zijn handelsmerken van de Wikimedia Foundation en worden gebruikt met toestemming van de Wikimedia Foundation. We worden niet bekrachtigd door en zijn niet verbonden met de Wikimedia Foundation. + De Wikimedia Commons-app is een open-source-app die is gemaakt en onderhouden door begunstigden en vrijwilligers van de Wikimedia-gemeenschap. De Wikimedia Foundation is niet betrokken bij het maken, ontwikkelen of onderhouden van de app. <a href=\"https://github.com/commons-app/apps-android-commons\">Broncode</a> en <a href=\"https://commons-app.github.io/\">website</a> op GitHub. Maak een nieuwe <a href=\"%1$s\">melding op GitHub</a> voor bugs en suggesties. Privacybeleid + Credits Over Terugkoppeling geven (via e-mail) Geen e-mail-client geïnstalleerd @@ -114,6 +134,7 @@ U hebt nog geen afbeeldingen geüpload. Opnieuw proberen Annuleren + Door deze foto in te dienen, verklaar ik dat dit mijn eigen werk is, dat het geen auteursrechtelijk beschermd materiaal of selfies bevat en dat het verder voldoet aan het <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">Wikimedia Commons-beleid</a>. Downloaden Standaard licentie Gebruik vorige titel en omschrijving @@ -131,10 +152,16 @@ Uw afbeeldingen helpen mensen over de hele wereld met kennis opdoen! Upload alstublieft alleen afbeeldingen die volledig door uzelf gemaakt zijn: Natuurlijke voorwerpen (bloemen, dieren, bergen) + Nuttige objecten (fietsen, treinstations) + Bekende mensen (uw burgemeester, olympische atleten die u ontmoet heeft) Gelieve NIET te uploaden: + Selfies of foto\'s van uw vrienden + Foto\'s die u van internet heeft gedownload Schermfoto\'s van oorspronkelijke toepassingen Voorbeeld van een upload: Titel: Opera van Sydney + Beschrijving: Sydney Opera House gezien vanaf de overkant van de baai + Categorieën: Sydney Opera House from the west, Sydney Opera House remote views Draag uw afbeelding bij. Help pagina\'s in Wikipedia tot leven te laten komen! Afbeeldingen op Wikipedia komen van Wikimedia Commons. Uw afbeeldingen helpen mensen van over de hele wereld te leren. @@ -145,16 +172,24 @@ Categorieën Laden... Geen selectie + Geen onderschrift Geen beschrijving Geen overleg Onbekende licentie Vernieuwen + Opslagtoestemming aanvragen Benodigde toestemming: Lees externe opslag. Zonder toestemming kan de app uw foto galerij niet bekijken. + Vereiste toestemming: externe opslag schrijven. Zonder dit heeft de app geen toegang tot uw camera/galerij. + Locatietoestemming aanvragen OK Waarschuwing + Dubbele afbeelding gevonden + Uploaden Ja Nee + Onderschrift Titel + Beschrijvingen Beschrijving Overleg Auteur @@ -163,12 +198,23 @@ Coördinaten Niet opgegeven Word een bèta tester + Meld u aan voor ons bètakanaal op Google Play en krijg vroege toegang tot nieuwe functies en bugfixes 2FA-code + Wilt u echt uitloggen? Commons logo Commons website Commons Facebookpagina + Commons Github-broncode + Media-afbeelding is mislukt Geen subcategorieën gevonden + Geen bovenliggende categorieën gevonden + Mount Zao + Lama\'s + Rainbow Bridge Tulp + Welkom op Wikipedia + Welkom Auteursrecht + Sydney Opera House Annuleren Openen Sluiten @@ -181,19 +227,29 @@ Afmelden Zelfstudie Meldingen + Beoordeling geen beschrijving gevonden + Commons-bestandspagina Wikidata-item Wikipedia-artikel + Beschrijf de media zo veel mogelijk: Waar is de opname gemaakt? Wat laat het zien? Wat is de context? Beschrijf de objecten of personen. Onthul informatie die niet gemakkelijk te raden is, bijvoorbeeld het tijdstip van de dag als het een landschap is. Als de media iets ongewoons laat zien, leg dan uit wat het ongebruikelijk maakt. + Geef een korte beschrijving van de afbeelding. Het eerste bijschrift zou worden gebruikt als de titel voor de afbeelding. Beperk tot 255 tekens. + Mogelijke problemen met deze afbeelding: Afbeelding is te donker. Afbeelding is onscherp. Afbeelding staat al in Commons. Deze foto is op een andere locatie genomen. Upload alleen foto\'s die je zelf hebt genomen. Upload geen foto\'s die je op Facebookaccounts van anderen hebt gevonden. Wil je deze afbeelding nog steeds uploaden? + Verbindingsfout + Het uploadproces vereist een actieve internettoegang. Controleer uw netwerkverbinding. + Problemen gevonden in afbeelding Upload alleen foto\'s die je zelf hebt genomen. Upload geen foto\'s die je van internet hebt gedownload. In-app opnamen opslaan + Foto\'s die met de in-app-camera zijn gemaakt, opslaan in de opslag van uw apparaat Aanmelden bij je profiel Logboekbestand verzenden + Stuur een logbestand via e-mail naar ontwikkelaars om problemen met de app op te lossen. Opmerking: logboeken kunnen mogelijk identificerende informatie bevatten Geen webbrowser gevonden om URL te openen Fout! URL niet gevonden Nomineer voor Verwijdering @@ -217,6 +273,7 @@ Zelfstudie overslaan Geen internet Fout bij ophalen berichten. + Fout bij ophalen van afbeelding voor beoordeling. Druk op vernieuwen om het opnieuw te proberen. Geen berichten. Vertalen Talen @@ -224,68 +281,329 @@ Ga door Annuleren Opnieuw proberen + Dit zijn de plaatsen bij u in de buurt die foto\'s nodig hebben om bijbehorende Wikipedia-artikelen te illustreren.\n\nHet klikken op \'ZOEK IN DIT GEBIED\' vergrendelt de kaart en start een zoekopdracht in de buurt rond die locatie. + Deze plaats heeft een foto nodig. + Deze plaats heeft al een foto. + Deze plek bestaat niet meer. Geen afbeeldingen gevonden! Er is een fout opgetreden tijdens het laden van de afbeeldingen. Geüpload door: %1$s Geblokkeerd + U bent geblokkeerd voor het bewerken van Commons Afbeelding van de dag Afbeelding van de dag Zoeken Commons doorzoeken Zoeken Recente zoekopdrachten: + Recente zoekopdrachten + Er is een fout opgetreden bij het laden van categorieën. + Er is een fout opgetreden tijdens het laden van de beschrijvingen. Media Categorieën + Items Uitgelicht + Geüpload via mobiel + Afbeelding toegevoegd aan %1$s op Wikidata! + Het bijwerken van corresponderende Wikidata-entiteit is mislukt! Stel in als achtergrond Achtergrond succesvol ingesteld! Vragenspel Kan je afbeelding worden geplaatst? Vraag Uitslag + Als u doorgaat met het uploaden van afbeeldingen die moeten worden verwijderd, wordt uw account waarschijnlijk geblokkeerd. Weet u zeker dat u de quiz wilt beëindigen? + Meer dan %1$s van de afbeeldingen die u heeft geüpload, zijn verwijderd. Als u doorgaat met het uploaden van afbeeldingen die moeten worden verwijderd, wordt uw account waarschijnlijk geblokkeerd.\n\nWilt u de tutorial opnieuw bekijken en vervolgens een quiz doen om u te helpen te leren welk type afbeeldingen u wel of niet moet uploaden? + Selfies hebben niet veel encyclopedische waarde. Upload geen foto van uzelf, tenzij u al een Wikipedia-artikel over uzelf heeft. + Foto\'s van monumenten en landschappen buiten kunnen in de meeste landen worden geüpload. Houd er rekening mee dat tijdelijke kunstinstallaties buiten vaak auteursrechtelijk beschermd zijn en niet OK zijn om te uploaden. + Screenshots van websites worden beschouwd als afgeleide werken en vallen onder het auteursrecht van de website zelf. Deze kunnen gebruikt worden na toestemming van de auteur. Zonder dergelijke toestemming wordt elke kunst die u maakt op basis van hun werk wettelijk beschouwd als een kopie zonder licentie die eigendom is van de oorspronkelijke auteur. + Een van de doelen van Commons is het verzamelen van kwaliteitsafbeeldingen. Daarom mogen wazige afbeeldingen niet worden geüpload. Probeer altijd mooie foto\'s te maken met goede belichting. + Foto\'s met technologie of cultuur zijn van harte welkom op Commons. + U heeft %1$s van de antwoorden goed. Gefeliciteerd! + Selecteer een van de twee opties om de vraag te beantwoorden + Inlogsessie verlopen, log opnieuw in. + Deel je quiz met je vrienden! Doorgaan Correct antwoord Fout antwoord + Is deze screenshot OK om te uploaden? Toepassing delen + Fout bij het ophalen van plaatsen in de buurt. Geen recente zoekopdrachten + Weet u zeker dat u uw zoekgeschiedenis wilt wissen? + Wilt u deze zoekopdracht verwijderen? + Zoekgeschiedenis verwijderd + Nomineer voor verwijdering Verwijderen + Prestaties + Profiel Statistieken + Ontvangen bedankjes + Uitgelichte afbeeldingen + Afbeeldingen via \"Plaatsen in de buurt\" + Niveau + Geüploade afbeeldingen + Afbeeldingen niet teruggedraaid + Gebruikte afbeeldingen + Deel u prestaties met uw vrienden! + Uw niveau neemt toe naarmate u aan deze vereisten voldoet. Items in de sectie \'statistieken\' tellen niet mee voor uw niveau. + minimum vereist: + Het aantal afbeeldingen dat u naar Commons heeft geüpload, via uploadsoftware + Het percentage afbeeldingen dat u naar Commons heeft geüpload en dat niet is verwijderd + Het aantal afbeeldingen dat u naar Commons heeft geüpload en dat in Wikimedia-artikelen is gebruikt Fout opgetreden! + Commons-melding + Gebruik aangepaste auteursnaam + Gebruik een aangepaste auteursnaam in plaats van uw gebruikersnaam tijdens het uploaden van foto\'s + Aangepaste auteursnaam Bijdragen Dichtbij Meldingen Meldingen (gelezen) Meldingen dichtbij weergeven + Toon in-app-melding voor de dichtstbijzijnde plaats die foto\'s nodig heeft Lijst Toestemming om op te slaan + We hebben uw toestemming nodig om toegang te krijgen tot de externe opslag van uw apparaat om afbeeldingen te uploaden. + U zult de dichtstbijzijnde plaats die foto\'s nodig heeft niet meer zien. U kunt deze melding desgewenst echter opnieuw inschakelen in de Instellingen. Stap %1$d van %2$d: %3$s Volgende Vorige Verzenden + Er bestaat een bestand met de bestandsnaam %1$s . Weet u zeker dat u door wilt gaan?\n\nOpmerking: Er wordt automatisch een geschikt achtervoegsel aan de bestandsnaam toegevoegd. + Er is geen compatibele kaarttoepassing gevonden op uw apparaat. Installeer een kaarttoepassing om deze functie te gebruiken. + Afbeeldingen + Locaties + Toevoegen/verwijderen van bladwijzers + Bladwijzers + U hebt geen bladwijzers toegevoegd + Bladwijzers + Logboekverzameling is gestart. Start de app opnieuw op, voer de actie uit die u wilt loggen en tik vervolgens nogmaals op \'Logboekbestand verzenden\' + Ik heb het per ongeluk geüpload + Ik wist niet dat het publiekelijk zichtbaar zou zijn + Ik realiseerde me dat het slecht is voor mijn privacy. + Ik ben van gedachten veranderd, ik wil niet dat het nog publiekelijk zichtbaar is. + Sorry, deze foto is niet interessant voor een encyclopedie Geüpload door mijzelf op %1$s, gebruikt in %2$d artikel(en). + Welkom bij Commons!\n\nUpload je eerste media door op de knop Toevoegen te klikken. + Geen categorieën geselecteerd + Afbeeldingen zonder categorieën zijn zelden bruikbaar. Weet je zeker dat je verder wilt gaan zonder categorieën te selecteren? + Geen beschrijvingen geselecteerd + Afbeeldingen met beschrijvingen zijn gemakkelijker te vinden en zullen eerder worden gebruikt. Weet u zeker dat u wilt doorgaan zonder beschrijvingen te selecteren? + (Voor alle afbeelding(en) in set) + Zoek in dit gebied Machtigingsverzoek + Wilt u dat wij uw huidige locatie gebruiken om de dichtstbijzijnde plaats weer te geven die foto\'s nodig heeft? + Kan zonder locatierechten de dichtstbijzijnde plaats waarvoor foto\'s nodig zijn niet weergeven Niet meer vragen Machtiging voor locatie vragen Naar machtiging voor locatie vragen als dat nodig is voor de functie meldingskaart dichtbij weergeven. + Er is iets misgegaan, we konden uw prestaties niet ophalen + U heeft zoveel bijdragen geleverd dat ons prestatieberekeningssysteem het niet aankan. Dit is de ultieme prestatie. + Eindigt op: + Campagnes weergeven + Bekijk de lopende campagnes + U ziet de campagnes niet meer. U kunt deze melding desgewenst echter opnieuw inschakelen in de Instellingen. + Voor deze functie is een netwerkverbinding vereist, controleer uw verbindingsinstellingen. + Er is een fout opgetreden tijdens het verwerken van de afbeelding. Probeer het opnieuw! + Token ophalen om te bewerken + Sjabloon toevoegen voor categoriecontrole + Categoriecontrole aanvragen voor %1$s + Categoriecontrole wordt aangevraagd + Categoriecontrole aangevraagd + Verzoek om categoriecontrole werkte niet + Categoriecontrole aangevraagd voor %1$s + Kan categoriecontrole niet aanvragen voor %1$s + Categoriecontrole aanvragen voor %1$s + Afgerond + Verzending bedankje is gelukt + Bedankje succesvol verzonden naar %1$s + Verzending bedankje naar %1$s is mislukt + Verzending bedankje is mislukt + Verstuur een bedankje voor %1$s + Voldoet dit aan de regels van het auteursrecht? + Is dit correct gecategoriseerd? + Valt dit binnen de scope? + Wilt u de bijdrager bedanken? + Klik op NEE om deze afbeelding te nomineren voor verwijdering als deze helemaal niet nuttig is. + Logo\'s, screenshots, filmposters zijn vaak auteursrechtschendingen.\nKlik NEE om deze afbeelding te nomineren voor verwijdering + %1$s zal worden aangemoedigd door uw waardering + Oh, dit is niet eens gecategoriseerd! + Deze afbeelding valt onder %1$s categorieën. + Het valt buiten de scope want het is + Het is schending van het auteursrecht omdat het + Volgende afbeelding + Ja, waarom niet + Als u op deze knop klikt, krijgt u nog een recent geüploade afbeelding van Wikimedia Commons + U kunt afbeeldingen bekijken en de kwaliteit van Wikimedia Commons verbeteren.\n De vier beoordelingsparameters zijn: \n - Valt deze afbeelding binnen de scope? \n - Voldoet deze afbeelding aan de regels van het auteursrecht? \n - Is deze afbeelding correct gecategoriseerd? \n - Als alles goed gaat kunt u ook de inzender bedanken. + Geen afbeeldingen gebruikt + Geen afbeeldingen teruggedraaid Geen afbeeldingen geüpload + Je heeft geen ongelezen meldingen U hebt geen gelezen melding Logboeken delen via Bekijk gelezen Ongelezen bekijken + Er is een fout opgetreden bij het kiezen van afbeeldingen Een ogenblik geduld… + Uitgelichte afbeeldingen zijn afbeeldingen van zeer bekwame fotografen en illustratoren op de site die door de Wikimedia Commons-gemeenschap zijn gekozen als de hoogste kwaliteit. + Afbeeldingen die zijn geüpload via Plaatsen in de buurt zijn de afbeeldingen die worden geüpload door plaatsen op de kaart te ontdekken. + Met deze functie kunnen redacteuren een bedankbericht sturen naar gebruikers die nuttige bewerkingen uitvoeren - door een kleine bedanklink op de geschiedenispagina of diff-pagina te gebruiken. + Kopiëren naar volgende media + Gekopieerd + Voorbeelden van goede afbeeldingen om te uploaden naar Commons + Voorbeelden van afbeeldingen die niet mogen worden geüpload + Sla deze afbeelding over + Download mislukt!!. We kunnen het bestand niet downloaden zonder toestemming voor externe opslag. + EXIF-tags beheren + Selecteer welke EXIF-tags in uploads moeten worden behouden Auteur Auteursrechten Locatie Cameramodel + Lensmodel + Serienummers + Software + Upload foto\'s rechtstreeks vanaf uw telefoon naar Wikimedia Commons. Download de Commons-app nu: %1$s + App delen via... + Afbeeldingsinfo + Geen categorieën gevonden + Geen beschrijvingen gevonden + Uploaden geannuleerd + Er zijn geen gegevens voor de titel of beschrijving van de vorige afbeelding + Waarom moet %1$s worden verwijderd? %1$s is geüpload door %2$s + Standaard beschrijvingstaal + Nomineren voor verwijdering + Afgerond + %1$s genomineerd voor verwijdering. + Mislukt + Kan verwijdering niet aanvragen. + Een selfie + Wazig + Onzin + Persfoto + Willekeurige foto van internet Logo + Want het is + Ik probeer categorieën bij te werken. + Categorie-update + Afgerond + + Categorie %1$s is toegevoegd. + Categorieën %1$s zijn toegevoegd. + + Kan geen categorieën toevoegen. + Categorieën bijwerken + Ik probeer de coördinaten bij te werken. + Coördinaten bijwerken + Afgerond + Coördinaten %1$s zijn toegevoegd. + Kan geen coördinaten toevoegen. + Kan coördinaten niet opvragen. Deel afbeelding via + U heeft nog geen bijdragen gedaan Profiel gemaakt! + De tekst is gekopieerd naar het klembord. + Melding gemarkeerd als gelezen + Er is een fout opgetreden! + Plaatsstatus: + Bestaat + Heeft foto nodig + Plaatstype: + Brug, museum, hotel, enz. + Er is iets misgegaan met inloggen, u moet uw wachtwoord opnieuw instellen !! + MEDIA + Plaats in de buurt gevonden + Is dit een foto van plaats %1$s? + Bladwijzers + Instellingen Bladwijzer verwijderd Als bladwijzer toegevoegd + Er is iets fout gegaan. Kan de achtergrond niet instellen + Instellen als achtergrond + Wordt ingesteld als achtergrond. Een ogenblik geduld... + Volgsysteem + Donker + Licht + Kan locatie-instellingen niet openen. Schakel locatie handmatig in + Kies voor de beste resultaten de modus van hoge nauwkeurigheid. + Locatie inschakelen? + In de buurt heeft locatie nodig om correct te werken + Heeft u deze twee foto\'s op dezelfde plek gemaakt? Wilt u de breedtegraad/lengtegraad van de afbeelding rechts gebruiken? + Laad meer + Geen plaatsen gevonden, probeer uw zoekcriteria te wijzigen. + Voorgestelde verbeteringen: + - Voeg categorieën toe aan deze afbeelding om de bruikbaarheid te verbeteren. + - Voeg deze afbeelding toe aan het bijbehorende Wikipedia-artikel dat geen afbeeldingen heeft. + Afbeelding toevoegen aan Wikipedia + Wil je deze afbeelding toevoegen aan het Wikipedia-artikel in het %1$ Bevestigen Instructies + 1. Gebruik de volgende wikitekst: + 2. Als u op Bevestigen klikt, wordt het Wikipedia-artikel geopend + 3. Zoek een geschikte sectie in het artikel voor uw afbeelding + 4. Klik op het pictogram Bewerken (zoals een potlood) voor die sectie. + 5. Plak de wikitekst op de juiste plaats. + 7. Publiceer het artikel + Kopieer wikicode naar klembord pauzeren hervatten Gepauzeerd + Meer + Favorieten + Prestaties + Scorebord + Ranking: + Aantal: + Ranking + Gebruiker + Aantal + Instellen als leaderboard-avatar + Instellen als avatar, even geduld aub + Avatar ingesteld + Fout bij instellen nieuwe avatar, probeer het opnieuw + Instellen als avatar + Jaarlijks + Wekelijks + Alle tijden + Uploaden + In de buurt + Gebruikt + Mijn ranking + Mapbox-telemetrie + Stuur geanonimiseerde locatie- en gebruiksgegevens naar Mapbox bij gebruik van de functie In de buurt + &#169; <a href=\"https://www.mapbox.com/about/maps/\">Mapbox</a> &#169; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> <a href=\"https://www.mapbox.com/map-feedback/\">Verbeter deze kaart</a> + Beperkte verbindingsmodus ingeschakeld! + Beperkte verbindingsmodus uitgeschakeld. Uploads die in behandeling zijn, worden nu hervat. + Beperkte verbindingsmodus + Kwaliteitsafbeeldingen + 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… + U hebt de beperkte verbindingsmodus ingeschakeld. Alle uploads worden gepauzeerd en worden hervat zodra u deze modus uitschakelt. + Beperkte verbindingsmodus is ingeschakeld. + Schrijf een kort bijschrift dat zegt wat uw foto laat zien. Geef in de beschrijving aan wat de foto interessant of typisch of zeldzaam maakt en leg de context uit, zichtbaar of niet. Gebruik zoveel mogelijk exacte terminologie. + Zoek en selecteer alle concepten die deze afbeelding uitbeeldt. Wees zo specifiek mogelijk. Als de afbeelding meerdere items weergeeft, kies ze dan allemaal binnen redelijke grenzen. Kies geen generieke tags als er meer specifieke tags beschikbaar zijn. + Selecteer de juiste categorieën. In tegenstelling tot afbeeldingen zijn categorieën alleen in het Engels. + Commons maakt uw foto\'s herbruikbaar en aangepasbaar voor iedereen. Wilt u afstand doen van alle rechten? Wilt u worden toegeschreven? Wilt u dat aanpassingen dezelfde licentie gebruiken? + Beeldt af + Medialicentie + Mediadetails + Categoriepagina bekijken + Itempagina bekijken + Taal van de gebruikersinterface van de app + Verwijdert onderschrift en beschrijving + Meer lezen + In alle talen + Kies een locatie + Pannen en zoomen om aan te passen + Locatiekiezer afsluiten + Selecteer locatie + Toon in kaart-app + Locatie bewerken + De afbeeldingsweergave van de locatiekiezer + De schaduw van de afbeeldingsweergave van de locatiekiezer + Afbeeldingslocatie + Controleer of de locatie correct is From 3b7aa0376dc6d0c487b8193d734dc50d38beb98b Mon Sep 17 00:00:00 2001 From: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> Date: Sun, 18 Jul 2021 05:40:32 +0530 Subject: [PATCH 46/78] Added option for sharing achievements and a back button in ProfileActivity (#4489) * UI done * Share added * Minor modification * tests added * tests added * tests added * modified * modifications * modification * Entered if * Entered if * 97% coverage * Separate * Minor modifications * Minor modifications --- .../main/java/fr/free/nrw/commons/Utils.java | 10 +- .../nrw/commons/profile/ProfileActivity.java | 112 +++++++++++++++++- .../achievements/AchievementsFragment.java | 88 +------------- .../commons/profile/ProfileActivityTest.kt | 73 +++++++++++- .../AchievementsFragmentUnitTests.kt | 24 ---- 5 files changed, 189 insertions(+), 118 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java index 587a65467..232fa6605 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -178,9 +178,13 @@ public class Utils { public static Bitmap getScreenShot(View view) { View screenView = view.getRootView(); screenView.setDrawingCacheEnabled(true); - Bitmap bitmap = Bitmap.createBitmap(screenView.getDrawingCache()); - screenView.setDrawingCacheEnabled(false); - return bitmap; + Bitmap drawingCache = screenView.getDrawingCache(); + if (drawingCache != null) { + Bitmap bitmap = Bitmap.createBitmap(drawingCache); + screenView.setDrawingCacheEnabled(false); + return bitmap; + } + return null; } /* 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 b443efa42..2e0f6c261 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 @@ -1,8 +1,20 @@ package fr.free.nrw.commons.profile; +import android.app.AlertDialog; 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.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.viewpager.widget.ViewPager; @@ -10,12 +22,18 @@ import butterknife.BindView; import butterknife.ButterKnife; 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; +import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.profile.achievements.AchievementsFragment; import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment; import fr.free.nrw.commons.theme.BaseActivity; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; /** * This activity will set two tabs, achievements and @@ -31,6 +49,12 @@ public class ProfileActivity extends BaseActivity { @BindView(R.id.tab_layout) TabLayout tabLayout; + @BindView(R.id.toolbar) + Toolbar toolbar; + + @Inject + SessionManager sessionManager; + private ViewPagerAdapter viewPagerAdapter; private AchievementsFragment achievementsFragment; private LeaderboardFragment leaderboardFragment; @@ -40,7 +64,9 @@ public class ProfileActivity extends BaseActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_profile); ButterKnife.bind(this); - setTitle(R.string.Profile); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setTitle(sessionManager.getUserName()); supportFragmentManager = getSupportFragmentManager(); viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); @@ -49,6 +75,16 @@ public class ProfileActivity extends BaseActivity { setTabs(); } + /** + * Navigate up event + * @return boolean + */ + @Override + public boolean onSupportNavigateUp() { + onBackPressed(); + return true; + } + /** * Creates a way to change current activity to AchievementActivity * @param context @@ -75,10 +111,84 @@ public class ProfileActivity extends BaseActivity { viewPagerAdapter.notifyDataSetChanged(); } + @Override public void onDestroy() { super.onDestroy(); compositeDisposable.clear(); } + /** + * To inflate menu + * @param menu Menu + * @return boolean + */ + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + final MenuInflater menuInflater = getMenuInflater(); + menuInflater.inflate(R.menu.menu_about, menu); + return super.onCreateOptionsMenu(menu); + } + + /** + * To receive the id of selected item and handle further logic for that selected item + * @param item MenuItem + * @return boolean + */ + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + // take screenshot in form of bitmap and show it in Alert Dialog + if (item.getItemId() == R.id.share_app_icon) { + final View rootView = getWindow().getDecorView().findViewById(android.R.id.content); + final Bitmap screenShot = Utils.getScreenShot(rootView); + showAlert(screenShot); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** + * It displays the alertDialog with Image of screenshot + * @param screenshot screenshot of the present screen + */ + public void showAlert(final Bitmap screenshot) { + final AlertDialog.Builder alert = new AlertDialog.Builder(this); + final LayoutInflater factory = LayoutInflater.from(this); + final View view = factory.inflate(R.layout.image_alert_layout, null); + final ImageView screenShotImage = view.findViewById(R.id.alert_image); + screenShotImage.setImageBitmap(screenshot); + final TextView shareMessage = view.findViewById(R.id.alert_text); + shareMessage.setText(R.string.achievements_share_message); + alert.setView(view); + alert.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot)); + alert.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()); + alert.show(); + } + + /** + * To take bitmap and store it temporary storage and share it + * @param bitmap bitmap of screenshot + */ + void shareScreen(final Bitmap bitmap) { + try { + final File file = new File(getExternalCacheDir(), "screen.png"); + final FileOutputStream fileOutputStream = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream); + fileOutputStream.flush(); + fileOutputStream.close(); + file.setReadable(true, false); + + final Uri fileUri = FileProvider + .getUriForFile(getApplicationContext(), + getPackageName() + ".provider", file); + grantUriPermission(getPackageName(), fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + final Intent intent = new Intent(android.content.Intent.ACTION_SEND); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Intent.EXTRA_STREAM, fileUri); + intent.setType("image/png"); + startActivity(Intent.createChooser(intent, getString(R.string.share_image_via))); + } catch (final IOException e) { + e.printStackTrace(); + } + } } \ 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 efd416631..fd2864398 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 @@ -3,14 +3,10 @@ package fr.free.nrw.commons.profile.achievements; import android.accounts.Account; import android.app.AlertDialog; import android.app.AlertDialog.Builder; -import android.content.Intent; -import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.util.DisplayMetrics; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -21,22 +17,11 @@ import android.widget.RelativeLayout; import android.widget.TextView; import androidx.appcompat.view.ContextThemeWrapper; import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.core.content.FileProvider; import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; - -import com.dinuscxj.progressbar.CircleProgressBar; -import org.apache.commons.lang3.StringUtils; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Objects; - -import javax.inject.Inject; - import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; +import com.dinuscxj.progressbar.CircleProgressBar; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.SessionManager; @@ -46,6 +31,9 @@ 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 org.apache.commons.lang3.StringUtils; import timber.log.Timber; /** @@ -174,74 +162,6 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { return rootView; } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { - // Inflate the menu; this adds items to the action bar if it is present. - super.onCreateOptionsMenu(menu, menuInflater); - menuInflater.inflate(R.menu.menu_about, menu); - item = menu.getItem(0); - item.setVisible(false); - } - - /** - * To receive the id of selected item and handle further logic for that selected item - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - // take screenshot in form of bitmap and show it in Alert Dialog - if (id == R.id.share_app_icon) { - View rootView = getActivity().getWindow().getDecorView().findViewById(android.R.id.content); - Bitmap screenShot = Utils.getScreenShot(rootView); - showAlert(screenShot); - } - - return super.onOptionsItemSelected(item); - } - - /** - * It displays the alertDialog with Image of screenshot - * @param screenshot - */ - public void showAlert(Bitmap screenshot){ - AlertDialog.Builder alertadd = new AlertDialog.Builder(getActivity()); - LayoutInflater factory = LayoutInflater.from(getActivity()); - 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.achievements_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(); - } - - /** - * To take bitmap and store it temporary storage and share it - * @param bitmap - */ - void shareScreen(Bitmap bitmap) { - try { - File file = new File(getActivity().getExternalCacheDir(), "screen.png"); - FileOutputStream fOut = new FileOutputStream(file); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut); - fOut.flush(); - fOut.close(); - file.setReadable(true, false); - Uri fileUri = FileProvider - .getUriForFile(getActivity().getApplicationContext(), getActivity().getPackageName()+".provider", file); - getActivity().grantUriPermission(getActivity().getPackageName(), fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - final Intent intent = new Intent(android.content.Intent.ACTION_SEND); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(Intent.EXTRA_STREAM, fileUri); - intent.setType("image/png"); - startActivity(Intent.createChooser(intent, getString(R.string.share_image_via))); - } catch (IOException e) { - e.printStackTrace(); - } - } - /** * To invoke the AlertDialog on clicking info button */ diff --git a/app/src/test/kotlin/fr/free/nrw/commons/profile/ProfileActivityTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/profile/ProfileActivityTest.kt index 1fe0d969e..2671f69f6 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/profile/ProfileActivityTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/profile/ProfileActivityTest.kt @@ -1,30 +1,46 @@ package fr.free.nrw.commons.profile import android.content.Context +import android.graphics.Bitmap +import android.os.Looper +import android.view.Menu +import android.view.MenuItem +import fr.free.nrw.commons.R import fr.free.nrw.commons.TestCommonsApplication import org.junit.Assert import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.powermock.api.mockito.PowerMockito +import org.mockito.Mock +import org.mockito.MockitoAnnotations import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows import org.robolectric.annotation.Config +import org.robolectric.fakes.RoboMenu +import org.robolectric.fakes.RoboMenuItem +import java.lang.reflect.Method @RunWith(RobolectricTestRunner::class) @Config(sdk = [21], application = TestCommonsApplication::class) class ProfileActivityTest { + @Mock private lateinit var activity: ProfileActivity - private lateinit var profileActivity: ProfileActivity + + @Mock private lateinit var mockContext: Context + @Mock + private lateinit var bitmap: Bitmap + @Before fun setUp() { + MockitoAnnotations.initMocks(this) activity = Robolectric.buildActivity(ProfileActivity::class.java).create().get() - mockContext = PowerMockito.mock(Context::class.java) - profileActivity = PowerMockito.mock(ProfileActivity::class.java) + mockContext = RuntimeEnvironment.application.applicationContext } @Test @@ -41,8 +57,53 @@ class ProfileActivityTest { @Test @Throws(Exception::class) - fun testStartYourself() { - ProfileActivity.startYourself(mockContext) + fun testOnCreateOptionsMenu() { + val menu: Menu = RoboMenu(mockContext) + activity.onCreateOptionsMenu(menu) } + @Test + @Throws(Exception::class) + fun testOnOptionsItemSelected() { + val menuItem: MenuItem = RoboMenuItem(R.menu.menu_about) + Shadows.shadowOf(Looper.getMainLooper()).idle() + activity.onOptionsItemSelected(menuItem) + } + + @Test + @Throws(Exception::class) + fun testOnOptionsShareItemSelected() { + val menuItemShare: MenuItem = RoboMenuItem(R.id.share_app_icon) + activity.onOptionsItemSelected(menuItemShare) + } + + @Test + @Throws(Exception::class) + fun testStartYourself() { + ProfileActivity.startYourself(activity) + } + + @Test + @Throws(Exception::class) + fun testShowAlert() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + activity.showAlert(bitmap) + } + + @Test + @Throws(Exception::class) + fun testShareScreen() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + val method: Method = ProfileActivity::class.java.getDeclaredMethod( + "shareScreen", Bitmap::class.java + ) + method.isAccessible = true + method.invoke(activity, bitmap) + } + + @Test + @Throws(Exception::class) + fun testOnSupportNavigateUp() { + activity.onSupportNavigateUp() + } } \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/profile/achievements/AchievementsFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/profile/achievements/AchievementsFragmentUnitTests.kt index 306c50490..bf3e576db 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/profile/achievements/AchievementsFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/profile/achievements/AchievementsFragmentUnitTests.kt @@ -1,7 +1,6 @@ package fr.free.nrw.commons.profile.achievements import android.content.Context -import android.graphics.Bitmap import android.os.Looper import android.view.MenuItem import android.widget.ImageView @@ -83,9 +82,6 @@ class AchievementsFragmentUnitTests { @Mock private lateinit var imageUploadedText: TextView - @Mock - private lateinit var bitmap: Bitmap - @Mock private lateinit var progressBar: ProgressBar @@ -143,14 +139,6 @@ class AchievementsFragmentUnitTests { Assert.assertNotNull(fragment) } - @Test - @Throws(Exception::class) - fun testShowAlert() { - Shadows.shadowOf(Looper.getMainLooper()).idle() - fragment.showAlert(bitmap) - } - - @Test @Throws(Exception::class) fun testShowInfoDialog() { @@ -329,16 +317,4 @@ class AchievementsFragmentUnitTests { method.isAccessible = true method.invoke(fragment) } - - @Test - @Throws(Exception::class) - fun testShareScreen() { - Shadows.shadowOf(Looper.getMainLooper()).idle() - val method: Method = AchievementsFragment::class.java.getDeclaredMethod( - "shareScreen", Bitmap::class.java - ) - method.isAccessible = true - method.invoke(fragment, bitmap) - } - } \ No newline at end of file From 876e25800118a1f1af6bf2ab14ef253829e4823b Mon Sep 17 00:00:00 2001 From: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Date: Sun, 18 Jul 2021 12:47:51 +0530 Subject: [PATCH 47/78] Fix #4496 depiction description not fully visible (#4509) --- .../res/layout/layout_upload_depicts_item.xml | 80 ++++++++++--------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/app/src/main/res/layout/layout_upload_depicts_item.xml b/app/src/main/res/layout/layout_upload_depicts_item.xml index 9a0549236..a92384801 100644 --- a/app/src/main/res/layout/layout_upload_depicts_item.xml +++ b/app/src/main/res/layout/layout_upload_depicts_item.xml @@ -1,46 +1,54 @@ + + + android:checkMark="?android:attr/textCheckMark" + android:checked="false" + android:gravity="center_vertical" + android:padding="@dimen/tiny_gap" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/depicted_image" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + - + + android:id="@+id/depicts_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Label" + android:textStyle="bold" /> + android:id="@+id/description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Description" /> + + From 686dc93257b8b7db3728adab5ef3d7d920f79d00 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 19 Jul 2021 13:01:25 +0200 Subject: [PATCH 48/78] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-fi/strings.xml | 12 ++++++++++++ app/src/main/res/values-ko/strings.xml | 4 ++++ app/src/main/res/values-nqo/strings.xml | 3 ++- app/src/main/res/values-ru/strings.xml | 3 +++ app/src/main/res/values-zh/strings.xml | 5 +++++ 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 9876529b1..32de41a1c 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -236,6 +236,7 @@ Tämä kuva otettiin eri paikassa. Tallenna vain kuvia, jotka olet itse ottanut. Älä tallenna kuvia, joita olet löytänyt muiden ihmisten Facebook-tileiltä. Haluatko silti tallentaa tämän kuvan? + Yhteysvirhe Kuvasta löytyi ongelmia Tallenna vain kuvia, jotka olet itse ottanut. Älä tallenna kuvia, joita olet ladannut internetistä. Tallenna sovelluksen sisäisiä otoksia @@ -452,6 +453,7 @@ Sarjanumerot Ohjelmisto Lähetä valokuvia suoraan Wikimedia Commonsiin puhelimestasi. Lataa Commons-appi nyt: %1$s + Jaa sovellus... Kuvan tiedot Luokkia ei löytynyt Peruutettu tallennus @@ -479,6 +481,7 @@ Ei voitu lisätä luokkia. Päivitetään luokkia + Onnistui Jaa kuva Et ole vielä lahjoittanut mitään Käyttäjätunnus luotu! @@ -501,6 +504,7 @@ Käytä järjestelmän Tumma Vaalea + Ota sijainti käyttöön? Lataa lisää Paikkoja ei löytynyt, yritä vaihtaa hakuehtojasi. Ehdota parannuksia: @@ -547,7 +551,15 @@ Jatketaan lähettämistä... Keskeytetään lähetys... Esittää + Median lisenssi + Median tiedot Näytä luokkasivu Lue lisää Kaikilla kielillä + Valitse sijainti + Valitse sijainti + Näytä karttasovelluksessa + Muokkaa sijaintia + Kuvan sijainti + Tarkista, onko sijainti oikea diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index b5f14eac8..5d0045d8f 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -524,4 +524,8 @@ 모든 언어 장소 선택하기 장소 선택 + 맵 앱에 표시 + 위치 편집 + 이미지 위치 + 위치가 올바른지 확인 diff --git a/app/src/main/res/values-nqo/strings.xml b/app/src/main/res/values-nqo/strings.xml index 9b49d1f49..fd4c10a98 100644 --- a/app/src/main/res/values-nqo/strings.xml +++ b/app/src/main/res/values-nqo/strings.xml @@ -37,7 +37,7 @@ ߟߊ߬ߦߟߍ߬ߟߌ ߓߘߊ߫ ߘߊߡߌ߬ߣߊ߬߹ %1$s ߟߊ߬ߦߟߍ߬ߟߌ ߊ߬ ߛߐ߲߬ߞߌ߲߫ ߞߴߌ ߟߊ߫ ߟߊ߬ߦߟߍ߬ߣߍ߲ ߠߎ߬ ߘߐߜߍ߫ - %1$s ߟߊ߬ߦߟߍ߬ߟߌ ߓߘߊ߫ ߘߊߡߌ߬ߣߊ߫ + %1$s ߟߊ߬ߦߟߍ ߦߴߌ ߘߐ߫ %1$s ߟߊ߬ߦߟߍ߬ߟߌ ߦߋ߫ ߛߋ߲߬ߠߊ߫ %1$s ߟߊ߬ߦߟߍ߬ߟߌ ߞߎ߲߬ߘߏ߲߬ߠߌ߲ ߦߴߌ ߘߐ߫ %1$s ߟߊ߬ߦߟߍ߬ߟߌ ߓߘߊ߫ ߗߌߙߏ߲߫ @@ -53,6 +53,7 @@ ߛߌ߬ߢߐ߲߮ ߒ ߠߊ߫ ߟߊ߬ߦߟߍ߬ߣߍ߲ ߠߎ߬ ߊ߬ ߟߊߖߍ߲ߛߍ߲߫ + ߞߐߕߐ߮ ߞߐߜߍ ߦߋ߫ ߝߍ߬ߛߓߍߟߌ (ߡߊߢߌ߬ߣߌ߲߬ߞߊ߬ߣߍ߲) ߝߍ߬ߛߓߍߟߌ ߘߏ߫ ߡߊߛߐ߫ ߞߐߕߐ߮ ߣߌ߲߬ ߠߊ߫ ߖߊ߰ߣߌ߲߫ ߞߊ߲߬ߛߓߍߟߌ diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 618d3f3cc..efb84a543 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -23,6 +23,7 @@ * Patriccck * Redredsonia * Rubin16 +* Thothsum * Vlad5250 * VoxelJ * Wikisaurus @@ -634,5 +635,7 @@ Выйти из окна выбора местоположения Выберите местоположение Показать в приложении карты + Изменить местоположение + Расположение изображения Проверьте правильность местоположения diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index e55cc2656..7ac5dc9c9 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -6,6 +6,7 @@ * A Retired User * Aefgh39622 * Angrydog001 +* Crowley666 * D41D8CD98F * Deathkon * GuoPC @@ -619,6 +620,10 @@ 放大和做小来调整 退出位置拾取器 选择地点 + 在地图应用中显示 + 编辑位置 位置拾取器的图像视图 地点拾取器的照片观测视野的阴影。 + 图像位置 + 检查位置是否正确 From 2a9ae73750e3f8df29cba9ad7662c52a7e257831 Mon Sep 17 00:00:00 2001 From: benryder1988 Date: Fri, 23 Jul 2021 12:40:41 +0100 Subject: [PATCH 49/78] Fixes #4455 - Warning when pressing "back" in upload wizard (#4519) * Overrides the back button during upload with dialog * Removing unneeded import --- .../free/nrw/commons/upload/UploadActivity.java | 17 +++++++++++++++++ app/src/main/res/values/strings.xml | 8 ++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) 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 4c8346858..372abae53 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 @@ -45,6 +45,7 @@ import fr.free.nrw.commons.upload.license.MediaLicenseFragment; import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment; import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback; import fr.free.nrw.commons.upload.worker.UploadWorker; +import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -502,4 +503,20 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, uploadCategoriesFragment.setCallback(null); } } + + /** + * Overrides the back button to make sure the user is prepared to lose their progress + */ + @Override + public void onBackPressed() { + DialogUtil.showAlertDialog(this, + getString(R.string.back_button_warning), + getString(R.string.back_button_warning_desc), + getString(R.string.back_button_continue), + getString(R.string.back_button_warning), + null, + this::finish + ); + } + } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0f9920ab6..06451e998 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -391,11 +391,15 @@ Upload your first media by tapping on the add button. No Categories Selected - Images without categories are rarely usable. Are you sure you want to continue without selecting categories? - No Depictions Selected + No Depictions Selected Images with depictions are more easily found and more likely to be used. Are you sure you want to continue without selecting depictions? + + Cancel Upload + Using the back button will cancel this upload and you will lose your progress + Continue Upload + (For all image(s) in set) Search this area Permission Request From ee81877ebb3ca9b9f0a7f01729697fe9c9d3cb96 Mon Sep 17 00:00:00 2001 From: neslihanturan Date: Mon, 26 Jul 2021 08:17:34 +0300 Subject: [PATCH 50/78] This error seemed to occur on compositeDisposable.clear() so null checked there (#4525) --- .../nrw/commons/contributions/ContributionBoundaryCallback.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 00f002176..bf6d42ac2 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 @@ -70,7 +70,9 @@ class ContributionBoundaryCallback @Inject constructor( } ) }else { - compositeDisposable.clear() + if (compositeDisposable != null){ + compositeDisposable.clear() + } } } From 06dd7e547df1a52ba299082a9080f6945fca2246 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 26 Jul 2021 13:01:49 +0200 Subject: [PATCH 51/78] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ab/strings.xml | 2 ++ app/src/main/res/values-de/strings.xml | 3 +++ app/src/main/res/values-fr/strings.xml | 3 +++ app/src/main/res/values-it/strings.xml | 2 ++ app/src/main/res/values-iw/strings.xml | 3 +++ app/src/main/res/values-ko/strings.xml | 2 ++ app/src/main/res/values-ky/strings.xml | 4 ++++ app/src/main/res/values-pl/strings.xml | 3 +++ app/src/main/res/values-pms/strings.xml | 3 +++ app/src/main/res/values-pt-rBR/strings.xml | 3 +++ app/src/main/res/values-ru/strings.xml | 3 +++ app/src/main/res/values-sk/strings.xml | 3 +++ app/src/main/res/values-tr/strings.xml | 3 +++ app/src/main/res/values-uk/strings.xml | 3 +++ app/src/main/res/values-zh/strings.xml | 2 ++ 15 files changed, 42 insertions(+) diff --git a/app/src/main/res/values-ab/strings.xml b/app/src/main/res/values-ab/strings.xml index 90389729b..b0b1cd5aa 100644 --- a/app/src/main/res/values-ab/strings.xml +++ b/app/src/main/res/values-ab/strings.xml @@ -1,6 +1,7 @@ @@ -86,4 +87,5 @@ Архиарақәа Иҭыҵтәуп ахҳаа ԥшаам + Аҭагалара аԥыхра diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2cb981aeb..aaabdc763 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -395,6 +395,9 @@ Bilder ohne Kategorien sind selten nutzbar. Bist du sicher, dass du ohne die Auswahl von Kategorien hochladen möchtest? Keine Motive ausgewählt Bilder mit Motive sind leichter zu finden und werden eher verwendet. Bist dusicher, dass du ohne Auswahl von Motiven fortfahren möchtest? + Hochladen abbrechen + Wenn du den Zurück-Button verwendest, wird das Hochladen abgebrochen und du verlierst deine bisherigen Schritte + Hochladen fortsetzen (für alle Bilder im Satz) Diesen Bereich durchsuchen Berechtigungsanfrage diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index bc68c9636..313e4068a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -403,6 +403,9 @@ Les images sans catégories sont rarement utilisables. Voulez-vous vraiment continuer sans sélectionner des catégories  appropriées ? Aucun élément représenté sélectionné Les images avec des éléments représentés sont plus faciles à trouver et plus susceptibles d’être utilisées. Voulez-vous vraiment continuer sans sélectionner des éléments représentés ? + Annuler le téléchargement + L’utilisation du bouton de retour annulera ce téléchargement et vous perdrez votre progression + Continuer le téléchargement (Pour toutes les images dans le jeu) Chercher dans cette zone Demande d’autorisation diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b09fc9b81..87ed689b7 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -386,6 +386,8 @@ Le immagini senza categorie sono raramente utilizzabili. Sei sicuro di voler continuare senza selezionare le categorie? Non sono stati selezionati elementi Le immagini con descrizioni possono essere trovate più facilmente ed è più probabile il loro utilizzo. Davvero vuoi continuare senza selezionare elementi raffigurati? + Annulla caricamento + Continua caricamento (Per tutte le immagini nell\'insieme) Cerca in questa area Richiesta di permesso diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index eae4d6540..617501071 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -406,6 +406,9 @@ תמונות ללא קטגוריות בדרך כלל אינן שימושיות. להמשיך ללא בחירת קטגוריות? לא נבחרו מוצגים קל יותר למצוא תמונות עם מוצגים ויש סיכוי גבוה יותר שמישהו ישתמש בהן. האם ברצונך באמת להמשיך בלי לבחור מוצגים? + ביטול ההעלאה + שימוש בכתפור החזרה יבטל את ההעלאה הזאת וההתקדמות שלך תלך לאיבוד + נמשך ההעלאה (לכל התמונות בסדרה) חיפוש באזור הזה בקשת הרשאה diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 5d0045d8f..59def2338 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -382,6 +382,8 @@ 선택된 분류 없음 분류가 없는 그림은 거의 유용하지 않습니다. 분류를 선택하지 않고 제출하시겠습니까? 선택된 서술 없음 + 올리기 취소 + 계속 올리기 이 지역을 검색 권한 요청 사진이 필요한 주변 장소를 표시하기 위해 현재 위치를 사용하시겠습니까? diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index 2834326ca..e6109a926 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -2,6 +2,7 @@ @@ -93,4 +94,7 @@ Жүктөлүүдө… Тандалган жок Жаңылоо + Жүктөөнү жокко чыгаруу + Артка баскычын колдонуу менен бул жүктөө жокко чыгарылат жана сиз ийгиликти жоготосуз + Жүктөөнү улантуу diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index f9cf246a2..a73f71c07 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -407,6 +407,9 @@ Obrazy bez kategorii rzadko nadają się do użycia. Czy na pewno chcesz kontynuować bez wybierania kategorii? Nie wybrano żadnych obrazów Obrazy ze wskazanymi przedstawieniami są łatwiejsze do znalezienia i częściej używane. Czy na pewno chcesz kontynuować bez wybierania przedstawień? + Anuluj przesyłanie + Użycie przycisku „Wstecz” spowoduje anulowanie przesyłania i utratę postępów + Kontynuuj przesyłanie (Dla wszystkich obrazów w zestawie) Przeszukaj ten obszar Prośba o pozwolenie diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index 6240fb0d8..4d4e8b057 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -374,6 +374,9 @@ Le plance sensa categorìa, a so da ràir dovràbij. É-lo sigur ëd vorèj continué sensa serne dle categorìe? Gnun-e descrission selessionà Le plance con dle descrission a l\'é pi bel fé troveje e a l\'é pi probàbil ch\'as deuvro. É-lo sigur ëd vorèj continué sensa selessioné dla descrission? + Anulé ël cariament + An sgnacand an sël boton andré, ës cariament a sarà anulà e chiel a perdrà ij sò progress + Andé anans a carié (Për tute le plance dl\'ansem) Arserché an costa zòna Arcesta d\'autorisassion diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6101bab17..98712a46e 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -390,6 +390,9 @@ Imagens sem categorias raramente são utilizáveis. Tem certeza de que deseja continuar sem selecionar categorias? Nenhuma representação selecionada Imagens com representações são mais facilmente encontradas e mais propensas a serem usadas. Tem certeza de que deseja continuar sem selecionar representações? + Cancelar carregamento + Usar o botão Voltar cancelará este carregamento e você perderá seu progresso + Continuar carregamento (Para todas as imagens no conjunto) Pesquisar nesta área Pedido de Permissão diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index efb84a543..9ba02bca8 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -419,6 +419,9 @@ Изображения без категорий используются редко. Вы уверены, что хотите продолжить, не выбрав категории? Описания не выбраны Изображения с описаниями будет проще найти и использовать. Вы уверены, что хотите продолжить без выбора описаний? + Отменить загрузку + Использование кнопки «Назад» отменит эту загрузку + Продолжить загрузку (Для всех изображений в наборе) Искать в этой области Запрос разрешения diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 61f5b8def..d14b79eda 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -395,6 +395,9 @@ Obrázky bez kategórií sú zriedka použiteľné. Naozaj chcete nahrať obrázok bez výberu kategórií? Neboli zvolené žiadne zobrazenia Kreslené alebo maľované obrázky je ľahšie nájsť a použiť. Chcete naozaj pokračovať bez zadania spôsobu vykresľovania obrázkov? + Zrušiť nahrávanie + Použitím tlačidla späť zrušíte toto nahrávanie aj celý pokrok nahrávania + Pokračujte v nahrávaní (Pre všetky obrázky v sade) Prehľadať túto oblasť Je požadované oprávnenie diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e148e5dfd..f2d80b0c6 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -396,6 +396,9 @@ Kategorisiz görüntüler nadiren kullanılabilir. Kategori seçmeden devam etmek istediğinizden emin misiniz? Hiçbir Tasvir Seçilmedi Tasvirleri olan resimler daha kolay bulunur ve kullanılması daha olasıdır. Tasvirleri seçmeden devam etmek istediğinizden emin misiniz? + Yüklemeyi İptal Et + Geri düğmesini kullanmak, bu yüklemeyi iptal eder ve ilerlemenizi kaybedersiniz + Yüklemeye Devam Et (Setteki tüm görüntü(ler) için) Bu alanı ara Yetki İsteği diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 01676ca8d..8368f5630 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -400,6 +400,9 @@ Зображення без категорій рідко використовуються. Ви впевнені, що хочете продовжити без вказаних категорій? Описів зображеного не вибрано Медіа, у яких вказані зображувані об\'єкти, можуть бути легше знайдені та використані надалі. Ви впевнені, що хочете продовжити не вказавши що саме тут зображено? + Скасувати завантаження + Використання кнопки \"Назад\" скасує це завантаження + Продовжити завантаження (Для всіх зображень у наборі) Шукати в цій зоні Запит на дозвіл diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 7ac5dc9c9..8008004a1 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -407,6 +407,8 @@ 没有类别的图像很少可用。确实要继续而不选择类别吗? 没有选择描写 带有描述的图像更容易被发现并且更可能被使用。您确定不选择描述继续吗? + 取消上传 + 继续上传 (对于设置中的所有图像) 搜索这个区域 需要许可 From ebae63de5d7cd15d65cbf5983aa007e0c6b4b7f7 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 29 Jul 2021 13:01:43 +0200 Subject: [PATCH 52/78] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ja/strings.xml | 2 ++ app/src/main/res/values-mk/strings.xml | 3 +++ app/src/main/res/values-sv/strings.xml | 3 +++ app/src/main/res/values-zh-rTW/strings.xml | 3 +++ 4 files changed, 11 insertions(+) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index a7e5e5e90..947f3fbce 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -389,6 +389,8 @@ カテゴリを指定しない画像は使用されることがほとんどありません。ほんとうにカテゴリを選択しないまま作業を続けますか? 描写を選択していません 描写を添えた画像はないものよりも探しやすく利用されやすくなります。描写を選ばないまま、先に進んでよいですか? + アップロードをキャンセル + アップロードを続ける (画像全点を組み写真にする場合) この地域を検索 許可を申請 diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 9f783d968..6860c4fa1 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -376,6 +376,9 @@ Некатегоризираните слики се слабо употребливи. Дали сигурно сакате да продолжите без да ставите категории? Немате избрано прикажувања Сликите со прикажувања се полезни за наоѓање, со што е поголема веројатноста дека ќе бидат искористени. Дали сигурно сакате да продолжите без да изберете прикажувања? + Откажи подигање + Ако стиснете на копчето за одење назад, со тоа ќе го откажете подигањето и ќе го изгубите досегашниот напредок + Продолжи со подигање (За сите слики во збирот) Пребарај на ова подрачје Барање за дозвола diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index c3be6ee2f..420143bc7 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -381,6 +381,9 @@ Bilder utan kategorier används sällan. Är du säker på att du vill fortsätta utan att välja kategorier? Inga beskrivningar har valts Bilder med beskrivningar kan hittas lättare och kommer mer sannolikt användas. Är du säker på att du vill fortsätta utan att välja beskrivningar? + Avbryt uppladdning + Bakåtknappen avbryter denna uppladdning och du förlorar dina framsteg + Fortsätt uppladdning (För alla bilder i uppsättningen) Sök i detta område Begäran om behörighet diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3aa28b414..fb0e6fd91 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -388,6 +388,9 @@ 不帶分類的圖片很難有機會被利用到,您確定您要不選擇分類來繼續嗎? 沒有選擇描寫 帶有描寫的圖片會更容易被找到,並且更可能被拿來使用。您確定您要不選擇描寫來繼續嗎? + 取消上傳 + 使用倒退按鈕將會取消此上傳,您並且會失去您的進度 + 繼續上傳 (在集合的所有圖片) 搜尋此區域 權限請求 From 3314905cfa8985d0a70854dc3f62720dd95fb1df Mon Sep 17 00:00:00 2001 From: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Date: Mon, 2 Aug 2021 14:53:57 +0530 Subject: [PATCH 53/78] Add ContributionsListFragment Unit Tests (#4533) --- .../ContributionsListFragmentUnitTests.kt | 353 ++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsListFragmentUnitTests.kt diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsListFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsListFragmentUnitTests.kt new file mode 100644 index 000000000..b0c7734f8 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsListFragmentUnitTests.kt @@ -0,0 +1,353 @@ +package fr.free.nrw.commons.contributions + +import android.content.Context +import android.content.res.Configuration +import android.os.Bundle +import android.os.Looper +import android.view.LayoutInflater +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.floatingactionbutton.FloatingActionButton +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.TestAppAdapter +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.upload.WikidataPlace +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.powermock.reflect.Whitebox +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import org.wikipedia.AppAdapter +import java.lang.reflect.Method + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ContributionsListFragmentUnitTests { + + private lateinit var activity: MainActivity + private lateinit var fragment: ContributionsListFragment + private lateinit var context: Context + private lateinit var layoutInflater: LayoutInflater + + @Mock + private lateinit var savedInstanceState: Bundle + + @Mock + private lateinit var rvContributionsList: RecyclerView + + @Mock + private lateinit var adapter: ContributionsListAdapter + + @Mock + private lateinit var contribution: Contribution + + @Mock + private lateinit var media: Media + + @Mock + private lateinit var wikidataPlace: WikidataPlace + + @Mock + private lateinit var callback: ContributionsListFragment.Callback + + @Mock + private lateinit var layoutManager: RecyclerView.LayoutManager + + @Mock + private lateinit var gridLayoutManager: GridLayoutManager + + @Mock + private lateinit var noContributionsYet: TextView + + @Mock + private lateinit var progressBar: ProgressBar + + @Mock + private lateinit var fabPlus: FloatingActionButton + + @Mock + private lateinit var fabCamera: FloatingActionButton + + @Mock + private lateinit var fabGallery: FloatingActionButton + + @Mock + private lateinit var newConfig: Configuration + + @Mock + private lateinit var fabLayout: LinearLayout + + @Mock + private lateinit var contributionsListPresenter: ContributionsListPresenter + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + AppAdapter.set(TestAppAdapter()) + + context = RuntimeEnvironment.application.applicationContext + activity = Robolectric.buildActivity(MainActivity::class.java).create().get() + layoutInflater = LayoutInflater.from(activity) + + fragment = ContributionsListFragment() + val fragmentManager: FragmentManager = activity.supportFragmentManager + val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction() + fragmentTransaction.add(fragment, null) + fragmentTransaction.commit() + + Whitebox.setInternalState(fragment, "rvContributionsList", rvContributionsList) + Whitebox.setInternalState(fragment, "adapter", adapter) + Whitebox.setInternalState(fragment, "callback", callback) + Whitebox.setInternalState(fragment, "noContributionsYet", noContributionsYet) + Whitebox.setInternalState(fragment, "progressBar", progressBar) + Whitebox.setInternalState(fragment, "fabPlus", fabPlus) + Whitebox.setInternalState(fragment, "fabCamera", fabCamera) + Whitebox.setInternalState(fragment, "fabGallery", fabGallery) + Whitebox.setInternalState(fragment, "fab_layout", fabLayout) + Whitebox.setInternalState( + fragment, + "contributionsListPresenter", + contributionsListPresenter + ) + } + + @Test + @Throws(Exception::class) + fun checkFragmentNotNull() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + Assert.assertNotNull(fragment) + } + + @Test + @Throws(Exception::class) + fun testOnCreateView() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.onCreateView(layoutInflater, null, savedInstanceState) + } + + @Test + @Throws(Exception::class) + fun testOnDetach() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.onDetach() + } + + @Test + @Throws(Exception::class) + fun testGetContributionStateAt() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + `when`(adapter.getContributionForPosition(anyInt())).thenReturn(contribution) + fragment.getContributionStateAt(0) + } + + @Test + @Throws(Exception::class) + fun testOnConfirmClicked() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + `when`(contribution.media).thenReturn(media) + `when`(media.wikiCode).thenReturn("") + `when`(contribution.wikidataPlace).thenReturn(wikidataPlace) + fragment.onConfirmClicked(contribution, true) + } + + @Test + @Throws(Exception::class) + fun testGetTotalMediaCount() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.totalMediaCount + } + + @Test + @Throws(Exception::class) + fun testGetMediaAtPositionCaseNonNull() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + `when`(adapter.getContributionForPosition(anyInt())).thenReturn(contribution) + `when`(contribution.media).thenReturn(media) + fragment.getMediaAtPosition(0) + } + + @Test + @Throws(Exception::class) + fun testGetMediaAtPositionCaseNull() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + `when`(adapter.getContributionForPosition(anyInt())).thenReturn(null) + fragment.getMediaAtPosition(0) + } + + @Test + @Throws(Exception::class) + fun testShowAddImageToWikipediaInstructions() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + val method: Method = ContributionsListFragment::class.java.getDeclaredMethod( + "showAddImageToWikipediaInstructions", + Contribution::class.java + ) + method.isAccessible = true + method.invoke(fragment, contribution) + } + + @Test + @Throws(Exception::class) + fun testResumeUpload() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.resumeUpload(contribution) + } + + @Test + @Throws(Exception::class) + fun testPauseUpload() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.pauseUpload(contribution) + } + + @Test + @Throws(Exception::class) + fun testAddImageToWikipedia() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.addImageToWikipedia(contribution) + } + + @Test + @Throws(Exception::class) + fun testOpenMediaDetail() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.openMediaDetail(0, true) + } + + @Test + @Throws(Exception::class) + fun testDeleteUpload() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.deleteUpload(contribution) + } + + @Test + @Throws(Exception::class) + fun testRetryUpload() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.retryUpload(contribution) + } + + @Test + @Throws(Exception::class) + fun testOnViewStateRestored() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + `when`(rvContributionsList.layoutManager).thenReturn(layoutManager) + fragment.onViewStateRestored(savedInstanceState) + } + + @Test + @Throws(Exception::class) + fun testOnSaveInstanceState() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + `when`(rvContributionsList.layoutManager).thenReturn(gridLayoutManager) + fragment.onSaveInstanceState(savedInstanceState) + } + + @Test + @Throws(Exception::class) + fun testShowNoContributionsUI() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.showNoContributionsUI(true) + } + + @Test + @Throws(Exception::class) + fun testShowProgress() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.showProgress(true) + } + + @Test + @Throws(Exception::class) + fun testShowWelcomeTip() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.showWelcomeTip(true) + } + + @Test + @Throws(Exception::class) + fun testAnimateFAB() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + `when`(fabPlus.isShown).thenReturn(false) + val method: Method = ContributionsListFragment::class.java.getDeclaredMethod( + "animateFAB", + Boolean::class.java + ) + method.isAccessible = true + method.invoke(fragment, true) + } + + @Test + @Throws(Exception::class) + fun testAnimateFABCaseShownAndOpen() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + `when`(fabPlus.isShown).thenReturn(true) + val method: Method = ContributionsListFragment::class.java.getDeclaredMethod( + "animateFAB", + Boolean::class.java + ) + method.isAccessible = true + method.invoke(fragment, true) + } + + @Test + @Throws(Exception::class) + fun testAnimateFABCaseShownAndClose() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + `when`(fabPlus.isShown).thenReturn(true) + val method: Method = ContributionsListFragment::class.java.getDeclaredMethod( + "animateFAB", + Boolean::class.java + ) + method.isAccessible = true + method.invoke(fragment, false) + } + + @Test + @Throws(Exception::class) + fun testSetListeners() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + val method: Method = ContributionsListFragment::class.java.getDeclaredMethod( + "setListeners" + ) + method.isAccessible = true + method.invoke(fragment) + } + + @Test + @Throws(Exception::class) + fun testInitializeAnimations() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + val method: Method = ContributionsListFragment::class.java.getDeclaredMethod( + "initializeAnimations" + ) + method.isAccessible = true + method.invoke(fragment) + } + + @Test + @Throws(Exception::class) + fun testOnConfigurationChanged() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + newConfig.orientation = Configuration.ORIENTATION_LANDSCAPE + fragment.onConfigurationChanged(newConfig) + } + +} \ No newline at end of file From a584c8f2393aaec8aeb81f94337c107f1fe24ead Mon Sep 17 00:00:00 2001 From: neslihanturan Date: Mon, 2 Aug 2021 12:25:07 +0300 Subject: [PATCH 54/78] Add nullcheck (#4530) --- .../java/fr/free/nrw/commons/media/MediaDetailFragment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 d913ff149..f7db4574e 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 @@ -546,8 +546,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements image.getHierarchy().setFailureImage(R.drawable.image_placeholder); DraweeController controller = Fresco.newDraweeControllerBuilder() - .setLowResImageRequest(ImageRequest.fromUri(media.getThumbUrl())) - .setImageRequest(ImageRequest.fromUri(media.getImageUrl())) + .setLowResImageRequest(ImageRequest.fromUri(media != null ? media.getThumbUrl() : null)) + .setImageRequest(ImageRequest.fromUri(media != null ? media.getImageUrl() : null)) .setControllerListener(aspectRatioListener) .setOldController(image.getController()) .build(); From 3dc36af8f1f84c95243486c868e66a7bacddcc1d Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 2 Aug 2021 13:01:28 +0200 Subject: [PATCH 55/78] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ta/strings.xml | 52 +++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 2a253a337..8c0745755 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -1,6 +1,7 @@ + பொதுவகம் செயலிழந்தது + ஏதோ தவறாகியுள்ளது! + நீங்கள் என்ன செய்கிறீர்கள் என்று எங்களிடம் கூறுங்கள், பின்னர் அதை எங்களுக்கு மின்னஞ்சல் மூலம் பகிரவும். அதை சரிசெய்ய எங்களுக்கு உதவும்! நன்றி! diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 8c0745755..d1068eeea 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -99,8 +99,19 @@ CC BY-SA 3.0 CC BY 3.0 CC BY-SA 4.0 + விக்கிபீடியாவில் பயன்படுத்தப்படும் பெரும்பாலான படங்களை விக்கிமீடியா காமன்ஸ் வழங்குகிறது. + உலகெங்கிலும் உள்ள மக்களுக்கு கல்வி கற்பதற்கு உங்கள் படங்கள் உதவுகின்றன! + தயவுசெய்து நீங்களே எடுத்த அல்லது முழுமையாக உருவாக்கிய படங்களை பதிவேற்றவும்: + இயற்கை பொருட்கள் (மலர்கள், விலங்குகள், மலைகள்) + பயனுள்ள பொருட்கள் (மிதிவண்டிகள், ரயில் நிலையங்கள்) + பிரபலமான நபர்கள் (உங்கள் நகரத்தலைவர், நீங்கள் சந்தித்த ஒலிம்பிக் விளையாட்டு வீரர்கள்) அருள்கூர்ந்து பதிவேற்றவேண்டாம்: + உங்கள் நண்பர்களின் சுயபடங்கள் அல்லது படங்கள் + நீங்கள் இணையத்திலிருந்து பதிவிறக்கம் செய்த படங்கள் + தனியுடைமையுடைய மென்பொருட்களின் திரைக்காட்சிகள் எடுத்துக்காட்டு பதிவேற்றம்: + தலைப்பு: சிட்னி ஒப்பேரா மாளிகை + விளக்கம்: விரிகுடாவின் குறுக்கே சிட்னி ஒப்பேரா மாளிகை பார்க்கப்படுகிறது ஆம்! <u>மேலதிக விவரம்</u> பகுப்புகள் From a94a28f743355b88e05010c3ce0cf91ab06a05c1 Mon Sep 17 00:00:00 2001 From: benryder1988 Date: Fri, 6 Aug 2021 09:43:09 +0100 Subject: [PATCH 57/78] Removing setText calls which seem prevent text selection (#4520) --- .../fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java | 2 -- 1 file changed, 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 cb51ac4ff..17ac18e51 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 @@ -123,8 +123,6 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter Date: Fri, 6 Aug 2021 15:48:19 +0530 Subject: [PATCH 58/78] #4005 For images in set removed if user selected one image (#4022) * #4005 For images in set removed if user selected one image * #4005 changed code as suggested * fixed syntax * added javadoc * added more java doc * refactored to more meaningful documentation * reverted the Project.xml file * cleaned documentations a little bit --- app/src/main/AndroidManifest.xml | 1 + .../nrw/commons/upload/UploadActivity.java | 14 ++++++++++++ .../categories/UploadCategoriesFragment.java | 22 ++++++++++++++++++- .../upload/depicts/DepictsFragment.java | 21 +++++++++++++++++- .../upload/license/MediaLicenseFragment.java | 21 +++++++++++++++++- 5 files changed, 76 insertions(+), 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3d996469b..cc6576aad 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + 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 372abae53..ef3544f6c 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 @@ -56,6 +56,8 @@ import java.util.Collections; import java.util.List; import javax.inject.Inject; import javax.inject.Named; +import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType.Object; +import kotlin.reflect.jvm.internal.impl.util.Checks; import timber.log.Timber; public class UploadActivity extends BaseActivity implements UploadContract.View, UploadBaseFragment.Callback { @@ -108,6 +110,10 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, private Place place; private List uploadableFiles = Collections.emptyList(); private int currentSelectedPosition = 0; + /* + Checks for if multiple files selected + */ + private boolean isMultipleFilesSelected = false; public static final String EXTRA_FILES = "commons_image_exta"; @@ -400,12 +406,20 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, Timber.d("Received intent %s with action %s", intent.toString(), intent.getAction()); uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES); + isMultipleFilesSelected = uploadableFiles.size() > 1; Timber.i("Received multiple upload %s", uploadableFiles.size()); place = intent.getParcelableExtra(PLACE_OBJECT); resetDirectPrefs(); } + /** + * Returns if multiple files selected or not. + */ + public boolean getIsMultipleFilesSelected() { + return isMultipleFilesSelected; + } + public void resetDirectPrefs() { directKvStore.remove(PLACE_OBJECT); } 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 1618c4a6e..610f93d99 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 @@ -1,7 +1,9 @@ package fr.free.nrw.commons.upload.categories; +import android.app.Activity; import android.os.Bundle; import android.text.Editable; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; @@ -22,6 +24,7 @@ import com.jakewharton.rxbinding2.view.RxView; import com.jakewharton.rxbinding2.widget.RxTextView; import fr.free.nrw.commons.R; import fr.free.nrw.commons.category.CategoryItem; +import fr.free.nrw.commons.upload.UploadActivity; import fr.free.nrw.commons.upload.UploadBaseFragment; import fr.free.nrw.commons.utils.DialogUtil; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -36,6 +39,8 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate @BindView(R.id.tv_title) TextView tvTitle; + @BindView(R.id.tv_subtitle) + TextView tvSubTitle; @BindView(R.id.til_container_search) TextInputLayout tilContainerEtSearch; @BindView(R.id.et_search) @@ -68,7 +73,8 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate private void init() { tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1, - callback.getTotalNumberOfSteps(), getString(R.string.categories_activity_title))); + callback.getTotalNumberOfSteps(), getString(R.string.categories_activity_title))); + setTvSubTitle(); tooltip.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -89,6 +95,20 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate .subscribe(filter -> searchForCategory(filter.toString()), Timber::e); } + /** + * Removes the tv subtitle If the activity is the instance of [UploadActivity] and + * if multiple files aren't selected. + */ + private void setTvSubTitle() { + final Activity activity = getActivity(); + if (activity instanceof UploadActivity) { + final boolean isMultipleFileSelected = ((UploadActivity) activity).getIsMultipleFilesSelected(); + if (!isMultipleFileSelected) { + tvSubTitle.setVisibility(View.GONE); + } + } + } + private void searchForCategory(String query) { presenter.searchForCategories(query); } 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 0b1dc5156..dce952ca1 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 @@ -1,5 +1,6 @@ package fr.free.nrw.commons.upload.depicts; +import android.app.Activity; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -20,6 +21,7 @@ import com.google.android.material.textfield.TextInputLayout; import com.jakewharton.rxbinding2.view.RxView; import com.jakewharton.rxbinding2.widget.RxTextView; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.upload.UploadActivity; import fr.free.nrw.commons.upload.UploadBaseFragment; import fr.free.nrw.commons.upload.UploadModel; import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; @@ -41,6 +43,8 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra @BindView(R.id.depicts_title) TextView depictsTitle; + @BindView(R.id.depicts_subtitle) + TextView depictsSubTitle; @BindView(R.id.depicts_search_container) TextInputLayout depictsSearchContainer; @BindView(R.id.depicts_search) @@ -76,7 +80,8 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra */ private void init() { depictsTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1, - callback.getTotalNumberOfSteps(), getString(R.string.depicts_step_title))); + callback.getTotalNumberOfSteps(), getString(R.string.depicts_step_title))); + setDepictsSubTitle(); tooltip.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -88,6 +93,20 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra addTextChangeListenerToSearchBox(); } + /** + * Removes the depicts subtitle If the activity is the instance of [UploadActivity] and + * if multiple files aren't selected. + */ + private void setDepictsSubTitle() { + final Activity activity = getActivity(); + if (activity instanceof UploadActivity) { + final boolean isMultipleFileSelected = ((UploadActivity) activity).getIsMultipleFilesSelected(); + if (!isMultipleFileSelected) { + depictsSubTitle.setVisibility(View.GONE); + } + } + } + /** * Initialise recyclerView and set adapter */ diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java index 530d4bf61..6164fd6cc 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.upload.license; +import android.app.Activity; import android.net.Uri; import android.os.Bundle; import android.text.Html; @@ -20,6 +21,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import fr.free.nrw.commons.upload.UploadActivity; import fr.free.nrw.commons.utils.DialogUtil; import java.util.List; @@ -38,6 +40,8 @@ public class MediaLicenseFragment extends UploadBaseFragment implements MediaLic @BindView(R.id.tv_title) TextView tvTitle; + @BindView(R.id.tv_subtitle) + TextView tvSubTitle; @BindView(R.id.spinner_license_list) Spinner spinnerLicenseList; @BindView(R.id.tv_share_license_summary) @@ -72,7 +76,8 @@ public class MediaLicenseFragment extends UploadBaseFragment implements MediaLic private void init() { tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1, - callback.getTotalNumberOfSteps(), getString(R.string.license_step_title))); + callback.getTotalNumberOfSteps(), getString(R.string.license_step_title))); + setTvSubTitle(); tooltip.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -84,6 +89,20 @@ public class MediaLicenseFragment extends UploadBaseFragment implements MediaLic presenter.getLicenses(); } + /** + * Removes the tv Subtitle If the activity is the instance of [UploadActivity] and + * if multiple files aren't selected. + */ + private void setTvSubTitle() { + final Activity activity = getActivity(); + if (activity instanceof UploadActivity) { + final boolean isMultipleFileSelected = ((UploadActivity) activity).getIsMultipleFilesSelected(); + if (!isMultipleFileSelected) { + tvSubTitle.setVisibility(View.GONE); + } + } + } + private void initPresenter() { presenter.onAttachView(this); } From 6dff9d674ef7153373d2c0640776404c82784e96 Mon Sep 17 00:00:00 2001 From: neslihanturan Date: Fri, 6 Aug 2021 14:02:49 +0300 Subject: [PATCH 59/78] Remove unnecessary imports and fix type error (#4535) --- .../main/java/fr/free/nrw/commons/upload/UploadActivity.java | 2 -- 1 file changed, 2 deletions(-) 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 ef3544f6c..970aa6ad0 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 @@ -56,8 +56,6 @@ import java.util.Collections; import java.util.List; import javax.inject.Inject; import javax.inject.Named; -import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType.Object; -import kotlin.reflect.jvm.internal.impl.util.Checks; import timber.log.Timber; public class UploadActivity extends BaseActivity implements UploadContract.View, UploadBaseFragment.Callback { From efc8a6e7d778a703258de26fb83cc5d00abd0eae Mon Sep 17 00:00:00 2001 From: Aditya Srivastava Date: Tue, 25 May 2021 00:54:55 +0530 Subject: [PATCH 60/78] Initialised xmls, made folder and image item. --- app/build.gradle | 2 +- .../main/res/drawable-ldpi/circle_shape.xml | 4 + app/src/main/res/drawable-ldpi/commons.xml | 62 +++++++++++++ .../res/layout/activity_custom_selector.xml | 7 ++ .../res/layout/fragment_custom_selector.xml | 42 +++++++++ .../layout/item_custom_selector_folder.xml | 84 ++++++++++++++++++ .../res/layout/item_custom_selector_image.xml | 87 +++++++++++++++++++ 7 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable-ldpi/circle_shape.xml create mode 100644 app/src/main/res/drawable-ldpi/commons.xml create mode 100644 app/src/main/res/layout/activity_custom_selector.xml create mode 100644 app/src/main/res/layout/fragment_custom_selector.xml create mode 100644 app/src/main/res/layout/item_custom_selector_folder.xml create mode 100644 app/src/main/res/layout/item_custom_selector_image.xml diff --git a/app/build.gradle b/app/build.gradle index c6f34cc68..68405acce 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -204,7 +204,7 @@ android { } } debug { - minifyEnabled true + minifyEnabled false testCoverageEnabled project.hasProperty('coverage') proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' testProguardFile 'test-proguard-rules.txt' diff --git a/app/src/main/res/drawable-ldpi/circle_shape.xml b/app/src/main/res/drawable-ldpi/circle_shape.xml new file mode 100644 index 000000000..d581bfb9f --- /dev/null +++ b/app/src/main/res/drawable-ldpi/circle_shape.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-ldpi/commons.xml b/app/src/main/res/drawable-ldpi/commons.xml new file mode 100644 index 000000000..4c2e6cabf --- /dev/null +++ b/app/src/main/res/drawable-ldpi/commons.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml new file mode 100644 index 000000000..1b6d22e00 --- /dev/null +++ b/app/src/main/res/layout/activity_custom_selector.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_custom_selector.xml b/app/src/main/res/layout/fragment_custom_selector.xml new file mode 100644 index 000000000..45a174bff --- /dev/null +++ b/app/src/main/res/layout/fragment_custom_selector.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_custom_selector_folder.xml b/app/src/main/res/layout/item_custom_selector_folder.xml new file mode 100644 index 000000000..3592255dd --- /dev/null +++ b/app/src/main/res/layout/item_custom_selector_folder.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_custom_selector_image.xml b/app/src/main/res/layout/item_custom_selector_image.xml new file mode 100644 index 000000000..7252f543f --- /dev/null +++ b/app/src/main/res/layout/item_custom_selector_image.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From a022a0bfaf57e948f2b8e96644d842be19c55690 Mon Sep 17 00:00:00 2001 From: Aditya Srivastava Date: Sat, 5 Jun 2021 05:25:10 +0530 Subject: [PATCH 61/78] xmls done --- app/src/main/res/drawable/ic_arrow_back_black.xml | 10 ++++++++++ app/src/main/res/drawable/ic_done_black.xml | 10 ++++++++++ app/src/main/res/drawable/ic_done_white.xml | 5 +++++ app/src/main/res/layout/custom_selector_toolbar.xml | 6 ++++++ 4 files changed, 31 insertions(+) create mode 100644 app/src/main/res/drawable/ic_arrow_back_black.xml create mode 100644 app/src/main/res/drawable/ic_done_black.xml create mode 100644 app/src/main/res/drawable/ic_done_white.xml create mode 100644 app/src/main/res/layout/custom_selector_toolbar.xml diff --git a/app/src/main/res/drawable/ic_arrow_back_black.xml b/app/src/main/res/drawable/ic_arrow_back_black.xml new file mode 100644 index 000000000..b5487b3ea --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_black.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_done_black.xml b/app/src/main/res/drawable/ic_done_black.xml new file mode 100644 index 000000000..899cbb684 --- /dev/null +++ b/app/src/main/res/drawable/ic_done_black.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_done_white.xml b/app/src/main/res/drawable/ic_done_white.xml new file mode 100644 index 000000000..2728880b7 --- /dev/null +++ b/app/src/main/res/drawable/ic_done_white.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/custom_selector_toolbar.xml b/app/src/main/res/layout/custom_selector_toolbar.xml new file mode 100644 index 000000000..28f5b725c --- /dev/null +++ b/app/src/main/res/layout/custom_selector_toolbar.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file From 269719c66022e8b96735143071eb9845eefbd5e4 Mon Sep 17 00:00:00 2001 From: Aditya Srivastava Date: Sat, 5 Jun 2021 05:25:47 +0530 Subject: [PATCH 62/78] xmls completed --- app/src/main/res/drawable/ic_done_black.xml | 2 +- .../res/layout/activity_custom_selector.xml | 23 ++++++++-- .../res/layout/custom_selector_toolbar.xml | 46 +++++++++++++++++-- .../res/layout/fragment_custom_selector.xml | 14 +++--- .../layout/item_custom_selector_folder.xml | 13 +++--- .../res/layout/item_custom_selector_image.xml | 15 +++--- app/src/main/res/values/attrs.xml | 2 + app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/styles.xml | 4 ++ 10 files changed, 91 insertions(+), 31 deletions(-) diff --git a/app/src/main/res/drawable/ic_done_black.xml b/app/src/main/res/drawable/ic_done_black.xml index 899cbb684..2d3858a70 100644 --- a/app/src/main/res/drawable/ic_done_black.xml +++ b/app/src/main/res/drawable/ic_done_black.xml @@ -5,6 +5,6 @@ android:viewportHeight="24" android:tint="?attr/colorControlNormal"> diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml index 1b6d22e00..f90ca51e2 100644 --- a/app/src/main/res/layout/activity_custom_selector.xml +++ b/app/src/main/res/layout/activity_custom_selector.xml @@ -1,7 +1,24 @@ - + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/custom_selector_toolbar.xml b/app/src/main/res/layout/custom_selector_toolbar.xml index 28f5b725c..45ebdc923 100644 --- a/app/src/main/res/layout/custom_selector_toolbar.xml +++ b/app/src/main/res/layout/custom_selector_toolbar.xml @@ -1,6 +1,44 @@ - + - \ No newline at end of file + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_custom_selector.xml b/app/src/main/res/layout/fragment_custom_selector.xml index 45a174bff..8a41cb6dd 100644 --- a/app/src/main/res/layout/fragment_custom_selector.xml +++ b/app/src/main/res/layout/fragment_custom_selector.xml @@ -6,20 +6,19 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + /> + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index ac5d31cf7..b3fb6d1a4 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -58,6 +58,7 @@ 0dp + 2dp 6dp 10dp 20dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06451e998..ab91a42e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -638,5 +638,7 @@ Upload your first media by tapping on the add button. The shadow of the image view of the location picker Image Location Check whether location is correct + Custom Selector + No Images diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 308291dea..1ae9e0a7c 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -55,6 +55,8 @@ @color/white @color/white @drawable/ic_search_white_24dp + @drawable/ic_done_white + @drawable/ic_arrow_back_white false false @@ -113,6 +115,8 @@ @color/disabled_button_text_color_dark @color/primaryDarkColor @drawable/ic_search_blue_24dp + @drawable/ic_done_black + @drawable/ic_arrow_back_black false false From 3cd87072e80f15694510a2585e206163db8eb253 Mon Sep 17 00:00:00 2001 From: Aditya Srivastava Date: Sat, 5 Jun 2021 08:00:07 +0530 Subject: [PATCH 63/78] removed unwanted attribute --- app/src/main/res/layout/item_custom_selector_folder.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/item_custom_selector_folder.xml b/app/src/main/res/layout/item_custom_selector_folder.xml index c13838e92..4dcc78c1a 100644 --- a/app/src/main/res/layout/item_custom_selector_folder.xml +++ b/app/src/main/res/layout/item_custom_selector_folder.xml @@ -31,7 +31,7 @@ android:layout_height="match_parent" android:id="@+id/album_overlay" android:alpha="0.05" - android:background=""/> + /> Date: Thu, 10 Jun 2021 13:02:00 +0530 Subject: [PATCH 64/78] Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable --- app/src/main/AndroidManifest.xml | 3 + .../ContributionsListFragment.java | 83 +++++++---- .../listeners/FolderClickListener.kt | 7 + .../listeners/ImageLoaderListener.kt | 8 + .../listeners/ImageSelectListener.kt | 7 + .../customselector/model/CallbackStatus.kt | 18 +++ .../commons/customselector/model/Folder.kt | 44 ++++++ .../nrw/commons/customselector/model/Image.kt | 125 ++++++++++++++++ .../commons/customselector/model/Result.kt | 13 ++ .../ui/adapter/FolderAdapter.kt | 141 ++++++++++++++++++ .../customselector/ui/adapter/ImageAdapter.kt | 135 +++++++++++++++++ .../ui/adapter/RecyclerViewAdapter.kt | 12 ++ .../ui/selector/CustomSelectorActivity.kt | 121 +++++++++++++++ .../ui/selector/CustomSelectorViewModel.kt | 36 +++++ .../CustomSelectorViewModelFactory.kt | 17 +++ .../ui/selector/FolderFragment.kt | 108 ++++++++++++++ .../ui/selector/ImageFileLoader.kt | 38 +++++ .../ui/selector/ImageFragment.kt | 126 ++++++++++++++++ .../customselector/ui/selector/ImageLoader.kt | 7 + .../nrw/commons/di/ActivityBuilderModule.java | 4 + .../commons/di/CommonsApplicationModule.java | 6 + .../nrw/commons/di/FragmentBuilderModule.java | 8 + .../res/layout/activity_custom_selector.xml | 9 +- .../res/layout/custom_selector_toolbar.xml | 7 +- .../layout/fragment_contributions_list.xml | 13 ++ .../res/layout/fragment_custom_selector.xml | 1 + .../layout/item_custom_selector_folder.xml | 45 +++--- .../res/layout/item_custom_selector_image.xml | 23 +-- 28 files changed, 1092 insertions(+), 73 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageLoaderListener.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/RecyclerViewAdapter.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelFactory.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cc6576aad..1c1068327 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -110,6 +110,9 @@ + + ) + fun onFailed(throwable: Throwable) +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt new file mode 100644 index 000000000..c29aa21e2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt @@ -0,0 +1,7 @@ +package fr.free.nrw.commons.customselector.listeners + +import fr.free.nrw.commons.customselector.model.Image + +interface ImageSelectListener { + fun onSelectedImagesChanged(selectedImages: ArrayList) +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt new file mode 100644 index 000000000..257b39a95 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt @@ -0,0 +1,18 @@ +package fr.free.nrw.commons.customselector.model + +sealed class CallbackStatus { + /** + IDLE : The callback is idle , doing nothing. + */ + object IDLE : CallbackStatus() + + /** + FETCHING : Fetching images. + */ + object FETCHING : CallbackStatus() + + /** + SUCCESS : Success fetching images. + */ + object SUCCESS : CallbackStatus() +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt new file mode 100644 index 000000000..0ce95ec22 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt @@ -0,0 +1,44 @@ +package fr.free.nrw.commons.customselector.model + +data class Folder( + /** + bucketId : Unique directory id, eg 540528482 + */ + var bucketId: Long, + + /** + name : bucket/folder name, eg Camera + */ + var name: String, + + /** + images : folder images, list of all images under this folder. + */ + var images: ArrayList = arrayListOf() + + +) { + /** + * Indicates whether some other object is "equal to" this one. + */ + override fun equals(other: Any?): Boolean { + + if (javaClass != other?.javaClass) { + return false + } + + other as Folder + + if (bucketId != other.bucketId) { + return false + } + if (name != other.name) { + return false + } + if (images != other.images) { + return false + } + + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt new file mode 100644 index 000000000..d6d296f29 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt @@ -0,0 +1,125 @@ +package fr.free.nrw.commons.customselector.model + +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable + +data class Image( + /** + id : Unique image id, primary key of image in device, eg 104950 + */ + var id: Long, + + /** + name : Name of the image with extension, eg CommonsLogo.jpeg + */ + var name: String, + + /** + uri : Uri of the image, points to image location or name, eg content://media/external/images/camera/10495 (Android 10) + */ + var uri: Uri, + + /** + path : System path of the image, eg storage/emulated/0/camera/CommonsLogo.jpeg + */ + var path: String, + + /** + bucketId : bucketId of folder, eg 540528482 + */ + var bucketId: Long = 0, + + /** + bucketName : name of folder, eg Camera + */ + var bucketName: String = "", + + /** + sha1 : sha1 of original image. + */ + var sha1: String = "" +) : Parcelable { + + /** + default parcelable constructor. + */ + constructor(parcel: Parcel): + this(parcel.readLong(), + parcel.readString()!!, + parcel.readParcelable(Uri::class.java.classLoader)!!, + parcel.readString()!!, + parcel.readLong(), + parcel.readString()!!, + parcel.readString()!! + ) + + /** + Write to parcel method. + */ + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeLong(id) + parcel.writeString(name) + parcel.writeParcelable(uri, flags) + parcel.writeString(path) + parcel.writeLong(bucketId) + parcel.writeString(bucketName) + parcel.writeString(sha1) + } + + /** + * Describe the kinds of special objects contained in this Parcelable + */ + override fun describeContents(): Int { + return 0 + } + + /** + * Indicates whether some other object is "equal to" this one. + */ + override fun equals(other: Any?): Boolean { + + if(javaClass != other?.javaClass) { + return false + } + + other as Image + + if(id != other.id) { + return false; + } + if(name != other.name) { + return false; + } + if(uri != other.uri) { + return false; + } + if(path != other.path) { + return false; + } + if(bucketId != other.bucketId) { + return false; + } + if(bucketName != other.bucketName) { + return false; + } + if(sha1 != other.sha1) { + return false; + } + + return true + } + + /** + * Parcelable companion object + */ + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): Image { + return Image(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt new file mode 100644 index 000000000..0eb4decbd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.customselector.model + +data class Result( + /** + * CallbackStatus : stores the result status + */ + val status:CallbackStatus, + + /** + * Images : images retrieved + */ + val images: ArrayList) { +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt new file mode 100644 index 000000000..11450549c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -0,0 +1,141 @@ +package fr.free.nrw.commons.customselector.ui.adapter + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import fr.free.nrw.commons.R +import fr.free.nrw.commons.customselector.listeners.FolderClickListener +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.ui.selector.ImageLoader + +class FolderAdapter( + /** + * Application context. + */ + context: Context, + + /** + * Folder Click listener for click events. + */ + private val itemClickListener: FolderClickListener +) : RecyclerViewAdapter(context) { + + /** + * Image Loader for loading images. + */ + private val imageLoader = ImageLoader() + + /** + * List of folders. + */ + private var folders: MutableList = mutableListOf() + + /** + * Create view holder, returns View holder item. + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder { + val itemView = inflater.inflate(R.layout.item_custom_selector_folder, parent, false) + return FolderViewHolder(itemView) + } + + /** + * Bind view holder, setup the item view, title, count and click listener + */ + override fun onBindViewHolder(holder: FolderViewHolder, position: Int) { + val folder = folders[position] + val count = folder.images.size + val previewImage = folder.images[0] + holder.name.text = folder.name + holder.count.text= count.toString() + holder.itemView.setOnClickListener{ + itemClickListener.onFolderClick(folder) + } + + //todo load image thumbnail. + } + + /** + * Initialise the data set. + */ + fun init(newFolders: List) { + val oldFolderList: MutableList = folders + val newFolderList = newFolders.toMutableList() + val diffResult = DiffUtil.calculateDiff( + FoldersDiffCallback(oldFolderList, newFolderList) + ) + folders = newFolderList + diffResult.dispatchUpdatesTo(this) + } + + + /** + * returns item count. + */ + override fun getItemCount(): Int { + return folders.size + } + + /** + * Folder view holder. + */ + class FolderViewHolder(itemView:View) : RecyclerView.ViewHolder(itemView) { + + /** + * Folder thumbnail image view. + */ + val image: ImageView = itemView.findViewById(R.id.folder_thumbnail) + + /** + * Folder/album name + */ + val name: TextView = itemView.findViewById(R.id.folder_name) + + /** + * Item count in Folder/Item + */ + val count: TextView = itemView.findViewById(R.id.folder_count) + } + + /** + * DiffUtilCallback. + */ + class FoldersDiffCallback( + var oldFolders: MutableList, + var newFolders: MutableList + ) : DiffUtil.Callback() { + /** + * Returns the size of the old list. + */ + override fun getOldListSize(): Int { + return oldFolders.size + } + + /** + * Returns the size of the new list. + */ + override fun getNewListSize(): Int { + return newFolders.size + } + + /** + * Called by the DiffUtil to decide whether two object represent the same Item. + */ + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldFolders.get(oldItemPosition).bucketId == newFolders.get(newItemPosition).bucketId + } + + /** + * Called by the DiffUtil when it wants to check whether two items have the same data. + * DiffUtil uses this information to detect if the contents of an item has changed. + */ + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldFolders.get(oldItemPosition).equals(newFolders.get(newItemPosition)) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt new file mode 100644 index 000000000..b29910c00 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -0,0 +1,135 @@ +package fr.free.nrw.commons.customselector.ui.adapter + +import android.content.Context +import android.view.ViewGroup +import fr.free.nrw.commons.R +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.Group +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import fr.free.nrw.commons.customselector.listeners.ImageSelectListener +import fr.free.nrw.commons.customselector.model.Image + +class ImageAdapter( + /** + * Application Context. + */ + context: Context, + + /** + * Image select listener for click events on image. + */ + private var imageSelectListener: ImageSelectListener ): + + RecyclerViewAdapter(context) { + + /** + * Currently selected images. + */ + private var selectedImages = arrayListOf() + + /** + * List of all images in adapter. + */ + private var images: ArrayList = ArrayList() + + /** + * create View holder. + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { + val itemView = inflater.inflate(R.layout.item_custom_selector_image,parent, false) + return ImageViewHolder(itemView) + } + + /** + * Bind View holder, load image, selected view, click listeners. + */ + override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { + val image=images[position] + // todo load image thumbnail, set selected view. + holder.itemView.setOnClickListener { + selectOrRemoveImage(image, position) + } + } + + /** + * Handle click event on an image, update counter on images. + */ + private fun selectOrRemoveImage(image:Image, position:Int){ + // todo select the image if not selected and remove it if already selected + } + + /** + * Initialize the data set. + */ + fun init(newImages:List) { + val oldImageList:ArrayList = images + val newImageList:ArrayList = ArrayList(newImages) + val diffResult = DiffUtil.calculateDiff( + ImagesDiffCallback(oldImageList, newImageList) + ) + images = newImageList + diffResult.dispatchUpdatesTo(this) + } + + /** + * Returns the total number of items in the data set held by the adapter. + * + * @return The total number of items in this adapter. + */ + override fun getItemCount(): Int { + return images.size + } + + /** + * Image view holder. + */ + class ImageViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { + val image: ImageView = itemView.findViewById(R.id.image_thumbnail) + val selectedNumber: TextView = itemView.findViewById(R.id.selected_count) + val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group) + val selectedGroup: Group = itemView.findViewById(R.id.selected_group) + } + + /** + * DiffUtilCallback. + */ + class ImagesDiffCallback( + var oldImageList: ArrayList, + var newImageList: ArrayList + ) : DiffUtil.Callback(){ + + /** + * Returns the size of the old list. + */ + override fun getOldListSize(): Int { + return oldImageList.size + } + + /** + * Returns the size of the new list. + */ + override fun getNewListSize(): Int { + return newImageList.size + } + + /** + * Called by the DiffUtil to decide whether two object represent the same Item. + */ + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return newImageList[newItemPosition].id == oldImageList[oldItemPosition].id + } + + /** + * Called by the DiffUtil when it wants to check whether two items have the same data. + * DiffUtil uses this information to detect if the contents of an item has changed. + */ + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldImageList[oldItemPosition].equals(newImageList[newItemPosition]) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/RecyclerViewAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/RecyclerViewAdapter.kt new file mode 100644 index 000000000..75f935302 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/RecyclerViewAdapter.kt @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.customselector.ui.adapter + +import android.content.Context +import android.view.LayoutInflater +import androidx.recyclerview.widget.RecyclerView + +/** + * Generic Recycler view adapter. + */ +abstract class RecyclerViewAdapter(val context: Context): RecyclerView.Adapter() { + val inflater: LayoutInflater = LayoutInflater.from(context) +} \ No newline at end of file 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 new file mode 100644 index 000000000..1ab30f67e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -0,0 +1,121 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.os.Bundle +import android.widget.ImageButton +import android.widget.TextView +import androidx.lifecycle.ViewModelProvider +import fr.free.nrw.commons.R +import fr.free.nrw.commons.customselector.listeners.FolderClickListener +import fr.free.nrw.commons.customselector.listeners.ImageSelectListener +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.theme.BaseActivity +import javax.inject.Inject + +class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectListener { + + /** + * View model. + */ + private lateinit var viewModel: CustomSelectorViewModel + + /** + * View Model Factory. + */ + @Inject lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory + + /** + * onCreate Activity, sets theme, initialises the view model, setup view. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_custom_selector) + + viewModel = ViewModelProvider(this,customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java) + + setupViews() + } + + /** + * Set up view, default folder view. + */ + private fun setupViews() { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, FolderFragment.newInstance()) + .commit() + fetchData() + setUpToolbar() + + // todo : open image fragment depending on the last user visit. + } + + /** + * Start data fetch in view model. + */ + private fun fetchData() { + viewModel.fetchImages() + } + + /** + * Change the title of the toolbar. + */ + private fun changeTitle(title:String) { + val titleText = findViewById(R.id.title) + if(titleText != null) { + titleText.text = title + } + } + + /** + * Set up the toolbar, back listener, done listener. + */ + private fun setUpToolbar() { + val back : ImageButton = findViewById(R.id.back) + back.setOnClickListener { onBackPressed() } + + // todo done listener. + } + + /** + * override on folder click, change the toolbar title on folder click. + */ + override fun onFolderClick(folder: Folder) { + supportFragmentManager.beginTransaction() + .add(R.id.fragment_container, ImageFragment.newInstance(folder.bucketId)) + .addToBackStack(null) + .commit() + changeTitle(folder.name) + } + + /** + * override Selected Images Change, update view model selected images. + */ + override fun onSelectedImagesChanged(selectedImages: ArrayList) { + // todo update selected images in view model. + } + + /** + * Back pressed. + * Change toolbar title. + */ + override fun onBackPressed() { + super.onBackPressed() + val fragment = supportFragmentManager.findFragmentById(R.id.fragment_container) + if(fragment != null && fragment is FolderFragment){ + changeTitle(getString(R.string.custom_selector_title)) + } + } + + + /** + * + * TODO + * Permission check. + * OnDone + * Activity Result. + * + * + */ + + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt new file mode 100644 index 000000000..a5f7cf6e5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener +import fr.free.nrw.commons.customselector.model.CallbackStatus +import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.customselector.model.Result + +class CustomSelectorViewModel(val context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() { + + /** + * Result Live Data + */ + val result = MutableLiveData(Result(CallbackStatus.IDLE, arrayListOf())) + + /** + * Fetch Images and supply to result. + */ + fun fetchImages() { + result.postValue(Result(CallbackStatus.FETCHING, arrayListOf())) + imageFileLoader.abortLoadImage() + imageFileLoader.loadDeviceImages(object: ImageLoaderListener { + + override fun onImageLoaded(images: ArrayList) { + result.postValue(Result(CallbackStatus.SUCCESS, images)) + } + + override fun onFailed(throwable: Throwable) { + result.postValue(Result(CallbackStatus.SUCCESS, arrayListOf())) + } + + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelFactory.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelFactory.kt new file mode 100644 index 000000000..d7a7d42f4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelFactory.kt @@ -0,0 +1,17 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import javax.inject.Inject + +/** + * View Model Factory. + */ +class CustomSelectorViewModelFactory @Inject constructor(val context: Context,val imageFileLoader: ImageFileLoader) : ViewModelProvider.Factory { + + override fun create(modelClass: Class) : CustomSelectorViewModel { + return CustomSelectorViewModel(context,imageFileLoader) as CustomSelectorViewModel + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt new file mode 100644 index 000000000..a3db47571 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -0,0 +1,108 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.GridLayoutManager +import fr.free.nrw.commons.R +import fr.free.nrw.commons.customselector.model.Result +import fr.free.nrw.commons.customselector.listeners.FolderClickListener +import fr.free.nrw.commons.customselector.model.CallbackStatus +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import kotlinx.android.synthetic.main.fragment_custom_selector.* +import kotlinx.android.synthetic.main.fragment_custom_selector.view.* +import javax.inject.Inject + +class FolderFragment : CommonsDaggerSupportFragment() { + + /** + * View Model for images. + */ + private var viewModel: CustomSelectorViewModel? = null + + /** + * View Model Factory. + */ + var customSelectorViewModelFactory: CustomSelectorViewModelFactory? = null + @Inject set + + + /** + * Folder Adapter. + */ + private lateinit var folderAdapter: FolderAdapter + + /** + * Grid Layout Manager for recycler view. + */ + private lateinit var gridLayoutManager: GridLayoutManager + + /** + * Companion newInstance. + */ + companion object{ + fun newInstance(): FolderFragment { + return FolderFragment() + } + } + + /** + * OnCreate Fragment, get the view model. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory!!).get(CustomSelectorViewModel::class.java) + + } + + /** + * OnCreateView. + * Inflate Layout, init adapter, init gridLayoutManager, setUp recycler view, observe the view model for result. + */ + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val root = inflater.inflate(R.layout.fragment_custom_selector, container, false) + folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener) + gridLayoutManager = GridLayoutManager(context, columnCount()) + with(root.selector_rv){ + this.layoutManager = gridLayoutManager + setHasFixedSize(true) + this.adapter = folderAdapter + } + viewModel?.result?.observe(viewLifecycleOwner, Observer { + handleResult(it) + }) + return root + } + + /** + * Handle view model result. + * Get folders from images. + * Load adapter. + */ + private fun handleResult(result: Result) { + if(result.status is CallbackStatus.SUCCESS){ + val folders = arrayListOf() + for( i in 1..12) { + folders.add(Folder(i.toLong(), "Folder$i",result.images)) + } + folderAdapter.init(folders) + folderAdapter.notifyDataSetChanged() + selector_rv.visibility = View.VISIBLE + } + loader.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE + } + + /** + * Return Column count ie span count for grid view adapter. + */ + private fun columnCount(): Int { + return 2 + // todo change column count depending on the orientation of the device. + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt new file mode 100644 index 000000000..738c40e98 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt @@ -0,0 +1,38 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.Context +import android.net.Uri +import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener +import fr.free.nrw.commons.customselector.model.Image + +class ImageFileLoader(val context: Context) { + + /** + * Load Device Images. + */ + fun loadDeviceImages(listener: ImageLoaderListener) { + var tempImage = Image(0, "temp", Uri.parse("http://www.google.com"), "path", 0, "bucket", "1223") + var array: ArrayList = ArrayList() + for(i in 1..100) { + array.add(tempImage) + } + listener.onImageLoaded(array) + + // todo load images from device using cursor. + } + + /** + * Abort loading images. + */ + fun abortLoadImage(){ + //todo Abort loading images. + } + + /** + * + * TODO + * Runnable Thread for image loading. + * Sha1 for image (original image). + * + */ +} \ No newline at end of file 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 new file mode 100644 index 000000000..c22313d65 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -0,0 +1,126 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.GridLayoutManager +import fr.free.nrw.commons.R +import fr.free.nrw.commons.customselector.listeners.ImageSelectListener +import fr.free.nrw.commons.customselector.model.CallbackStatus +import fr.free.nrw.commons.customselector.model.Result +import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import kotlinx.android.synthetic.main.fragment_custom_selector.* +import kotlinx.android.synthetic.main.fragment_custom_selector.view.* +import javax.inject.Inject + +class ImageFragment: CommonsDaggerSupportFragment() { + + /** + * Current bucketId. + */ + private var bucketId: Long? = null + + /** + * View model for images. + */ + private lateinit var viewModel: CustomSelectorViewModel + + /** + * View model Factory. + */ + lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory + @Inject set + + /** + * Image Adapter for recycle view. + */ + private lateinit var imageAdapter: ImageAdapter + + /** + * GridLayoutManager for recycler view. + */ + private lateinit var gridLayoutManager: GridLayoutManager + + + companion object { + + /** + * BucketId args name + */ + const val BUCKET_ID = "BucketId" + + /** + * newInstance from bucketId. + */ + fun newInstance(bucketId: Long): ImageFragment { + val fragment = ImageFragment() + val args = Bundle() + args.putLong(BUCKET_ID, bucketId) + fragment.arguments = args + return fragment + } + } + + /** + * OnCreate + * Get BucketId, view Model. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + bucketId = arguments?.getLong(BUCKET_ID) + viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java) + } + + /** + * OnCreateView + * Init imageAdapter, gridLayoutManger. + * SetUp recycler view. + */ + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + + val root = inflater.inflate(R.layout.fragment_custom_selector, container, false) + imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener) + gridLayoutManager = GridLayoutManager(context,getSpanCount()) + with(root.selector_rv){ + this.layoutManager = gridLayoutManager + setHasFixedSize(true) + this.adapter = imageAdapter + } + + viewModel.result.observe(viewLifecycleOwner, Observer{ + handleResult(it) + }) + + return root + } + + /** + * Handle view model result. + */ + private fun handleResult(result:Result){ + if(result.status is CallbackStatus.SUCCESS){ + val images = result.images + if(images.isNotEmpty()) { + imageAdapter.init(images) + selector_rv.visibility = View.VISIBLE + } + else{ + selector_rv.visibility = View.GONE + } + } + loader.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE + } + + /** + * getSpanCount for GridViewManager. + */ + private fun getSpanCount(): Int { + return 3 + // todo change span count depending on the device orientation and other factos. + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..22da8cbbb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt @@ -0,0 +1,7 @@ +package fr.free.nrw.commons.customselector.ui.selector + +/** + * Image Loader class, loads images, depending on API results. + */ +class ImageLoader { +} \ No newline at end of file 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 index 28c79c612..6381bdc8e 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -8,6 +8,7 @@ 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.explore.depictions.WikidataItemDetailsActivity; import fr.free.nrw.commons.explore.SearchActivity; import fr.free.nrw.commons.notification.NotificationActivity; @@ -34,6 +35,9 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract MainActivity bindContributionsActivity(); + @ContributesAndroidInjector + abstract CustomSelectorActivity bindCustomSelectorActivity(); + @ContributesAndroidInjector abstract SettingsActivity bindSettingsActivity(); 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 1e19de5f4..bca71de98 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 @@ -17,6 +17,7 @@ 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.ui.selector.ImageFileLoader; import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.db.AppDatabase; import fr.free.nrw.commons.kvstore.JsonKvStore; @@ -66,6 +67,11 @@ public class CommonsApplicationModule { this.applicationContext = applicationContext; } + @Provides + public ImageFileLoader providesImageFileLoader() { + return new ImageFileLoader(this.applicationContext); + } + @Provides public Context providesApplicationContext() { return this.applicationContext; diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java index 3757a2147..f255134ea 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -8,6 +8,8 @@ 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.categories.media.CategoriesMediaFragment; @@ -49,6 +51,12 @@ public abstract class FragmentBuilderModule { @ContributesAndroidInjector abstract MediaDetailFragment bindMediaDetailFragment(); + @ContributesAndroidInjector + abstract FolderFragment bindFolderFragment(); + + @ContributesAndroidInjector + abstract ImageFragment bindImageFragment(); + @ContributesAndroidInjector abstract MediaDetailPagerFragment bindMediaDetailPagerFragment(); diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml index f90ca51e2..9587e7c0a 100644 --- a/app/src/main/res/layout/activity_custom_selector.xml +++ b/app/src/main/res/layout/activity_custom_selector.xml @@ -15,10 +15,11 @@ app:layout_constraintTop_toTopOf="parent"/> - + app:layout_constraintTop_toBottomOf="@+id/toolbar_layout"/> \ No newline at end of file diff --git a/app/src/main/res/layout/custom_selector_toolbar.xml b/app/src/main/res/layout/custom_selector_toolbar.xml index 45ebdc923..29b9ab66b 100644 --- a/app/src/main/res/layout/custom_selector_toolbar.xml +++ b/app/src/main/res/layout/custom_selector_toolbar.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" > - @@ -29,7 +31,7 @@ android:text="@string/custom_selector_title" style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" /> - diff --git a/app/src/main/res/layout/fragment_contributions_list.xml b/app/src/main/res/layout/fragment_contributions_list.xml index 49e4e60c7..e9852f49a 100644 --- a/app/src/main/res/layout/fragment_contributions_list.xml +++ b/app/src/main/res/layout/fragment_contributions_list.xml @@ -69,6 +69,19 @@ app:fabSize="mini" app:srcCompat="@drawable/ic_photo_white_24dp" /> + + diff --git a/app/src/main/res/layout/item_custom_selector_folder.xml b/app/src/main/res/layout/item_custom_selector_folder.xml index 4dcc78c1a..077968c6a 100644 --- a/app/src/main/res/layout/item_custom_selector_folder.xml +++ b/app/src/main/res/layout/item_custom_selector_folder.xml @@ -1,8 +1,8 @@ - @@ -24,49 +23,47 @@ android:id="@+id/folder_thumbnail" android:layout_width="match_parent" android:layout_height="match_parent" - android:scaleType="centerCrop"/> + android:background="@color/black" + android:alpha="0.15" + android:scaleType="centerCrop" /> + android:alpha="0.05" /> + app:layout_constraintBottom_toBottomOf="parent"> + android:textSize="16sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" /> + android:textStyle="bold" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintRight_toRightOf="parent" /> @@ -75,7 +72,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="visible" - app:constraint_referenced_ids="folder_details,album_overlay"/> + app:constraint_referenced_ids="folder_details,album_overlay" /> diff --git a/app/src/main/res/layout/item_custom_selector_image.xml b/app/src/main/res/layout/item_custom_selector_image.xml index e3240e90e..eec1eb9d9 100644 --- a/app/src/main/res/layout/item_custom_selector_image.xml +++ b/app/src/main/res/layout/item_custom_selector_image.xml @@ -3,16 +3,16 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" + android:padding="@dimen/dimen_2" android:layout_height="wrap_content"> @@ -38,18 +38,19 @@ android:layout_width="@dimen/dimen_20" android:layout_height="@dimen/dimen_20" app:layout_constraintDimensionRatio="H,1:1" - android:layout_margin="@dimen/dimen_10" - android:gravity="center|center_vertical" - android:includeFontPadding="false" + android:textSize="11sp" android:textStyle="bold" - android:textColor="@color/black" + android:layout_margin="@dimen/dimen_6" + android:gravity="center|center_vertical" + style="@style/TextAppearance.AppCompat.Small" + android:text="12" android:background="@drawable/circle_shape" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> Date: Sun, 13 Jun 2021 16:10:04 +0530 Subject: [PATCH 65/78] [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code --- app/build.gradle | 4 + .../contributions/ContributionController.java | 20 ++++ .../ContributionsListFragment.java | 3 +- .../customselector/helper/ImageHelper.kt | 94 +++++++++++++++++++ .../ui/adapter/FolderAdapter.kt | 4 +- .../customselector/ui/adapter/ImageAdapter.kt | 2 + .../ui/selector/CustomSelectorViewModel.kt | 20 +++- .../ui/selector/FolderFragment.kt | 10 +- .../ui/selector/ImageFileLoader.kt | 94 ++++++++++++++++--- .../ui/selector/ImageFragment.kt | 6 +- .../layout/item_custom_selector_folder.xml | 17 ++-- .../res/layout/item_custom_selector_image.xml | 6 +- 12 files changed, 240 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt diff --git a/app/build.gradle b/app/build.gradle index 68405acce..501595fda 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -142,6 +142,10 @@ dependencies { def work_version = "2.4.0" // Kotlin + coroutines implementation "androidx.work:work-runtime-ktx:$work_version" + + //Glide + implementation 'com.github.bumptech.glide:glide:4.12.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' } android { 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 15c61d836..778b1afdc 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 @@ -8,6 +8,7 @@ import android.content.Context; import android.content.Intent; import androidx.annotation.NonNull; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity; import fr.free.nrw.commons.filepicker.DefaultCallback; import fr.free.nrw.commons.filepicker.FilePicker; import fr.free.nrw.commons.filepicker.FilePicker.ImageSource; @@ -58,6 +59,25 @@ public class ContributionController { initiateGalleryUpload(activity, allowMultipleUploads); } + /** + * Initiate gallery picker with permission + */ + public void initiateCustomGalleryPickWithPermission(final Activity activity) { + boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true); + Intent intent = new Intent(activity,CustomSelectorActivity.class); + if (!useExtStorage) { + activity.startActivity(intent); + return; + } + + PermissionUtils.checkPermissionsAndPerformAction(activity, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + () -> activity.startActivity(intent), + R.string.storage_permission_title, + R.string.write_storage_permission_rationale); + } + + /** * Open chooser for gallery uploads */ 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 4bb12d7b9..6161e06ac 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 @@ -267,8 +267,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl @OnClick(R.id.fab_custom_gallery) void launchCustomSelector(){ - Intent intent = new Intent(getActivity(), CustomSelectorActivity.class); - startActivity(intent); + controller.initiateCustomGalleryPickWithPermission(getActivity()); } private void animateFAB(final boolean isFabOpen) { diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt new file mode 100644 index 000000000..1b676b6e2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt @@ -0,0 +1,94 @@ +package fr.free.nrw.commons.customselector.helper + +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.model.Image +import timber.log.Timber +import java.io.* +import java.math.BigInteger +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import kotlin.collections.ArrayList +import kotlin.collections.LinkedHashMap + +/** + * Image Helper object, includes all the static functions required by custom selector + */ + +object ImageHelper { + + /** + * Returns the list of folders from given image list. + */ + fun folderListFromImages(images: List): List { + val folderMap: MutableMap = LinkedHashMap() + for (image in images) { + val bucketId = image.bucketId + val bucketName = image.bucketName + var folder = folderMap[bucketId] + if (folder == null) { + folder = Folder(bucketId, bucketName) + folderMap[bucketId] = folder + } + folder.images.add(image) + } + return ArrayList(folderMap.values) + } + + /** + * Filters the images based on the given bucketId (folder) + */ + fun filterImages(images: ArrayList, bukketId: Long?): ArrayList { + if (bukketId == null) return images + + val filteredImages = arrayListOf() + for (image in images) { + if (image.bucketId == bukketId) { + filteredImages.add(image) + } + } + return filteredImages + } + + /** + * Generates the file sha1 from file input stream. + */ + fun generateSHA1(`is`: InputStream): String { + val digest: MessageDigest = try { + MessageDigest.getInstance("SHA1") + } catch (e: NoSuchAlgorithmException) { + Timber.e(e, "Exception while getting Digest") + return "" + } + val buffer = ByteArray(8192) + var read: Int + return try { + while (`is`.read(buffer).also { read = it } > 0) { + digest.update(buffer, 0, read) + } + val md5sum = digest.digest() + val bigInt = BigInteger(1, md5sum) + var output = bigInt.toString(16) + output = String.format("%40s", output).replace(' ', '0') + Timber.i("File SHA1: %s", output) + output + } catch (e: IOException) { + Timber.e(e, "IO Exception") + "" + } finally { + try { + `is`.close() + } catch (e: IOException) { + Timber.e(e, "Exception on closing input stream") + } + } + } + + /** + * Gets the file input stream from the file path. + */ + @Throws(FileNotFoundException::class) + fun getFileInputStream(filePath: String?): InputStream { + return FileInputStream(filePath) + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt index 11450549c..5d28a46d1 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -7,6 +7,7 @@ import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.Folder @@ -49,8 +50,9 @@ class FolderAdapter( val folder = folders[position] val count = folder.images.size val previewImage = folder.images[0] + Glide.with(context).load(previewImage.uri).into(holder.image) holder.name.text = folder.name - holder.count.text= count.toString() + holder.count.text = count.toString() holder.itemView.setOnClickListener{ itemClickListener.onFolderClick(folder) } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index b29910c00..53de6de77 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -9,6 +9,7 @@ import android.widget.TextView import androidx.constraintlayout.widget.Group import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.Image @@ -49,6 +50,7 @@ class ImageAdapter( override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { val image=images[position] // todo load image thumbnail, set selected view. + Glide.with(context).load(image.uri).into(holder.image) holder.itemView.setOnClickListener { selectOrRemoveImage(image, position) } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt index a5f7cf6e5..26b8033ba 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt @@ -1,14 +1,20 @@ package fr.free.nrw.commons.customselector.ui.selector import android.content.Context +import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener import fr.free.nrw.commons.customselector.model.CallbackStatus import fr.free.nrw.commons.customselector.model.Image import fr.free.nrw.commons.customselector.model.Result +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel -class CustomSelectorViewModel(val context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() { +class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() { + + private val scope = CoroutineScope(Dispatchers.Main) /** * Result Live Data @@ -20,9 +26,8 @@ class CustomSelectorViewModel(val context: Context,var imageFileLoader: ImageFil */ fun fetchImages() { result.postValue(Result(CallbackStatus.FETCHING, arrayListOf())) - imageFileLoader.abortLoadImage() + scope.cancel() imageFileLoader.loadDeviceImages(object: ImageLoaderListener { - override fun onImageLoaded(images: ArrayList) { result.postValue(Result(CallbackStatus.SUCCESS, images)) } @@ -30,7 +35,14 @@ class CustomSelectorViewModel(val context: Context,var imageFileLoader: ImageFil override fun onFailed(throwable: Throwable) { result.postValue(Result(CallbackStatus.SUCCESS, arrayListOf())) } + },scope) + } - }) + /** + * Clear the coroutine task linked with context. + */ + override fun onCleared() { + scope.cancel() + super.onCleared() } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt index a3db47571..ffacde0e7 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager import fr.free.nrw.commons.R +import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.model.Result import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.CallbackStatus @@ -29,7 +30,7 @@ class FolderFragment : CommonsDaggerSupportFragment() { * View Model Factory. */ var customSelectorViewModelFactory: CustomSelectorViewModelFactory? = null - @Inject set + @Inject set /** @@ -67,7 +68,7 @@ class FolderFragment : CommonsDaggerSupportFragment() { */ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val root = inflater.inflate(R.layout.fragment_custom_selector, container, false) - folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener) + folderAdapter = FolderAdapter(activity!!, activity as FolderClickListener) gridLayoutManager = GridLayoutManager(context, columnCount()) with(root.selector_rv){ this.layoutManager = gridLayoutManager @@ -87,10 +88,7 @@ class FolderFragment : CommonsDaggerSupportFragment() { */ private fun handleResult(result: Result) { if(result.status is CallbackStatus.SUCCESS){ - val folders = arrayListOf() - for( i in 1..12) { - folders.add(Folder(i.toLong(), "Folder$i",result.images)) - } + val folders = ImageHelper.folderListFromImages(result.images) folderAdapter.init(folders) folderAdapter.notifyDataSetChanged() selector_rv.visibility = View.VISIBLE diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt index 738c40e98..95cb8233f 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt @@ -1,26 +1,97 @@ package fr.free.nrw.commons.customselector.ui.selector +import android.content.ContentUris import android.content.Context -import android.net.Uri +import android.provider.MediaStore import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener import fr.free.nrw.commons.customselector.model.Image +import kotlinx.coroutines.* +import java.io.File +import kotlin.coroutines.CoroutineContext -class ImageFileLoader(val context: Context) { +class ImageFileLoader(val context: Context) : CoroutineScope{ /** - * Load Device Images. + * Coroutine context for fetching images. */ - fun loadDeviceImages(listener: ImageLoaderListener) { - var tempImage = Image(0, "temp", Uri.parse("http://www.google.com"), "path", 0, "bucket", "1223") - var array: ArrayList = ArrayList() - for(i in 1..100) { - array.add(tempImage) - } - listener.onImageLoaded(array) + override val coroutineContext: CoroutineContext = Dispatchers.Main - // todo load images from device using cursor. + /** + * Media paramerters required. + */ + private val projection = arrayOf( + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.DATA, + MediaStore.Images.Media.BUCKET_ID, + MediaStore.Images.Media.BUCKET_DISPLAY_NAME) + + /** + * Load Device Images under coroutine. + */ + fun loadDeviceImages(listener: ImageLoaderListener, scope: CoroutineScope) { + launch(Dispatchers.Main) { + withContext(Dispatchers.IO) { + getImages(listener) + } + } } + + /** + * Load the device images using cursor + */ + private fun getImages(listener:ImageLoaderListener) { + val cursor = context.contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, MediaStore.Images.Media.DATE_ADDED + " DESC") + if (cursor == null) { + listener.onFailed(NullPointerException()) + return + } + + val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID) + val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) + val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA) + val bucketIdColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID) + val bucketNameColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME) + + val images = arrayListOf() + if (cursor.moveToFirst()) { + do { + if (Thread.interrupted()) { + listener.onFailed(NullPointerException()) + return + } + val id = cursor.getLong(idColumn) + val name = cursor.getString(nameColumn) + val path = cursor.getString(dataColumn) + val bucketId = cursor.getLong(bucketIdColumn) + val bucketName = cursor.getString(bucketNameColumn) + + val file = + if (path == null || path.isEmpty()) { + null + } else try { + File(path) + } catch (ignored: Exception) { + null + } + + + if (file != null && file.exists()) { + if (id != null && name != null && path != null && bucketId != null && bucketName != null) { + val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) + val image = Image(id, name, uri, path, bucketId, bucketName) + images.add(image) + } + } + + } while (cursor.moveToNext()) + } + cursor.close() + listener.onImageLoaded(images) + } + + /** * Abort loading images. */ @@ -31,7 +102,6 @@ class ImageFileLoader(val context: Context) { /** * * TODO - * Runnable Thread for image loading. * Sha1 for image (original image). * */ 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 c22313d65..f4b5c9934 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 @@ -4,11 +4,11 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager import fr.free.nrw.commons.R +import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.CallbackStatus import fr.free.nrw.commons.customselector.model.Result @@ -34,7 +34,7 @@ class ImageFragment: CommonsDaggerSupportFragment() { * View model Factory. */ lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory - @Inject set + @Inject set /** * Image Adapter for recycle view. @@ -106,7 +106,7 @@ class ImageFragment: CommonsDaggerSupportFragment() { if(result.status is CallbackStatus.SUCCESS){ val images = result.images if(images.isNotEmpty()) { - imageAdapter.init(images) + imageAdapter.init(ImageHelper.filterImages(images,bucketId)) selector_rv.visibility = View.VISIBLE } else{ diff --git a/app/src/main/res/layout/item_custom_selector_folder.xml b/app/src/main/res/layout/item_custom_selector_folder.xml index 077968c6a..6d4df4307 100644 --- a/app/src/main/res/layout/item_custom_selector_folder.xml +++ b/app/src/main/res/layout/item_custom_selector_folder.xml @@ -19,30 +19,30 @@ android:background="@color/white" android:layout_height="match_parent"> - + android:background="@color/black" + android:alpha="0.15" /> diff --git a/app/src/main/res/layout/item_custom_selector_image.xml b/app/src/main/res/layout/item_custom_selector_image.xml index eec1eb9d9..021f463bc 100644 --- a/app/src/main/res/layout/item_custom_selector_image.xml +++ b/app/src/main/res/layout/item_custom_selector_image.xml @@ -19,7 +19,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - @@ -78,7 +78,7 @@ android:id="@+id/uploaded_group" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="visible" + android:visibility="gone" app:constraint_referenced_ids="uploaded_overlay,uploaded_overlay_icon"/> From 6686ad505a7e2ef1b9d5d9f42797c0d35454767d Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Thu, 17 Jun 2021 14:59:27 +0530 Subject: [PATCH 66/78] [GSoC] Image Selection (#4457) * Localisation updates from https://translatewiki.net. * Fixes #4357 After switching to different account, contributions screen shows pictures of previous account (#4421) * Update UploadMediaDetailFragment.java * Update LoginActivity.java Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account * Revert "Update UploadMediaDetailFragment.java" This reverts commit b1b4257f205b022ffaadee9f947357e5fc04c337. Co-authored-by: Obsidian_zero <1198474846@qq.com> * Remove unnecessary whitespace from a message (#4439) * Merge v3.0.1 into master (#4446) * Versioning and changelog for v3.0.0 (#4152) * Versioning for v3.0.0 * Update changelog.md * Handled migration 8-9-10 in BookmarksLocationDao (#4154) * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * Fixes #4179 (#4180) * Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked() * Updated >ContributionsFragment#backButtonClicked() to handle back press properly * Fixes #4179 (#4181) * Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed() * Cherrypick for hotfix3.1 (#4205) * Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. * fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146) * fixed bug:app crashes on viewing review in Review Fragment #4135 * Fixed the issue with back button in contribution tab. (#4177) Co-authored-by: Pratham2305 * Fixed the issue with back navigation button on toolbar in explore tab. (#4175) * Fix (#4148) Issues on theme change * fixed themeChange crashes * fixed comments * Overlooked the title bar Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> * Fixes #4173 (#4396) * Fix #4147 Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390) * Update query to fetch descriptions * Make description added to NearbyResultItem * Make string operations to display description and label in a combined way * Fix reviews, remove long description from list and swap label and description texts * Fix repeated information issue * Fix double information issue * fix style issues * Remove douplicated information * Changes made (#4354) * Remove nonexistent method * Fix #4283 IllegalStateException (#4440) * Fix #4283 IllegalStateException * Fix flickering issue * Versioning for v3.0.1 * Update changelog.md Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> * Localisation updates from https://translatewiki.net. * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Added a feature for editing coordinates (#4418) * not * Place Picker added * Pick location and API call linked * minor warnings resolved * Code conventions followed * issue fixed * Wikitext edited properly * minor modification * Location Picker added * Bottom sheet removed * Location picker fully implemented * credit added * credit added * issues fixed * issues fixed * minor issue fixed * Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451) * Localisation updates from https://translatewiki.net. * Fixes 4344 - Duplicate Uploads (#4442) * Fixes 4344 - Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries. * Update states to IN_PROGRESS before uploads are processed * Image selection added * Forwarded activity result to upload wizard * Initialised xmls, made folder and image item. * xmls done * xmls completed * removed unwanted attribute * Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Image selection added * Forwarded activity result to upload wizard * [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * fixed merge errors * Documented the remaining function Co-authored-by: translatewiki.net Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com> Co-authored-by: Obsidian_zero <1198474846@qq.com> Co-authored-by: Amir E. Aharoni Co-authored-by: Josephine Lim Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> --- .../contributions/ContributionController.java | 10 +-- .../ContributionsListFragment.java | 54 ++++++------- .../customselector/helper/ImageHelper.kt | 30 ++++++- .../customselector/ui/adapter/ImageAdapter.kt | 78 +++++++++++++++++-- .../ui/selector/CustomSelectorActivity.kt | 53 +++++++++---- .../ui/selector/CustomSelectorViewModel.kt | 11 ++- .../free/nrw/commons/data/DBOpenHelper.java | 2 +- .../nrw/commons/filepicker/Constants.java | 1 + .../nrw/commons/filepicker/FilePicker.java | 48 +++++++++++- 9 files changed, 226 insertions(+), 61 deletions(-) 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 778b1afdc..27cef1c0f 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 @@ -8,7 +8,6 @@ import android.content.Context; import android.content.Intent; import androidx.annotation.NonNull; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity; import fr.free.nrw.commons.filepicker.DefaultCallback; import fr.free.nrw.commons.filepicker.FilePicker; import fr.free.nrw.commons.filepicker.FilePicker.ImageSource; @@ -63,16 +62,11 @@ public class ContributionController { * Initiate gallery picker with permission */ public void initiateCustomGalleryPickWithPermission(final Activity activity) { - boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true); - Intent intent = new Intent(activity,CustomSelectorActivity.class); - if (!useExtStorage) { - activity.startActivity(intent); - return; - } + setPickerConfiguration(activity,true); PermissionUtils.checkPermissionsAndPerformAction(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE, - () -> activity.startActivity(intent), + () -> FilePicker.openCustomSelector(activity, 0), R.string.storage_permission_title, R.string.write_storage_permission_rationale); } 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 6161e06ac..1d5a19326 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 @@ -265,34 +265,34 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl }); } - @OnClick(R.id.fab_custom_gallery) - void launchCustomSelector(){ - controller.initiateCustomGalleryPickWithPermission(getActivity()); - } - - private void animateFAB(final boolean isFabOpen) { - this.isFabOpen = !isFabOpen; - if (fabPlus.isShown()) { - if (isFabOpen) { - fabPlus.startAnimation(rotate_backward); - fabCamera.startAnimation(fab_close); - fabGallery.startAnimation(fab_close); - fabCustomGallery.startAnimation(fab_close); - fabCamera.hide(); - fabGallery.hide(); - fabCustomGallery.hide(); - } else { - fabPlus.startAnimation(rotate_forward); - fabCamera.startAnimation(fab_open); - fabGallery.startAnimation(fab_open); - fabCustomGallery.startAnimation(fab_open); - fabCamera.show(); - fabGallery.show(); - fabCustomGallery.show(); - } - this.isFabOpen = !isFabOpen; + @OnClick(R.id.fab_custom_gallery) + void launchCustomSelector(){ + controller.initiateCustomGalleryPickWithPermission(getActivity()); + } + + private void animateFAB(final boolean isFabOpen) { + this.isFabOpen = !isFabOpen; + if (fabPlus.isShown()) { + if (isFabOpen) { + fabPlus.startAnimation(rotate_backward); + fabCamera.startAnimation(fab_close); + fabGallery.startAnimation(fab_close); + fabCustomGallery.startAnimation(fab_close); + fabCamera.hide(); + fabGallery.hide(); + fabCustomGallery.hide(); + } else { + fabPlus.startAnimation(rotate_forward); + fabCamera.startAnimation(fab_open); + fabGallery.startAnimation(fab_open); + fabCustomGallery.startAnimation(fab_open); + fabCamera.show(); + fabGallery.show(); + fabCustomGallery.show(); + } + this.isFabOpen = !isFabOpen; + } } - } /** * Shows welcome message if user has no contributions yet i.e. new user. diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt index 1b676b6e2..9228dc5ac 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt @@ -11,7 +11,7 @@ import kotlin.collections.ArrayList import kotlin.collections.LinkedHashMap /** - * Image Helper object, includes all the static functions required by custom selector + * Image Helper object, includes all the static functions required by custom selector. */ object ImageHelper { @@ -49,6 +49,34 @@ object ImageHelper { return filteredImages } + /** + * getIndex: Returns the index of image in given list. + */ + fun getIndex(list: ArrayList, image: Image): Int { + return list.indexOf(image) + } + + /** + * Gets the list of indices from the master list. + */ + fun getIndexList(list: ArrayList, masterList: ArrayList): ArrayList { + + /** + * TODO + * Can be optimised as masterList is sorted by time. + */ + + val indexes = arrayListOf() + for(image in list) { + val index = getIndex(masterList,image) + if (index == -1) { + continue + } + indexes.add(index) + } + return indexes + } + /** * Generates the file sha1 from file input stream. */ diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index 53de6de77..a38200463 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -10,6 +10,7 @@ import androidx.constraintlayout.widget.Group import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.Image @@ -26,6 +27,16 @@ class ImageAdapter( RecyclerViewAdapter(context) { + /** + * ImageSelectedOrUpdated payload class. + */ + class ImageSelectedOrUpdated + + /** + * ImageUnselected payload class. + */ + class ImageUnselected + /** * Currently selected images. */ @@ -49,18 +60,41 @@ class ImageAdapter( */ override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { val image=images[position] - // todo load image thumbnail, set selected view. + val selectedIndex = ImageHelper.getIndex(selectedImages,image) + val isSelected = selectedIndex != -1 + if(isSelected){ + holder.itemSelected(selectedIndex+1) + } + else { + holder.itemUnselected(); + } Glide.with(context).load(image.uri).into(holder.image) holder.itemView.setOnClickListener { - selectOrRemoveImage(image, position) + selectOrRemoveImage(holder, position) } } /** * Handle click event on an image, update counter on images. */ - private fun selectOrRemoveImage(image:Image, position:Int){ - // todo select the image if not selected and remove it if already selected + private fun selectOrRemoveImage(holder:ImageViewHolder, position:Int){ + val clickedIndex = ImageHelper.getIndex(selectedImages,images[position]) + if (clickedIndex != -1) { + selectedImages.removeAt(clickedIndex) + notifyItemChanged(position,ImageUnselected()) + val indexes = ImageHelper.getIndexList(selectedImages, images) + for (index in indexes) { + notifyItemChanged(index, ImageSelectedOrUpdated()) + } + } else { + /** + * TODO + * Show toast on tapping an uploaded item. + */ + selectedImages.add(images[position]) + notifyItemChanged(position, ImageSelectedOrUpdated()) + } + imageSelectListener.onSelectedImagesChanged(selectedImages) } /** @@ -90,9 +124,39 @@ class ImageAdapter( */ class ImageViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { val image: ImageView = itemView.findViewById(R.id.image_thumbnail) - val selectedNumber: TextView = itemView.findViewById(R.id.selected_count) - val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group) - val selectedGroup: Group = itemView.findViewById(R.id.selected_group) + private val selectedNumber: TextView = itemView.findViewById(R.id.selected_count) + private val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group) + private val selectedGroup: Group = itemView.findViewById(R.id.selected_group) + + /** + * Item selected view. + */ + fun itemSelected(index: Int) { + selectedGroup.visibility = View.VISIBLE + selectedNumber.text = index.toString() + } + + /** + * Item Unselected view. + */ + fun itemUnselected() { + selectedGroup.visibility = View.GONE + } + + /** + * Item Uploaded view. + */ + fun itemUploaded() { + uploadedGroup.visibility = View.VISIBLE + } + + /** + * Item Not Uploaded view. + */ + fun itemNotUploaded() { + uploadedGroup.visibility = View.GONE + } + } /** 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 1ab30f67e..099c89a86 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 @@ -1,5 +1,7 @@ package fr.free.nrw.commons.customselector.ui.selector +import android.app.Activity +import android.content.Intent import android.os.Bundle import android.widget.ImageButton import android.widget.TextView @@ -10,6 +12,7 @@ import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.model.Image import fr.free.nrw.commons.theme.BaseActivity +import java.io.File import javax.inject.Inject class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectListener { @@ -73,7 +76,8 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL val back : ImageButton = findViewById(R.id.back) back.setOnClickListener { onBackPressed() } - // todo done listener. + val done : ImageButton = findViewById(R.id.done) + done.setOnClickListener { onDone() } } /** @@ -91,9 +95,44 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL * override Selected Images Change, update view model selected images. */ override fun onSelectedImagesChanged(selectedImages: ArrayList) { + viewModel.selectedImages.value = selectedImages // todo update selected images in view model. } + /** + * OnDone clicked. + * Get the selected images. Remove any non existent file, forward the data to finish selector. + */ + fun onDone() { + val selectedImages = viewModel.selectedImages.value + if(selectedImages.isNullOrEmpty()) { + finishPickImages(arrayListOf()) + return + } + var i = 0 + while (i < selectedImages.size) { + val path = selectedImages[i].path + val file = File(path) + if (!file.exists()) { + selectedImages.removeAt(i) + i-- + } + i++ + } + finishPickImages(selectedImages) + } + + /** + * finishPickImages, Load the data to the intent and set result. + * Finish the activity. + */ + private fun finishPickImages(images: ArrayList) { + val data = Intent() + data.putParcelableArrayListExtra("Images", images) + setResult(Activity.RESULT_OK, data) + finish() + } + /** * Back pressed. * Change toolbar title. @@ -106,16 +145,4 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL } } - - /** - * - * TODO - * Permission check. - * OnDone - * Activity Result. - * - * - */ - - } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt index 26b8033ba..4f56a808b 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt @@ -1,7 +1,6 @@ package fr.free.nrw.commons.customselector.ui.selector import android.content.Context -import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener @@ -14,10 +13,18 @@ import kotlinx.coroutines.cancel class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() { + /** + * Scope for coroutine task (image fetch). + */ private val scope = CoroutineScope(Dispatchers.Main) /** - * Result Live Data + * Stores selected images. + */ + var selectedImages: MutableLiveData> = MutableLiveData() + + /** + * Result Live Data. */ val result = MutableLiveData(Result(CallbackStatus.IDLE, arrayListOf())) diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java index d5989e1b3..e37f1942b 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java @@ -13,7 +13,7 @@ import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao; public class DBOpenHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "commons.db"; - private static final int DATABASE_VERSION = 15; + private static final int DATABASE_VERSION = 14; public static final String CONTRIBUTIONS_TABLE = "contributions"; private final String DROP_TABLE_STATEMENT="DROP TABLE IF EXISTS %s"; 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 83d838bc2..4b5b91e68 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 @@ -10,6 +10,7 @@ public interface Constants { 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); 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 698e2d51f..6d516abd9 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 @@ -15,6 +15,8 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +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; @@ -51,6 +53,11 @@ public class FilePicker implements Constants { .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, configuration(context).allowsMultiplePickingInGallery()); } + 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); @@ -97,6 +104,14 @@ public class FilePicker implements Constants { activity.startActivityForResult(intent, RequestCodes.PICK_PICTURE_FROM_GALLERY); } + /** + * Opens Custom Selector + */ + public static void openCustomSelector(Activity activity, int type) { + Intent intent = createCustomSelectorIntent(activity, type); + activity.startActivityForResult(intent, RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR); + } + /** * Opens the camera app to pick image clicked by user */ @@ -135,12 +150,15 @@ public class FilePicker implements Constants { if (requestCode == RequestCodes.PICK_PICTURE_FROM_GALLERY || requestCode == RequestCodes.TAKE_PICTURE || requestCode == RequestCodes.CAPTURE_VIDEO || - requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS) { + 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.CAPTURE_VIDEO) { @@ -197,6 +215,32 @@ public class FilePicker implements Constants { } } + 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)); + } + } + + 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; + } + private static void onPictureReturnedFromGallery(Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { try { List files = getFilesFromGalleryPictures(data, activity); @@ -301,7 +345,7 @@ public class FilePicker implements Constants { public enum ImageSource { - GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO + GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO, CUSTOM_SELECTOR } public interface Callbacks { From da35c18b5f1cd35dd9f83d7c59fe7de46bc67b4c Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Tue, 22 Jun 2021 03:53:30 +0530 Subject: [PATCH 67/78] [GSoC] Show uploaded images differently. (#4464) * uploaded images shown differently * Loaded images before query * Handled exceptions, Made ImageLoader injectable, Document and clean code --- .../customselector/helper/ImageHelper.kt | 50 +------ .../ui/adapter/FolderAdapter.kt | 7 +- .../customselector/ui/adapter/ImageAdapter.kt | 24 +++- .../ui/selector/FolderFragment.kt | 7 +- .../ui/selector/ImageFragment.kt | 10 +- .../customselector/ui/selector/ImageLoader.kt | 131 +++++++++++++++++- .../nrw/commons/filepicker/PickedFiles.java | 4 +- .../free/nrw/commons/upload/FileProcessor.kt | 4 +- 8 files changed, 174 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt index 9228dc5ac..0a751d47b 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt @@ -1,15 +1,20 @@ package fr.free.nrw.commons.customselector.helper +import android.content.Context +import com.mapbox.android.core.FileUtils import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.filepicker.Constants import timber.log.Timber import java.io.* import java.math.BigInteger import java.security.MessageDigest import java.security.NoSuchAlgorithmException import kotlin.collections.ArrayList +import kotlin.collections.HashMap import kotlin.collections.LinkedHashMap + /** * Image Helper object, includes all the static functions required by custom selector. */ @@ -68,7 +73,7 @@ object ImageHelper { val indexes = arrayListOf() for(image in list) { - val index = getIndex(masterList,image) + val index = getIndex(masterList, image) if (index == -1) { continue } @@ -76,47 +81,4 @@ object ImageHelper { } return indexes } - - /** - * Generates the file sha1 from file input stream. - */ - fun generateSHA1(`is`: InputStream): String { - val digest: MessageDigest = try { - MessageDigest.getInstance("SHA1") - } catch (e: NoSuchAlgorithmException) { - Timber.e(e, "Exception while getting Digest") - return "" - } - val buffer = ByteArray(8192) - var read: Int - return try { - while (`is`.read(buffer).also { read = it } > 0) { - digest.update(buffer, 0, read) - } - val md5sum = digest.digest() - val bigInt = BigInteger(1, md5sum) - var output = bigInt.toString(16) - output = String.format("%40s", output).replace(' ', '0') - Timber.i("File SHA1: %s", output) - output - } catch (e: IOException) { - Timber.e(e, "IO Exception") - "" - } finally { - try { - `is`.close() - } catch (e: IOException) { - Timber.e(e, "Exception on closing input stream") - } - } - } - - /** - * Gets the file input stream from the file path. - */ - @Throws(FileNotFoundException::class) - fun getFileInputStream(filePath: String?): InputStream { - return FileInputStream(filePath) - } - } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt index 5d28a46d1..fb3e49794 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -11,7 +11,6 @@ import com.bumptech.glide.Glide import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.Folder -import fr.free.nrw.commons.customselector.ui.selector.ImageLoader class FolderAdapter( /** @@ -23,12 +22,8 @@ class FolderAdapter( * Folder Click listener for click events. */ private val itemClickListener: FolderClickListener -) : RecyclerViewAdapter(context) { - /** - * Image Loader for loading images. - */ - private val imageLoader = ImageLoader() +) : RecyclerViewAdapter(context) { /** * List of folders. diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index a38200463..9029e03bc 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -6,6 +6,7 @@ import fr.free.nrw.commons.R import android.view.View import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import androidx.constraintlayout.widget.Group import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -13,6 +14,7 @@ import com.bumptech.glide.Glide import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.customselector.ui.selector.ImageLoader class ImageAdapter( /** @@ -23,7 +25,13 @@ class ImageAdapter( /** * Image select listener for click events on image. */ - private var imageSelectListener: ImageSelectListener ): + private var imageSelectListener: ImageSelectListener, + + /** + * ImageLoader queries images. + */ + private var imageLoader: ImageLoader +): RecyclerViewAdapter(context) { @@ -48,7 +56,7 @@ class ImageAdapter( private var images: ArrayList = ArrayList() /** - * create View holder. + * Create View holder. */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { val itemView = inflater.inflate(R.layout.item_custom_selector_image,parent, false) @@ -69,6 +77,7 @@ class ImageAdapter( holder.itemUnselected(); } Glide.with(context).load(image.uri).into(holder.image) + imageLoader.queryAndSetView(holder,image) holder.itemView.setOnClickListener { selectOrRemoveImage(holder, position) } @@ -87,12 +96,12 @@ class ImageAdapter( notifyItemChanged(index, ImageSelectedOrUpdated()) } } else { - /** - * TODO - * Show toast on tapping an uploaded item. - */ + if(holder.isItemUploaded()){ + Toast.makeText(context,"Already Uploaded image", Toast.LENGTH_SHORT).show() + } else { selectedImages.add(images[position]) notifyItemChanged(position, ImageSelectedOrUpdated()) + } } imageSelectListener.onSelectedImagesChanged(selectedImages) } @@ -150,6 +159,9 @@ class ImageAdapter( uploadedGroup.visibility = View.VISIBLE } + fun isItemUploaded():Boolean { + return uploadedGroup.visibility == View.VISIBLE + } /** * Item Not Uploaded view. */ diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt index ffacde0e7..1d5901c9d 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -12,9 +12,10 @@ import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.model.Result import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.CallbackStatus -import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.media.MediaClient +import fr.free.nrw.commons.upload.FileProcessor import kotlinx.android.synthetic.main.fragment_custom_selector.* import kotlinx.android.synthetic.main.fragment_custom_selector.view.* import javax.inject.Inject @@ -32,7 +33,11 @@ class FolderFragment : CommonsDaggerSupportFragment() { var customSelectorViewModelFactory: CustomSelectorViewModelFactory? = null @Inject set + var fileProcessor: FileProcessor? = null + @Inject set + var mediaClient: MediaClient? = null + @Inject set /** * Folder Adapter. */ 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 f4b5c9934..a2de0ed29 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 @@ -36,6 +36,12 @@ class ImageFragment: CommonsDaggerSupportFragment() { lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory @Inject set + /** + * Image loader for adapter. + */ + var imageLoader: ImageLoader? = null + @Inject set + /** * Image Adapter for recycle view. */ @@ -84,7 +90,7 @@ class ImageFragment: CommonsDaggerSupportFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val root = inflater.inflate(R.layout.fragment_custom_selector, container, false) - imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener) + imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!) gridLayoutManager = GridLayoutManager(context,getSpanCount()) with(root.selector_rv){ this.layoutManager = gridLayoutManager @@ -118,6 +124,8 @@ class ImageFragment: CommonsDaggerSupportFragment() { /** * getSpanCount for GridViewManager. + * + * @return spanCount. */ private fun getSpanCount(): Int { return 3 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 22da8cbbb..a3ae38e34 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 @@ -1,7 +1,136 @@ package fr.free.nrw.commons.customselector.ui.selector +import android.content.Context +import androidx.exifinterface.media.ExifInterface +import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter.ImageViewHolder +import fr.free.nrw.commons.filepicker.PickedFiles +import fr.free.nrw.commons.media.MediaClient +import fr.free.nrw.commons.upload.FileProcessor +import fr.free.nrw.commons.upload.FileUtilsWrapper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.IOException +import java.util.* +import javax.inject.Inject +import kotlin.collections.HashMap + /** * Image Loader class, loads images, depending on API results. */ -class ImageLoader { +class ImageLoader @Inject constructor( + + /** + * MediaClient for SHA1 query. + */ + var mediaClient: MediaClient, + + /** + * FileProcessor to pre-process the file. + */ + var fileProcessor: FileProcessor, + + /** + * File Utils Wrapper for SHA1 + */ + var fileUtilsWrapper: FileUtilsWrapper, + + /** + * Context for coroutine. + */ + val context: Context) { + + /** + * Maps to facilitate image query. + */ + private var mapImageSHA1: HashMap = HashMap() + private var mapHolderImage : HashMap = HashMap() + private var mapResult: HashMap = HashMap() + + /** + * Query image and setUp the view. + */ + fun queryAndSetView(holder: ImageViewHolder, image: Image){ + + /** + * Recycler view uses same view holder, so we can identify the latest query image from holder. + */ + mapHolderImage[holder] = image + holder.itemNotUploaded() + + CoroutineScope(Dispatchers.Main).launch { + var value = false + withContext(Dispatchers.Default) { + if(mapHolderImage[holder] != image) { + // View holder has a new query image, terminate this query. + return@withContext + } + val sha1 = getSHA1(image) + if(mapHolderImage[holder] != image) { + // View holder has a new query image, terminate this query. + return@withContext + } + value = querySHA1(sha1) + } + if(mapHolderImage[holder] == image) { + // View holder and latest query image match, setup the view. + if (value) { + holder.itemUploaded() + } else { + holder.itemNotUploaded() + } + } + } + } + + /** + * Query SHA1, return result if previously queried, otherwise start a new query. + * + * @return Query result. + */ + private fun querySHA1(SHA1: String): Boolean { + if(mapResult[SHA1] != null) { + return mapResult[SHA1]!! + } + val isUploaded = mediaClient.checkFileExistsUsingSha(SHA1).blockingGet() + mapResult[SHA1] = isUploaded + return isUploaded + } + + /** + * Get SHA1, return SHA1 if available, otherwise generate and store the SHA1. + * + * @return sha1 of the image + */ + private fun getSHA1(image: Image): String{ + if(mapImageSHA1[image] != null) { + return mapImageSHA1[image]!! + } + val sha1 = generateModifiedSHA1(image); + mapImageSHA1[image] = sha1; + return sha1; + } + + /** + * Generate Modified SHA1 using present Exif settings. + * + * @return modified sha1 + */ + private fun generateModifiedSHA1(image: Image) : String { + val uploadableFile = PickedFiles.pickedExistingPicture(context, image.uri) + val exifInterface: ExifInterface? = try { + ExifInterface(uploadableFile.file!!) + } catch (e: IOException) { + Timber.e(e) + null + } + fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact()) + val sha1 = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(uploadableFile.filePath)) + uploadableFile.file.delete() + return sha1 + } + } \ 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 index 01e68c940..c5eb101bc 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java @@ -25,7 +25,7 @@ import java.util.UUID; import timber.log.Timber; -class PickedFiles implements Constants { +public class PickedFiles implements Constants { private static String getFolderName(@NonNull Context context) { return FilePicker.configuration(context).getFolderName(); @@ -104,7 +104,7 @@ class PickedFiles implements Constants { }); } - 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 + 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 InputStream pictureInputStream = context.getContentResolver().openInputStream(photoUri); File directory = tempImageDirectory(context); File photoFile = new File(directory, UUID.randomUUID().toString() + "." + getMimeType(context, photoUri)); 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 ff3f63eb8..5ad6952ee 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 @@ -77,7 +77,7 @@ class FileProcessor @Inject constructor( * * @return tags to be redacted */ - private fun getExifTagsToRedact(): Set { + fun getExifTagsToRedact(): Set { val prefManageEXIFTags = defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS) ?: emptySet() val redactTags: Set = @@ -91,7 +91,7 @@ class FileProcessor @Inject constructor( * @param exifInterface ExifInterface object * @param redactTags tags to be redacted */ - private fun redactExifTags(exifInterface: ExifInterface?, redactTags: Set) { + fun redactExifTags(exifInterface: ExifInterface?, redactTags: Set) { compositeDisposable.add( Observable.fromIterable(redactTags) .flatMap { Observable.fromArray(*FileMetadataUtils.getTagsFromPref(it)) } From 9a534a9faa325e997c116637b9b00f2a1f05e765 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Tue, 29 Jun 2021 10:09:00 +0530 Subject: [PATCH 68/78] [GSoC] Added Uploaded status table in room database. (#4476) * added Uploaded status table in room database * Added unique property, minor refractoring * Database intigrated * Database integrated * Handled result null exception * Exceptions handled and refractored * Introduced constants * moved to sealed class * No database insert on network error * queried original image * documented the code * Updated uploaded status on upload success --- .../nrw/commons/contributions/Contribution.kt | 6 +- .../customselector/database/UploadedDao.kt | 88 +++++++++++ .../customselector/database/UploadedStatus.kt | 39 +++++ .../customselector/ui/selector/ImageLoader.kt | 148 ++++++++++++++---- .../fr/free/nrw/commons/db/AppDatabase.kt | 5 +- .../commons/di/CommonsApplicationModule.java | 15 +- .../free/nrw/commons/upload/UploadItem.java | 9 +- .../free/nrw/commons/upload/UploadModel.java | 3 +- .../nrw/commons/upload/worker/UploadWorker.kt | 23 +++ 9 files changed, 297 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatus.kt diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt index b46d1087e..6b895232f 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt @@ -39,7 +39,8 @@ data class Contribution constructor( var dataLength: Long = 0, var dateCreated: Date? = null, var dateModified: Date? = null, - var hasInvalidLocation : Int = 0 + var hasInvalidLocation : Int = 0, + var contentUri: Uri? = null ) : Parcelable { fun completeWith(media: Media): Contribution { @@ -64,7 +65,8 @@ data class Contribution constructor( decimalCoords = item.gpsCoords.decimalCoords, dateCreatedSource = "", depictedItems = depictedItems, - wikidataPlace = from(item.place) + wikidataPlace = from(item.place), + contentUri = item.contentUri ) /** diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt new file mode 100644 index 000000000..d9f2fc55e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt @@ -0,0 +1,88 @@ +package fr.free.nrw.commons.customselector.database + +import androidx.room.* +import kotlinx.coroutines.runBlocking +import java.util.* +import kotlinx.coroutines.* + +/** + * UploadedStatusDao for Custom Selector. + */ +@Dao +abstract class UploadedStatusDao { + + /** + * Insert into uploaded status. + */ + @Insert( onConflict = OnConflictStrategy.REPLACE ) + abstract suspend fun insert(uploadedStatus: UploadedStatus) + + /** + * Update uploaded status entry. + */ + @Update + abstract suspend fun update(uploadedStatus: UploadedStatus) + + /** + * Delete uploaded status entry. + */ + @Delete + abstract suspend fun delete(uploadedStatus: UploadedStatus) + + /** + * Get All entries from the uploaded status table. + */ + @Query("SELECT * FROM uploaded_table") + abstract suspend fun getAll() : List + + /** + * Query uploaded status with image sha1. + */ + @Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ") + abstract suspend 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 + + /** + * Asynchronous insert into uploaded status table. + */ + fun insertUploaded(uploadedStatus: UploadedStatus) = runBlocking { + async { + uploadedStatus.lastUpdated = Calendar.getInstance().time as Date? + insert(uploadedStatus) + }.await() + } + + /** + * Asynchronous delete from uploaded status table. + */ + fun deleteUploaded(uploadedStatus: UploadedStatus) = runBlocking { + async { delete(uploadedStatus) } + } + + /** + * Asynchronous update entry in uploaded status table. + */ + fun updateUploaded(uploadedStatus: UploadedStatus) = runBlocking { + async { update(uploadedStatus) } + } + + /** + * Asynchronous image sha1 query. + */ + fun getUploadedFromImageSHA1(imageSHA1: String) = runBlocking { + async { getFromImageSHA1(imageSHA1) }.await() + } + + /** + * Asynchronous modified image sha1 query. + */ + fun getUploadedFromModifiedImageSHA1(modifiedImageSHA1: String) = runBlocking { + async { getFromModifiedImageSHA1(modifiedImageSHA1) }.await() + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatus.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatus.kt new file mode 100644 index 000000000..93e4a8243 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatus.kt @@ -0,0 +1,39 @@ +package fr.free.nrw.commons.customselector.database + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import java.util.* + +/** + * Entity class for Uploaded Status. + */ +@Entity(tableName = "uploaded_table", indices = [Index(value = ["modifiedImageSHA1"], unique = true)]) +data class UploadedStatus( + + /** + * Original image sha1. + */ + @PrimaryKey + val imageSHA1 : String, + + /** + * Modified image sha1 (after exif changes). + */ + val modifiedImageSHA1 : String, + + /** + * imageSHA1 query result from API. + */ + var imageResult : Boolean, + + /** + * modifiedImageSHA1 query result from API. + */ + var modifiedImageResult : Boolean, + + /** + * lastUpdated for data validation. + */ + var lastUpdated : Date? = null +) 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 a3ae38e34..f2d4d5709 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 @@ -1,7 +1,10 @@ package fr.free.nrw.commons.customselector.ui.selector import android.content.Context +import android.net.Uri import androidx.exifinterface.media.ExifInterface +import fr.free.nrw.commons.customselector.database.UploadedStatus +import fr.free.nrw.commons.customselector.database.UploadedStatusDao import fr.free.nrw.commons.customselector.model.Image import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter.ImageViewHolder import fr.free.nrw.commons.filepicker.PickedFiles @@ -14,6 +17,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.io.IOException +import java.net.UnknownHostException import java.util.* import javax.inject.Inject import kotlin.collections.HashMap @@ -38,22 +42,28 @@ class ImageLoader @Inject constructor( */ var fileUtilsWrapper: FileUtilsWrapper, + /** + * UploadedStatusDao for cache query. + */ + var uploadedStatusDao: UploadedStatusDao, + /** * Context for coroutine. */ - val context: Context) { + val context: Context +) { /** * Maps to facilitate image query. */ - private var mapImageSHA1: HashMap = HashMap() - private var mapHolderImage : HashMap = HashMap() - private var mapResult: HashMap = HashMap() + private var mapImageSHA1: HashMap = HashMap() + private var mapHolderImage : HashMap = HashMap() + private var mapResult: HashMap = HashMap() /** * Query image and setUp the view. */ - fun queryAndSetView(holder: ImageViewHolder, image: Image){ + fun queryAndSetView(holder: ImageViewHolder, image: Image) { /** * Recycler view uses same view holder, so we can identify the latest query image from holder. @@ -62,26 +72,46 @@ class ImageLoader @Inject constructor( holder.itemNotUploaded() CoroutineScope(Dispatchers.Main).launch { - var value = false + + var result : Result = Result.NOTFOUND withContext(Dispatchers.Default) { - if(mapHolderImage[holder] != image) { - // View holder has a new query image, terminate this query. - return@withContext + + if (mapHolderImage[holder] == image) { + val imageSHA1 = getImageSHA1(image.uri) + val uploadedStatus = uploadedStatusDao.getUploadedFromImageSHA1(imageSHA1) + + val sha1 = uploadedStatus?.let { + result = getResultFromUploadedStatus(uploadedStatus) + uploadedStatus.modifiedImageSHA1 + } ?: run { + if(mapHolderImage[holder] == image) { + getSHA1(image) + } else { + "" + } + } + + if (mapHolderImage[holder] == image && + result in arrayOf(Result.NOTFOUND, Result.INVALID) && + sha1.isNotEmpty()) { + // Query original image. + result = querySHA1(imageSHA1) + if( result is Result.TRUE ) { + // Original image found. + insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false) + } + else { + // Original image not found, query modified image. + result = querySHA1(sha1) + if (result != Result.ERROR) { + insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE) + } + } + } } - val sha1 = getSHA1(image) - if(mapHolderImage[holder] != image) { - // View holder has a new query image, terminate this query. - return@withContext - } - value = querySHA1(sha1) } if(mapHolderImage[holder] == image) { - // View holder and latest query image match, setup the view. - if (value) { - holder.itemUploaded() - } else { - holder.itemNotUploaded() - } + if (result is Result.TRUE) holder.itemUploaded() else holder.itemNotUploaded() } } } @@ -91,13 +121,26 @@ class ImageLoader @Inject constructor( * * @return Query result. */ - private fun querySHA1(SHA1: String): Boolean { - if(mapResult[SHA1] != null) { - return mapResult[SHA1]!! + private fun querySHA1(SHA1: String): Result { + mapResult[SHA1]?.let{ + return it + } + var result : Result = Result.FALSE + try { + if (mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()) { + mapResult[SHA1] = Result.TRUE + result = Result.TRUE + } + } catch (e: Exception) { + if (e is UnknownHostException) { + // Handle no network connection. + Timber.e(e, "Network Connection Error") + } + result = Result.ERROR + e.printStackTrace() + } finally { + return result } - val isUploaded = mediaClient.checkFileExistsUsingSha(SHA1).blockingGet() - mapResult[SHA1] = isUploaded - return isUploaded } /** @@ -105,15 +148,45 @@ class ImageLoader @Inject constructor( * * @return sha1 of the image */ - private fun getSHA1(image: Image): String{ - if(mapImageSHA1[image] != null) { - return mapImageSHA1[image]!! + private fun getSHA1(image: Image): String { + mapImageSHA1[image]?.let{ + return it } val sha1 = generateModifiedSHA1(image); mapImageSHA1[image] = sha1; return sha1; } + /** + * Insert into uploaded status table. + */ + private fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){ + uploadedStatusDao.insertUploaded(UploadedStatus(imageSha1, modifiedImageSha1, imageResult, modifiedImageResult)) + } + + /** + * Get image sha1 from uri, used to retrieve the original image sha1. + */ + private fun getImageSHA1(uri: Uri): String { + return fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri)) + } + + /** + * Get result data from database. + */ + private fun getResultFromUploadedStatus(uploadedStatus: UploadedStatus): Result { + if (uploadedStatus.imageResult || uploadedStatus.modifiedImageResult) { + return Result.TRUE + } else { + uploadedStatus.lastUpdated?.let { + if (it.date >= Calendar.getInstance().time.date - INVALIDATE_DAY_COUNT) { + return Result.FALSE + } + } + } + return Result.INVALID + } + /** * Generate Modified SHA1 using present Exif settings. * @@ -133,4 +206,19 @@ class ImageLoader @Inject constructor( return sha1 } + /** + * Sealed Result class. + */ + sealed class Result { + object TRUE : Result() + object FALSE : Result() + object INVALID : Result() + object NOTFOUND : Result() + object ERROR : Result() + } + + companion object { + const val INVALIDATE_DAY_COUNT: Int = 7 + } + } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt index c408ea5f7..7f6ea7027 100644 --- a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt +++ b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt @@ -5,6 +5,8 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.ContributionDao +import fr.free.nrw.commons.customselector.database.UploadedStatus +import fr.free.nrw.commons.customselector.database.UploadedStatusDao import fr.free.nrw.commons.upload.depicts.Depicts import fr.free.nrw.commons.upload.depicts.DepictsDao @@ -12,9 +14,10 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao * The database for accessing the respective DAOs * */ -@Database(entities = [Contribution::class, Depicts::class], version = 8, exportSchema = false) +@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class], version = 8, exportSchema = false) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun contributionDao(): ContributionDao abstract fun DepictsDao(): DepictsDao; + abstract fun UploadedStatusDao(): UploadedStatusDao; } 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 bca71de98..7d9c061ff 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 @@ -17,6 +17,7 @@ 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.UploadedStatusDao; import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader; import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.db.AppDatabase; @@ -68,8 +69,8 @@ public class CommonsApplicationModule { } @Provides - public ImageFileLoader providesImageFileLoader() { - return new ImageFileLoader(this.applicationContext); + public ImageFileLoader providesImageFileLoader(Context context) { + return new ImageFileLoader(context); } @Provides @@ -250,13 +251,21 @@ public class CommonsApplicationModule { } /** - * Get the reference of DepictsDao class + * 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(); + } + @Provides public ContentResolver providesContentResolver(Context context){ return context.getContentResolver(); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java index 0487fd87f..bed3e3454 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java @@ -23,6 +23,7 @@ public class UploadItem { private final String createdTimestampSource; private final BehaviorSubject imageQuality; private boolean hasInvalidLocation; + private final Uri contentUri; @SuppressLint("CheckResult") @@ -31,7 +32,8 @@ public class UploadItem { final ImageCoordinates gpsCoords, final Place place, final long createdTimestamp, - final String createdTimestampSource) { + final String createdTimestampSource, + final Uri contentUri) { this.createdTimestampSource = createdTimestampSource; uploadMediaDetails = new ArrayList<>(Collections.singletonList(new UploadMediaDetail())); this.place = place; @@ -39,6 +41,7 @@ public class UploadItem { this.mimeType = mimeType; this.gpsCoords = gpsCoords; this.createdTimestamp = createdTimestamp; + this.contentUri = contentUri; imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT); } @@ -66,8 +69,10 @@ public class UploadItem { return imageQuality.getValue(); } + public Uri getContentUri() { return contentUri; } + public void setImageQuality(final int imageQuality) { - this.imageQuality.onNext(imageQuality); + this.imageQuality.onNext(imageQuality); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java index cf72fa5d6..1d1b7117f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java @@ -106,7 +106,8 @@ public class UploadModel { final UploadItem uploadItem = new UploadItem( Uri.parse(uploadableFile.getFilePath()), uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate, - createdTimestampSource); + createdTimestampSource, + uploadableFile.getContentUri()); if (place != null) { uploadItem.getUploadMediaDetails().set(0, new UploadMediaDetail(place)); } 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 ad571f622..5c1ee6846 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 @@ -18,8 +18,11 @@ import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.contributions.ChunkInfo import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.ContributionDao +import fr.free.nrw.commons.customselector.database.UploadedStatus +import fr.free.nrw.commons.customselector.database.UploadedStatusDao import fr.free.nrw.commons.di.ApplicationlessInjection import fr.free.nrw.commons.media.MediaClient +import fr.free.nrw.commons.upload.FileUtilsWrapper import fr.free.nrw.commons.upload.StashUploadState import fr.free.nrw.commons.upload.UploadClient import fr.free.nrw.commons.upload.UploadResult @@ -49,12 +52,18 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : @Inject lateinit var contributionDao: ContributionDao + @Inject + lateinit var uploadedStatusDao: UploadedStatusDao + @Inject lateinit var uploadClient: UploadClient @Inject lateinit var mediaClient: MediaClient + @Inject + lateinit var fileUtilsWrapper: FileUtilsWrapper + private val PROCESSING_UPLOADS_NOTIFICATION_TAG = BuildConfig.APPLICATION_ID + " : upload_tag" private val PROCESSING_UPLOADS_NOTIFICATION_ID = 101 @@ -387,6 +396,20 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .blockingGet() contributionFromUpload.dateModified=Date() contributionDao.deleteAndSaveContribution(contribution, contributionFromUpload) + + // Upload success, save to uploaded status. + saveIntoUploadedStatus(contribution) + } + + /** + * Save to uploadedStatusDao. + */ + private fun saveIntoUploadedStatus(contribution: Contribution) { + contribution.contentUri?.let { + val imageSha1 = fileUtilsWrapper.getSHA1(appContext.contentResolver.openInputStream(it)) + val modifiedSha1 = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(contribution.localUri?.path)) + uploadedStatusDao.insertUploaded(UploadedStatus(imageSha1, modifiedSha1, imageSha1 == modifiedSha1, true)); + } } private fun findUniqueFileName(fileName: String): String { From f4ff853881bc12f9b0e11794ff0262aaf92aaff1 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Wed, 30 Jun 2021 11:53:04 +0530 Subject: [PATCH 69/78] Image Helper test (#4485) --- .../customselector/helper/ImageHelperTest.kt | 55 +++++++++++++++++++ .../nrw/commons/filepicker/FilePickerTest.kt | 10 ++++ 2 files changed, 65 insertions(+) create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/helper/ImageHelperTest.kt diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/helper/ImageHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/helper/ImageHelperTest.kt new file mode 100644 index 000000000..2fe382368 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/helper/ImageHelperTest.kt @@ -0,0 +1,55 @@ +package fr.free.nrw.commons.customselector.helper + +import android.net.Uri +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.model.Image +import org.junit.jupiter.api.Assertions.* + +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock + +/** + * Custom Selector Image Helper Test + */ +internal class ImageHelperTest { + + var uri: Uri = mock(Uri::class.java) + private val folderImage1 = Image(1, "image1", uri, "abc/abc", 1, "bucket1") + private val folderImage2 = Image(2, "image1", uri, "xyz/xyz", 2, "bucket2") + private val mockImageList = ArrayList(listOf(folderImage1, folderImage2)) + private val folderImageList1 = ArrayList(listOf(folderImage1)) + private val folderImageList2 = ArrayList(listOf(folderImage2)) + + /** + * Test folder list from images. + */ + @Test + fun folderListFromImages() { + val folderList = ArrayList(listOf(Folder(1, "bucket1", folderImageList1), Folder(2, "bucket2", folderImageList2))) + assertEquals(folderList, ImageHelper.folderListFromImages(mockImageList)) + } + + /** + * Test filter images. + */ + @Test + fun filterImages() { + assertEquals(folderImageList1, ImageHelper.filterImages(mockImageList, 1)) + } + + /** + * Test get index from image list. + */ + @Test + fun getIndex() { + assertEquals(1,ImageHelper.getIndex(mockImageList, folderImage2)) + } + + /** + * Test get index list. + */ + @Test + fun getIndexList() { + assertEquals(ArrayList(listOf(0)), ImageHelper.getIndexList(mockImageList, folderImageList2)) + } +} \ No newline at end of file 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 b9712df04..ae841cd1c 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 @@ -54,4 +54,14 @@ class FilePickerTest { verify(activity).startActivityForResult(ArgumentMatchers.anyObject(), requestCodeCaptor?.capture()?.toInt()!!) assertEquals(requestCodeCaptor?.value, RequestCodes.TAKE_PICTURE) } + + @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.anyObject(), requestCodeCaptor?.capture()?.toInt()!!) + assertEquals(requestCodeCaptor?.value, RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR) + } } \ No newline at end of file From be522b1c792436b9898c30d3e3c19dfd8b67d45f Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Sat, 3 Jul 2021 11:32:54 +0530 Subject: [PATCH 70/78] [GSoC] Adapter Tests (#4488) * Added FolderAdapterTest * Image Adapter Test --- .../ui/adapter/FolderAdapterTest.kt | 81 ++++++++++++ .../ui/adapter/ImageAdapterTest.kt | 121 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapterTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapterTest.kt new file mode 100644 index 000000000..6a6271b96 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapterTest.kt @@ -0,0 +1,81 @@ +package fr.free.nrw.commons.customselector.ui.adapter + +import fr.free.nrw.commons.R +import android.content.Context +import android.net.Uri +import android.view.LayoutInflater +import android.view.View +import android.widget.GridLayout +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.customselector.listeners.FolderClickListener +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Custom Selector Folder Adapter Test. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +class FolderAdapterTest { + + private var uri: Uri = Mockito.mock(Uri::class.java) + private lateinit var activity: CustomSelectorActivity + private lateinit var folderAdapter: FolderAdapter + private lateinit var image: Image + private lateinit var folder: Folder + private lateinit var folderList: ArrayList + + @Before + @Throws(Exception::class) + fun setUp() { + activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).get() + image = Image(1, "image", uri, "abc/abc", 1, "bucket1") + folder = Folder(1, "bucket1", ArrayList(listOf(image))) + folderList = ArrayList(listOf(folder)) + folderAdapter = FolderAdapter(activity, activity as FolderClickListener) + } + + /** + * Test on create view holder. + */ + @Test + fun onCreateViewHolder() { + folderAdapter.createViewHolder(GridLayout(activity), 0) + } + + /** + * Test on bind view holder. + */ + @Test + fun onBindViewHolder() { + folderAdapter.init(folderList) + val inflater = activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val listItemView: View = inflater.inflate(R.layout.item_custom_selector_folder, null, false) + folderAdapter.onBindViewHolder(FolderAdapter.FolderViewHolder(listItemView), 0) + } + + /** + * Test init. + */ + @Test + fun init() { + folderAdapter.init(folderList) + } + + /** + * Test get item count. + */ + @Test + fun getItemCount() { + assertEquals(0, folderAdapter.itemCount) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt new file mode 100644 index 000000000..8de08a2d3 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt @@ -0,0 +1,121 @@ +package fr.free.nrw.commons.customselector.ui.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.widget.GridLayout +import fr.free.nrw.commons.R +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.customselector.listeners.ImageSelectListener +import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity +import fr.free.nrw.commons.customselector.ui.selector.ImageLoader +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions +import org.junit.runner.RunWith +import org.mockito.* +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.lang.reflect.Field + +/** + * Custom Selector image adapter test. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +class ImageAdapterTest { + @Mock + private lateinit var image: Image + @Mock + private lateinit var imageLoader: ImageLoader + @Mock + private lateinit var imageSelectListener: ImageSelectListener + + private lateinit var activity: CustomSelectorActivity + private lateinit var imageAdapter: ImageAdapter + private lateinit var images : ArrayList + private lateinit var holder: ImageAdapter.ImageViewHolder + private lateinit var selectedImageField: Field + + /** + * Set up variables. + */ + @Before + @Throws(Exception::class) + fun setUp() { + MockitoAnnotations.initMocks(this) + activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).get() + imageAdapter = ImageAdapter(activity, imageSelectListener, imageLoader) + images = ArrayList() + + val inflater = activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val listItemView: View = inflater.inflate(R.layout.item_custom_selector_image, null, false) + holder = ImageAdapter.ImageViewHolder(listItemView) + + selectedImageField = imageAdapter.javaClass.getDeclaredField("selectedImages") + selectedImageField.isAccessible = true + } + + /** + * Test on create view holder. + */ + @Test + fun onCreateViewHolder() { + imageAdapter.createViewHolder(GridLayout(activity), 0) + } + + /** + * Test on bind view holder. + */ + @Test + fun onBindViewHolder() { + // Parameters. + images.add(image) + imageAdapter.init(images) + + // Test conditions. + imageAdapter.onBindViewHolder(holder, 0) + selectedImageField.set(imageAdapter, images) + imageAdapter.onBindViewHolder(holder, 0) + } + + /** + * Test init. + */ + @Test + fun init() { + imageAdapter.init(images) + } + + /** + * Test private function select or remove image. + */ + @Test + fun selectOrRemoveImage() { + // Access function + val func = imageAdapter.javaClass.getDeclaredMethod("selectOrRemoveImage", ImageAdapter.ImageViewHolder::class.java, Int::class.java) + func.isAccessible = true + + // Parameters + images.addAll(listOf(image, image)) + imageAdapter.init(images) + + // Test conditions + holder.itemUploaded() + func.invoke(imageAdapter, holder, 0) + holder.itemNotUploaded() + func.invoke(imageAdapter, holder, 0) + selectedImageField.set(imageAdapter, images) + func.invoke(imageAdapter, holder, 1) + } + + /** + * Test get item count. + */ + @Test + fun getItemCount() { + Assertions.assertEquals(0, imageAdapter.itemCount) + } +} \ No newline at end of file From 4382cdaf14472e231f36f7f58c77b6e416cf5f10 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Wed, 14 Jul 2021 16:35:08 +0530 Subject: [PATCH 71/78] [GSoC] Master rebase. (#4505) * Localisation updates from https://translatewiki.net. * Fixes #4357 After switching to different account, contributions screen shows pictures of previous account (#4421) * Update UploadMediaDetailFragment.java * Update LoginActivity.java Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account * Revert "Update UploadMediaDetailFragment.java" This reverts commit b1b4257f205b022ffaadee9f947357e5fc04c337. Co-authored-by: Obsidian_zero <1198474846@qq.com> * Remove unnecessary whitespace from a message (#4439) * Merge v3.0.1 into master (#4446) * Versioning and changelog for v3.0.0 (#4152) * Versioning for v3.0.0 * Update changelog.md * Handled migration 8-9-10 in BookmarksLocationDao (#4154) * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * Fixes #4179 (#4180) * Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked() * Updated >ContributionsFragment#backButtonClicked() to handle back press properly * Fixes #4179 (#4181) * Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed() * Cherrypick for hotfix3.1 (#4205) * Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. * fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146) * fixed bug:app crashes on viewing review in Review Fragment #4135 * Fixed the issue with back button in contribution tab. (#4177) Co-authored-by: Pratham2305 * Fixed the issue with back navigation button on toolbar in explore tab. (#4175) * Fix (#4148) Issues on theme change * fixed themeChange crashes * fixed comments * Overlooked the title bar Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> * Fixes #4173 (#4396) * Fix #4147 Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390) * Update query to fetch descriptions * Make description added to NearbyResultItem * Make string operations to display description and label in a combined way * Fix reviews, remove long description from list and swap label and description texts * Fix repeated information issue * Fix double information issue * fix style issues * Remove douplicated information * Changes made (#4354) * Remove nonexistent method * Fix #4283 IllegalStateException (#4440) * Fix #4283 IllegalStateException * Fix flickering issue * Versioning for v3.0.1 * Update changelog.md Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> * Localisation updates from https://translatewiki.net. * Added a feature for editing coordinates (#4418) * not * Place Picker added * Pick location and API call linked * minor warnings resolved * Code conventions followed * issue fixed * Wikitext edited properly * minor modification * Location Picker added * Bottom sheet removed * Location picker fully implemented * credit added * credit added * issues fixed * issues fixed * minor issue fixed * Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451) * Localisation updates from https://translatewiki.net. * Fixes 4344 - Duplicate Uploads (#4442) * Fixes 4344 - Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries. * Update states to IN_PROGRESS before uploads are processed * Fixes #3694 Pre-select places as depictions (#4452) * WikidataEditService: stop automatically adding WikidataPlace as a depiction When the user initiates the upload process from Nearby and also manually adds the place as a depiction, the depiction is added twice. Since this behavior is invisible to the user, it is being removed in preparation for auto-selecting the place as a depiction on the DepictsFragment screen. * DepictsFragment: auto-select place as a depiction Pass the Place reference from UploadActivity to DepictsFragment and select the corresponding DepictedItem. Using the place id, retrieve the corresponding Entity to create and select a DepictedItem. * UploadRepository: use Place from UploadItem to obtain a DepictedItem Instead of passing a Place object from UploadActivity to DepictsFragment and then passing the Place object up the chain to obtain and select a DepictedItem, retrieve the Place object directly within UploadRepository * DepictsFragment: select Place depiction when fragment becomes visible * UploadDepictsAdapter: make adapter aware of selection state Update selection state when recycled list items are automatically selected, preventing automatically selected items from appearing as unselected until they are forced to re-bind (i.e. after scrolling) * DepictsFragment: pre-select place depictions for all UploadItems If several images are selected and set to different places, pre-select all place depictions to reinforce the intended upload workflow philosophy (i.e. all images in a set are intended to be from/of the same place). See discussion in commons-app/apps-android-commons#3694 * DepictsFragment: scroll to the top every time list is updated * Typo fixes (#4461) * Fixed typo on class documentation of TextUtils * corrected comma placement in documentation * Fixed typos in comments * fix-issue-4424 (#4445) Co-authored-by: Pratham2305 * fix edit categories ui (#4414) Co-authored-by: Pratham2305 * Fix doom version issue (#4463) * Update db version * DBOpenHelper version update * fix :Back Pressed Event not work in Explore tab when user not login (#4404) * fix :Back Pressed Event not work in Explore tab * minor changes * fix :Upload count or number of contribution does not get updated when media is successful uploaded (#4399) * * fix:Number of Contributions not updated * Add javadocs * minor changes * made minor changes * String was nonsense and untranslatible, fixed (#4466) * Ability to show captions and descriptions in all entered languages (#4355) * implement Ability to show captions and descriptions in all entered languages *Add Javadoc * handle Back event of fragment(mediaDetailFragment) * fix minor bugs * add internationalization * revert previous changes * fix visibility bug * resolve conflict * Fixes #4437 - Changed indentation on files with 2 spaces to 4 spaces (#4462) * Edited Project.xml to make indent size 4 * Changed files with 2 space indentation to use 4 space indentation * Edited Project.xml to make indent size 4 * changed files with 2 space indent to 4 space indent * fix :Back Pressed Event not work in Explore tab when user not login (#4404) * fix :Back Pressed Event not work in Explore tab * minor changes * fix :Upload count or number of contribution does not get updated when media is successful uploaded (#4399) * * fix:Number of Contributions not updated * Add javadocs * minor changes * made minor changes * String was nonsense and untranslatible, fixed (#4466) * Ability to show captions and descriptions in all entered languages (#4355) * implement Ability to show captions and descriptions in all entered languages *Add Javadoc * handle Back event of fragment(mediaDetailFragment) * fix minor bugs * add internationalization * revert previous changes * fix visibility bug * resolve conflict Co-authored-by: Prince kushwaha <65972015+Prince-kushwaha@users.noreply.github.com> Co-authored-by: neslihanturan * Use more understandable strings (#4470) * Fix #3792 Missing Column Issue (#4468) * Fix Missing Column Issue * Fix tests * Add UploadCategoriesFragment Unit Tests (#4473) * Panorama (#4467) * panoramic images fixed * made requested changes * Minor refactoring Co-authored-by: Aditya Srivastava * Localisation updates from https://translatewiki.net. * Main activity title is sometimes "Contributions", sometimes "Commons" (#4472) Fixes #4438 Replace == with equals() in onRestoreInstanceState * Localisation updates from https://translatewiki.net. * caption and description copyable (#4481) * Removed next button in quiz (#4382) * issues resolved * modification done * warning fixed * issues resolved * Button added * don't know function added * Button added * modification done * modification done * Localisation updates from https://translatewiki.net. * Added option to show and modify location while uploading (#4475) * initial commit * Everything done * minor modification * minor modification * Issues fixed * minor modifications * issue fixed * Issues fixed * Tutorial removed from log out state (#4479) * tutorial removed from log out state * Issue removed * Update changelog.md * Versioning for v3.0.2 * Fix #4482 (#4484) * Fix crash when image resolution is very high (#4483) * Localisation updates from https://translatewiki.net. * Add Contributions Fragment Unit Tests (#4490) * Fix Tests Errors (#4491) * Add UploadMediaDetailFragment Unit Tests (#4492) * Localisation updates from https://translatewiki.net. * Localisation updates from https://translatewiki.net. * Localisation updates from https://translatewiki.net. * Initialised xmls, made folder and image item. * xmls done * xmls completed * removed unwanted attribute * Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable * [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * [GSoC] Image Selection (#4457) * Localisation updates from https://translatewiki.net. * Fixes #4357 After switching to different account, contributions screen shows pictures of previous account (#4421) * Update UploadMediaDetailFragment.java * Update LoginActivity.java Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account * Revert "Update UploadMediaDetailFragment.java" This reverts commit b1b4257f205b022ffaadee9f947357e5fc04c337. Co-authored-by: Obsidian_zero <1198474846@qq.com> * Remove unnecessary whitespace from a message (#4439) * Merge v3.0.1 into master (#4446) * Versioning and changelog for v3.0.0 (#4152) * Versioning for v3.0.0 * Update changelog.md * Handled migration 8-9-10 in BookmarksLocationDao (#4154) * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * Fixes #4179 (#4180) * Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked() * Updated >ContributionsFragment#backButtonClicked() to handle back press properly * Fixes #4179 (#4181) * Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed() * Cherrypick for hotfix3.1 (#4205) * Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. * fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146) * fixed bug:app crashes on viewing review in Review Fragment #4135 * Fixed the issue with back button in contribution tab. (#4177) Co-authored-by: Pratham2305 * Fixed the issue with back navigation button on toolbar in explore tab. (#4175) * Fix (#4148) Issues on theme change * fixed themeChange crashes * fixed comments * Overlooked the title bar Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> * Fixes #4173 (#4396) * Fix #4147 Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390) * Update query to fetch descriptions * Make description added to NearbyResultItem * Make string operations to display description and label in a combined way * Fix reviews, remove long description from list and swap label and description texts * Fix repeated information issue * Fix double information issue * fix style issues * Remove douplicated information * Changes made (#4354) * Remove nonexistent method * Fix #4283 IllegalStateException (#4440) * Fix #4283 IllegalStateException * Fix flickering issue * Versioning for v3.0.1 * Update changelog.md Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> * Localisation updates from https://translatewiki.net. * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Added a feature for editing coordinates (#4418) * not * Place Picker added * Pick location and API call linked * minor warnings resolved * Code conventions followed * issue fixed * Wikitext edited properly * minor modification * Location Picker added * Bottom sheet removed * Location picker fully implemented * credit added * credit added * issues fixed * issues fixed * minor issue fixed * Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451) * Localisation updates from https://translatewiki.net. * Fixes 4344 - Duplicate Uploads (#4442) * Fixes 4344 - Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries. * Update states to IN_PROGRESS before uploads are processed * Image selection added * Forwarded activity result to upload wizard * Initialised xmls, made folder and image item. * xmls done * xmls completed * removed unwanted attribute * Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Image selection added * Forwarded activity result to upload wizard * [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * fixed merge errors * Documented the remaining function Co-authored-by: translatewiki.net Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com> Co-authored-by: Obsidian_zero <1198474846@qq.com> Co-authored-by: Amir E. Aharoni Co-authored-by: Josephine Lim Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> * [GSoC] Show uploaded images differently. (#4464) * uploaded images shown differently * Loaded images before query * Handled exceptions, Made ImageLoader injectable, Document and clean code * [GSoC] Added Uploaded status table in room database. (#4476) * added Uploaded status table in room database * Added unique property, minor refractoring * Database intigrated * Database integrated * Handled result null exception * Exceptions handled and refractored * Introduced constants * moved to sealed class * No database insert on network error * queried original image * documented the code * Updated uploaded status on upload success * Image Helper test (#4485) * [GSoC] Adapter Tests (#4488) * Added FolderAdapterTest * Image Adapter Test * merge fix * rebase fix Co-authored-by: translatewiki.net Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com> Co-authored-by: Obsidian_zero <1198474846@qq.com> Co-authored-by: Amir E. Aharoni Co-authored-by: Josephine Lim Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> Co-authored-by: Brigham Byerly <6891883+byerlyb20@users.noreply.github.com> Co-authored-by: Jamie Brown Co-authored-by: Prince kushwaha <65972015+Prince-kushwaha@users.noreply.github.com> Co-authored-by: Nicolas Raoul Co-authored-by: Ashar --- app/src/main/res/values/attrs.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index f43772fb5..fb61e8d18 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -51,6 +51,7 @@ + From 89fd4f4b1fb25b6eb7d6d7837c3d81a42c378321 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Sun, 18 Jul 2021 05:47:01 +0530 Subject: [PATCH 72/78] [GSoC] Custom Selector Tests (#4494) * Localisation updates from https://translatewiki.net. * Fixes #4357 After switching to different account, contributions screen shows pictures of previous account (#4421) * Update UploadMediaDetailFragment.java * Update LoginActivity.java Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account * Revert "Update UploadMediaDetailFragment.java" This reverts commit b1b4257f205b022ffaadee9f947357e5fc04c337. Co-authored-by: Obsidian_zero <1198474846@qq.com> * Remove unnecessary whitespace from a message (#4439) * Merge v3.0.1 into master (#4446) * Versioning and changelog for v3.0.0 (#4152) * Versioning for v3.0.0 * Update changelog.md * Handled migration 8-9-10 in BookmarksLocationDao (#4154) * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * Fixes #4179 (#4180) * Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked() * Updated >ContributionsFragment#backButtonClicked() to handle back press properly * Fixes #4179 (#4181) * Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed() * Cherrypick for hotfix3.1 (#4205) * Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. * fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146) * fixed bug:app crashes on viewing review in Review Fragment #4135 * Fixed the issue with back button in contribution tab. (#4177) Co-authored-by: Pratham2305 * Fixed the issue with back navigation button on toolbar in explore tab. (#4175) * Fix (#4148) Issues on theme change * fixed themeChange crashes * fixed comments * Overlooked the title bar Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> * Fixes #4173 (#4396) * Fix #4147 Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390) * Update query to fetch descriptions * Make description added to NearbyResultItem * Make string operations to display description and label in a combined way * Fix reviews, remove long description from list and swap label and description texts * Fix repeated information issue * Fix double information issue * fix style issues * Remove douplicated information * Changes made (#4354) * Remove nonexistent method * Fix #4283 IllegalStateException (#4440) * Fix #4283 IllegalStateException * Fix flickering issue * Versioning for v3.0.1 * Update changelog.md Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> * Localisation updates from https://translatewiki.net. * Added a feature for editing coordinates (#4418) * not * Place Picker added * Pick location and API call linked * minor warnings resolved * Code conventions followed * issue fixed * Wikitext edited properly * minor modification * Location Picker added * Bottom sheet removed * Location picker fully implemented * credit added * credit added * issues fixed * issues fixed * minor issue fixed * Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451) * Localisation updates from https://translatewiki.net. * Fixes 4344 - Duplicate Uploads (#4442) * Fixes 4344 - Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries. * Update states to IN_PROGRESS before uploads are processed * Fixes #3694 Pre-select places as depictions (#4452) * WikidataEditService: stop automatically adding WikidataPlace as a depiction When the user initiates the upload process from Nearby and also manually adds the place as a depiction, the depiction is added twice. Since this behavior is invisible to the user, it is being removed in preparation for auto-selecting the place as a depiction on the DepictsFragment screen. * DepictsFragment: auto-select place as a depiction Pass the Place reference from UploadActivity to DepictsFragment and select the corresponding DepictedItem. Using the place id, retrieve the corresponding Entity to create and select a DepictedItem. * UploadRepository: use Place from UploadItem to obtain a DepictedItem Instead of passing a Place object from UploadActivity to DepictsFragment and then passing the Place object up the chain to obtain and select a DepictedItem, retrieve the Place object directly within UploadRepository * DepictsFragment: select Place depiction when fragment becomes visible * UploadDepictsAdapter: make adapter aware of selection state Update selection state when recycled list items are automatically selected, preventing automatically selected items from appearing as unselected until they are forced to re-bind (i.e. after scrolling) * DepictsFragment: pre-select place depictions for all UploadItems If several images are selected and set to different places, pre-select all place depictions to reinforce the intended upload workflow philosophy (i.e. all images in a set are intended to be from/of the same place). See discussion in commons-app/apps-android-commons#3694 * DepictsFragment: scroll to the top every time list is updated * Typo fixes (#4461) * Fixed typo on class documentation of TextUtils * corrected comma placement in documentation * Fixed typos in comments * fix-issue-4424 (#4445) Co-authored-by: Pratham2305 * fix edit categories ui (#4414) Co-authored-by: Pratham2305 * Fix doom version issue (#4463) * Update db version * DBOpenHelper version update * fix :Back Pressed Event not work in Explore tab when user not login (#4404) * fix :Back Pressed Event not work in Explore tab * minor changes * fix :Upload count or number of contribution does not get updated when media is successful uploaded (#4399) * * fix:Number of Contributions not updated * Add javadocs * minor changes * made minor changes * String was nonsense and untranslatible, fixed (#4466) * Ability to show captions and descriptions in all entered languages (#4355) * implement Ability to show captions and descriptions in all entered languages *Add Javadoc * handle Back event of fragment(mediaDetailFragment) * fix minor bugs * add internationalization * revert previous changes * fix visibility bug * resolve conflict * Fixes #4437 - Changed indentation on files with 2 spaces to 4 spaces (#4462) * Edited Project.xml to make indent size 4 * Changed files with 2 space indentation to use 4 space indentation * Edited Project.xml to make indent size 4 * changed files with 2 space indent to 4 space indent * fix :Back Pressed Event not work in Explore tab when user not login (#4404) * fix :Back Pressed Event not work in Explore tab * minor changes * fix :Upload count or number of contribution does not get updated when media is successful uploaded (#4399) * * fix:Number of Contributions not updated * Add javadocs * minor changes * made minor changes * String was nonsense and untranslatible, fixed (#4466) * Ability to show captions and descriptions in all entered languages (#4355) * implement Ability to show captions and descriptions in all entered languages *Add Javadoc * handle Back event of fragment(mediaDetailFragment) * fix minor bugs * add internationalization * revert previous changes * fix visibility bug * resolve conflict Co-authored-by: Prince kushwaha <65972015+Prince-kushwaha@users.noreply.github.com> Co-authored-by: neslihanturan * Use more understandable strings (#4470) * Fix #3792 Missing Column Issue (#4468) * Fix Missing Column Issue * Fix tests * Add UploadCategoriesFragment Unit Tests (#4473) * Panorama (#4467) * panoramic images fixed * made requested changes * Minor refactoring Co-authored-by: Aditya Srivastava * Localisation updates from https://translatewiki.net. * Main activity title is sometimes "Contributions", sometimes "Commons" (#4472) Fixes #4438 Replace == with equals() in onRestoreInstanceState * Localisation updates from https://translatewiki.net. * caption and description copyable (#4481) * Removed next button in quiz (#4382) * issues resolved * modification done * warning fixed * issues resolved * Button added * don't know function added * Button added * modification done * modification done * Localisation updates from https://translatewiki.net. * Added option to show and modify location while uploading (#4475) * initial commit * Everything done * minor modification * minor modification * Issues fixed * minor modifications * issue fixed * Issues fixed * Tutorial removed from log out state (#4479) * tutorial removed from log out state * Issue removed * Update changelog.md * Versioning for v3.0.2 * Fix #4482 (#4484) * Fix crash when image resolution is very high (#4483) * Localisation updates from https://translatewiki.net. * Add Contributions Fragment Unit Tests (#4490) * Fix Tests Errors (#4491) * Add UploadMediaDetailFragment Unit Tests (#4492) * Localisation updates from https://translatewiki.net. * Folder Fragment test * Folder Fragment test done * Initialised xmls, made folder and image item. * xmls done * xmls completed * removed unwanted attribute * Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable * [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * [GSoC] Image Selection (#4457) * Localisation updates from https://translatewiki.net. * Fixes #4357 After switching to different account, contributions screen shows pictures of previous account (#4421) * Update UploadMediaDetailFragment.java * Update LoginActivity.java Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account * Revert "Update UploadMediaDetailFragment.java" This reverts commit b1b4257f205b022ffaadee9f947357e5fc04c337. Co-authored-by: Obsidian_zero <1198474846@qq.com> * Remove unnecessary whitespace from a message (#4439) * Merge v3.0.1 into master (#4446) * Versioning and changelog for v3.0.0 (#4152) * Versioning for v3.0.0 * Update changelog.md * Handled migration 8-9-10 in BookmarksLocationDao (#4154) * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * Fixes #4179 (#4180) * Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked() * Updated >ContributionsFragment#backButtonClicked() to handle back press properly * Fixes #4179 (#4181) * Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed() * Cherrypick for hotfix3.1 (#4205) * Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. * fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146) * fixed bug:app crashes on viewing review in Review Fragment #4135 * Fixed the issue with back button in contribution tab. (#4177) Co-authored-by: Pratham2305 * Fixed the issue with back navigation button on toolbar in explore tab. (#4175) * Fix (#4148) Issues on theme change * fixed themeChange crashes * fixed comments * Overlooked the title bar Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> * Fixes #4173 (#4396) * Fix #4147 Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390) * Update query to fetch descriptions * Make description added to NearbyResultItem * Make string operations to display description and label in a combined way * Fix reviews, remove long description from list and swap label and description texts * Fix repeated information issue * Fix double information issue * fix style issues * Remove douplicated information * Changes made (#4354) * Remove nonexistent method * Fix #4283 IllegalStateException (#4440) * Fix #4283 IllegalStateException * Fix flickering issue * Versioning for v3.0.1 * Update changelog.md Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> * Localisation updates from https://translatewiki.net. * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Added a feature for editing coordinates (#4418) * not * Place Picker added * Pick location and API call linked * minor warnings resolved * Code conventions followed * issue fixed * Wikitext edited properly * minor modification * Location Picker added * Bottom sheet removed * Location picker fully implemented * credit added * credit added * issues fixed * issues fixed * minor issue fixed * Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451) * Localisation updates from https://translatewiki.net. * Fixes 4344 - Duplicate Uploads (#4442) * Fixes 4344 - Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries. * Update states to IN_PROGRESS before uploads are processed * Image selection added * Forwarded activity result to upload wizard * Initialised xmls, made folder and image item. * xmls done * xmls completed * removed unwanted attribute * Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Image selection added * Forwarded activity result to upload wizard * [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * fixed merge errors * Documented the remaining function Co-authored-by: translatewiki.net Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com> Co-authored-by: Obsidian_zero <1198474846@qq.com> Co-authored-by: Amir E. Aharoni Co-authored-by: Josephine Lim Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> * [GSoC] Show uploaded images differently. (#4464) * uploaded images shown differently * Loaded images before query * Handled exceptions, Made ImageLoader injectable, Document and clean code * [GSoC] Added Uploaded status table in room database. (#4476) * added Uploaded status table in room database * Added unique property, minor refractoring * Database intigrated * Database integrated * Handled result null exception * Exceptions handled and refractored * Introduced constants * moved to sealed class * No database insert on network error * queried original image * documented the code * Updated uploaded status on upload success * Image Helper test (#4485) * [GSoC] Adapter Tests (#4488) * Added FolderAdapterTest * Image Adapter Test * Folder Fragment test * Folder Fragment test done * Fragment test complete * Added Custom Selector View Model Test * ImageFileLoaderTest * Update strings.xml * Custom Selector Activiy test * Image Loader Test Co-authored-by: translatewiki.net Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com> Co-authored-by: Obsidian_zero <1198474846@qq.com> Co-authored-by: Amir E. Aharoni Co-authored-by: Josephine Lim Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> Co-authored-by: Brigham Byerly <6891883+byerlyb20@users.noreply.github.com> Co-authored-by: Jamie Brown Co-authored-by: Prince kushwaha <65972015+Prince-kushwaha@users.noreply.github.com> Co-authored-by: Nicolas Raoul Co-authored-by: Ashar --- .../ui/selector/FolderFragment.kt | 19 +- .../ui/selector/ImageFragment.kt | 27 ++- .../customselector/ui/selector/ImageLoader.kt | 6 +- .../explore/ExploreListRootFragment.java | 2 +- .../ui/selector/CustomSelectorActivityTest.kt | 96 ++++++++ .../selector/CustomSelectorViewModelTest.kt | 41 ++++ .../ui/selector/FolderFragmentTest.kt | 130 +++++++++++ .../ui/selector/ImageFileLoaderTest.kt | 123 ++++++++++ .../ui/selector/ImageFragmentTest.kt | 135 +++++++++++ .../ui/selector/ImageLoaderTest.kt | 218 ++++++++++++++++++ app/src/test/resources/imageLoaderTestFile | 0 11 files changed, 786 insertions(+), 11 deletions(-) create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/FolderFragmentTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoaderTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt create mode 100644 app/src/test/resources/imageLoaderTestFile diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt index 1d5901c9d..e43c0798c 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -4,9 +4,11 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ProgressBar import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.model.Result @@ -16,7 +18,6 @@ import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter import fr.free.nrw.commons.di.CommonsDaggerSupportFragment import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.upload.FileProcessor -import kotlinx.android.synthetic.main.fragment_custom_selector.* import kotlinx.android.synthetic.main.fragment_custom_selector.view.* import javax.inject.Inject @@ -27,6 +28,12 @@ class FolderFragment : CommonsDaggerSupportFragment() { */ private var viewModel: CustomSelectorViewModel? = null + /** + * View Elements + */ + private var selectorRV: RecyclerView? = null + private var loader: ProgressBar? = null + /** * View Model Factory. */ @@ -75,6 +82,8 @@ class FolderFragment : CommonsDaggerSupportFragment() { val root = inflater.inflate(R.layout.fragment_custom_selector, container, false) folderAdapter = FolderAdapter(activity!!, activity as FolderClickListener) gridLayoutManager = GridLayoutManager(context, columnCount()) + selectorRV = root.selector_rv + loader = root.loader with(root.selector_rv){ this.layoutManager = gridLayoutManager setHasFixedSize(true) @@ -96,9 +105,13 @@ class FolderFragment : CommonsDaggerSupportFragment() { val folders = ImageHelper.folderListFromImages(result.images) folderAdapter.init(folders) folderAdapter.notifyDataSetChanged() - selector_rv.visibility = View.VISIBLE + selectorRV?.let { + it.visibility = View.VISIBLE + } + } + loader?.let { + it.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE } - loader.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE } /** 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 a2de0ed29..f1583c54f 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 @@ -4,9 +4,11 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ProgressBar import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.listeners.ImageSelectListener @@ -28,7 +30,13 @@ class ImageFragment: CommonsDaggerSupportFragment() { /** * View model for images. */ - private lateinit var viewModel: CustomSelectorViewModel + private var viewModel: CustomSelectorViewModel? = null + + /** + * View Elements + */ + private var selectorRV: RecyclerView? = null + private var loader: ProgressBar? = null /** * View model Factory. @@ -98,10 +106,13 @@ class ImageFragment: CommonsDaggerSupportFragment() { this.adapter = imageAdapter } - viewModel.result.observe(viewLifecycleOwner, Observer{ + viewModel?.result?.observe(viewLifecycleOwner, Observer{ handleResult(it) }) + selectorRV = root.selector_rv + loader = root.loader + return root } @@ -113,13 +124,19 @@ class ImageFragment: CommonsDaggerSupportFragment() { val images = result.images if(images.isNotEmpty()) { imageAdapter.init(ImageHelper.filterImages(images,bucketId)) - selector_rv.visibility = View.VISIBLE + selectorRV?.let{ + it.visibility = View.VISIBLE + } } else{ - selector_rv.visibility = View.GONE + selectorRV?.let{ + it.visibility = View.GONE + } } } - loader.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE + loader?.let { + it.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE + } } /** 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 f2d4d5709..5680cc775 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 @@ -19,6 +19,7 @@ import timber.log.Timber import java.io.IOException import java.net.UnknownHostException import java.util.* +import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.collections.HashMap @@ -179,7 +180,8 @@ class ImageLoader @Inject constructor( return Result.TRUE } else { uploadedStatus.lastUpdated?.let { - if (it.date >= Calendar.getInstance().time.date - INVALIDATE_DAY_COUNT) { + val duration = Calendar.getInstance().time.time - it.time + if (TimeUnit.MILLISECONDS.toDays(duration) < INVALIDATE_DAY_COUNT) { return Result.FALSE } } @@ -218,7 +220,7 @@ class ImageLoader @Inject constructor( } companion object { - const val INVALIDATE_DAY_COUNT: Int = 7 + const val INVALIDATE_DAY_COUNT: Long = 7 } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java index 32b38fea7..e88f14b55 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java @@ -40,7 +40,7 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem featuredArguments.putString("categoryName", title); listFragment.setArguments(featuredArguments); } - + @Nullable @Override public View onCreateView(@NonNull final LayoutInflater inflater, 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 new file mode 100644 index 000000000..6d55a49e2 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt @@ -0,0 +1,96 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.net.Uri +import android.os.Bundle +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.model.Image +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Custom Selector Activity Test + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +class CustomSelectorActivityTest { + + private lateinit var activity: CustomSelectorActivity + + /** + * Set up the tests. + */ + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + activity = Robolectric.buildActivity(CustomSelectorActivity::class.java) + .get() + val onCreate = activity.javaClass.getDeclaredMethod("onCreate", Bundle::class.java) + onCreate.isAccessible = true + onCreate.invoke(activity, null) + } + + /** + * Test activity not null. + */ + @Test + @Throws(Exception::class) + fun testActivityNotNull() { + assertNotNull(activity) + } + + /** + * Test changeTitle function. + */ + @Test + @Throws(Exception::class) + fun testChangeTitle() { + val func = activity.javaClass.getDeclaredMethod("changeTitle", String::class.java) + func.isAccessible = true + func.invoke(activity, "test") + } + + /** + * Test onFolderClick function. + */ + @Test + @Throws(Exception::class) + fun testOnFolderClick() { + activity.onFolderClick(Folder(1, "test", arrayListOf())); + } + + /** + * Test selectedImagesChanged function. + */ + @Test + @Throws(Exception::class) + fun testOnSelectedImagesChanged() { + activity.onSelectedImagesChanged(ArrayList()) + } + + /** + * Test onDone function. + */ + @Test + @Throws(Exception::class) + fun testOnDone() { + activity.onDone() + activity.onSelectedImagesChanged(ArrayList(arrayListOf(Image(1, "test", Uri.parse("test"), "test", 1)))); + activity.onDone() + } + + /** + * Test onBackPressed Function. + */ + @Test + @Throws(Exception::class) + fun testOnBackPressed() { + activity.onBackPressed() + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelTest.kt new file mode 100644 index 000000000..309392d4d --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelTest.kt @@ -0,0 +1,41 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.Context +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +/** + * Custom Selector View Model test. + */ +class CustomSelectorViewModelTest { + + private lateinit var viewModel: CustomSelectorViewModel + + @Mock + private lateinit var imageFileLoader: ImageFileLoader + + @Mock + private lateinit var context: Context + + /** + * Set up the test. + */ + @Before + fun setUp(){ + MockitoAnnotations.initMocks(this) + viewModel = CustomSelectorViewModel(context, imageFileLoader); + } + + /** + * Test onCleared(); + */ + @Test + fun testOnCleared(){ + val func = viewModel.javaClass.getDeclaredMethod("onCleared") + func.isAccessible = true + func.invoke(viewModel); + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/FolderFragmentTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/FolderFragmentTest.kt new file mode 100644 index 000000000..53094d6f7 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/FolderFragmentTest.kt @@ -0,0 +1,130 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.Context +import android.os.Bundle +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import fr.free.nrw.commons.customselector.model.Result +import android.widget.ProgressBar +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import androidx.recyclerview.widget.RecyclerView +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.soloader.SoLoader +import fr.free.nrw.commons.R +import fr.free.nrw.commons.TestAppAdapter +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.customselector.model.CallbackStatus +import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.powermock.reflect.Whitebox +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import org.wikipedia.AppAdapter +import java.lang.reflect.Field + +/** + * Custom Selector Folder Fragment Test. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) +class FolderFragmentTest { + + private lateinit var fragment: FolderFragment + private lateinit var view: View + private lateinit var selectorRV : RecyclerView + private lateinit var loader : ProgressBar + private lateinit var layoutInflater: LayoutInflater + private lateinit var context: Context + private lateinit var viewModelField:Field + + @Mock + private lateinit var adapter: FolderAdapter + + @Mock + private lateinit var savedInstanceState: Bundle + + /** + * Setup the folder fragment. + */ + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + context = RuntimeEnvironment.application.applicationContext + AppAdapter.set(TestAppAdapter()) + SoLoader.setInTestMode() + Fresco.initialize(context) + val activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).create().get() + + fragment = FolderFragment.newInstance() + val fragmentManager: FragmentManager = activity.supportFragmentManager + val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction() + fragmentTransaction.add(fragment, null) + fragmentTransaction.commit() + + layoutInflater = LayoutInflater.from(activity) + view = layoutInflater.inflate(R.layout.fragment_custom_selector, null) as View + + selectorRV = view.findViewById(R.id.selector_rv) + loader = view.findViewById(R.id.loader) + + Whitebox.setInternalState(fragment, "folderAdapter", adapter) + Whitebox.setInternalState(fragment, "selectorRV", selectorRV ) + Whitebox.setInternalState(fragment, "loader", loader) + + viewModelField = fragment.javaClass.getDeclaredField("viewModel") + viewModelField.isAccessible = true + } + + /** + * Test onCreateView + */ + @Test + @Throws(Exception::class) + fun testOnCreateView() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + viewModelField.set(fragment, null) + fragment.onCreateView(layoutInflater, null, savedInstanceState) + } + + /** + * Test onCreate + */ + @Test + @Throws(Exception::class) + fun testOnCreate() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.onCreate(savedInstanceState) + } + + /** + * Test columnCount. + */ + @Test + fun testColumnCount() { + val func = fragment.javaClass.getDeclaredMethod("columnCount") + func.isAccessible = true + assertEquals(2, func.invoke(fragment)) + } + + /** + * Test handleResult. + */ + @Test + fun testHandleResult() { + val func = fragment.javaClass.getDeclaredMethod("handleResult", Result::class.java) + func.isAccessible = true + func.invoke(fragment, Result(CallbackStatus.SUCCESS, arrayListOf())) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoaderTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoaderTest.kt new file mode 100644 index 000000000..e30d47216 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoaderTest.kt @@ -0,0 +1,123 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.ContentResolver +import android.content.Context +import android.provider.MediaStore +import com.nhaarman.mockitokotlin2.anyOrNull +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.same +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.powermock.reflect.Whitebox +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import org.robolectric.fakes.RoboCursor +import java.io.File +import kotlin.coroutines.CoroutineContext + +/** + * Custom Selector Image File loader test. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ImageFileLoaderTest { + + @Mock + private lateinit var mockContentResolver: ContentResolver + + @Mock + private lateinit var context: Context; + + @Mock + private lateinit var imageLoaderListener: ImageLoaderListener + + @Mock + private lateinit var coroutineScope: CoroutineScope + + private lateinit var imageCursor: RoboCursor + private lateinit var coroutineContext: CoroutineContext + private lateinit var projection: List + private lateinit var imageFileLoader: ImageFileLoader + + /** + * Setup before tests. + */ + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + coroutineContext = Dispatchers.Main + imageCursor = RoboCursor() + imageFileLoader = ImageFileLoader(context) + projection = listOf( + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.DATA, + MediaStore.Images.Media.BUCKET_ID, + MediaStore.Images.Media.BUCKET_DISPLAY_NAME + ) + + Whitebox.setInternalState(imageFileLoader, "coroutineContext", coroutineContext) + } + + /** + * Test loading device images. + */ + @Test + fun testLoadDeviceImages() { + imageFileLoader.loadDeviceImages(imageLoaderListener, coroutineScope) + } + + /** + * Test get images from the device function. + */ + @Test + fun testGetImages() { + val func = imageFileLoader.javaClass.getDeclaredMethod( + "getImages", + ImageLoaderListener::class.java + ) + func.isAccessible = true + + val image1 = arrayOf(1, "imageLoaderTestFile", "src/test/resources/imageLoaderTestFile", 1, "downloads") + val image2 = arrayOf(2, "imageLoaderTestFile", null, 1, "downloads") + File("src/test/resources/imageLoaderTestFile").createNewFile() + + imageCursor.setColumnNames(projection) + imageCursor.setResults(arrayOf(image1, image2)); + + val contentResolver: ContentResolver = mock { + on { + query( + same(MediaStore.Images.Media.EXTERNAL_CONTENT_URI), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } doReturn imageCursor; + } + + // test null cursor. + `when`( + context.contentResolver + ).thenReturn(mockContentResolver) + func.invoke(imageFileLoader, imageLoaderListener); + + // test demo cursor. + `when`( + context.contentResolver + ).thenReturn(contentResolver) + func.invoke(imageFileLoader, imageLoaderListener); + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt new file mode 100644 index 000000000..9794003c8 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt @@ -0,0 +1,135 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.Context +import android.os.Bundle +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.widget.ProgressBar +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import androidx.recyclerview.widget.RecyclerView +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.soloader.SoLoader +import fr.free.nrw.commons.R +import fr.free.nrw.commons.TestAppAdapter +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.customselector.model.CallbackStatus +import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.customselector.model.Result +import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.powermock.reflect.Whitebox +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import org.wikipedia.AppAdapter +import java.lang.reflect.Field + +/** + * Custom Selector Image Fragment Test. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ImageFragmentTest { + + private lateinit var fragment: ImageFragment + private lateinit var view: View + private lateinit var selectorRV : RecyclerView + private lateinit var loader : ProgressBar + private lateinit var layoutInflater: LayoutInflater + private lateinit var context: Context + private lateinit var viewModelField: Field + + @Mock + private lateinit var image: Image + + @Mock + private lateinit var adapter: ImageAdapter + + @Mock + private lateinit var savedInstanceState: Bundle + + /** + * Setup the image fragment. + */ + @Before + fun setUp(){ + MockitoAnnotations.initMocks(this) + context = RuntimeEnvironment.application.applicationContext + AppAdapter.set(TestAppAdapter()) + SoLoader.setInTestMode() + Fresco.initialize(context) + val activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).create().get() + + fragment = ImageFragment.newInstance(1) + val fragmentManager: FragmentManager = activity.supportFragmentManager + val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction() + fragmentTransaction.add(fragment, null) + fragmentTransaction.commit() + + layoutInflater = LayoutInflater.from(activity) + view = layoutInflater.inflate(R.layout.fragment_custom_selector, null, false) as View + selectorRV = view.findViewById(R.id.selector_rv) + loader = view.findViewById(R.id.loader) + + Whitebox.setInternalState(fragment, "imageAdapter", adapter) + Whitebox.setInternalState(fragment, "selectorRV", selectorRV ) + Whitebox.setInternalState(fragment, "loader", loader) + + viewModelField = fragment.javaClass.getDeclaredField("viewModel") + viewModelField.isAccessible = true + } + + /** + * Test onCreate + */ + @Test + @Throws(Exception::class) + fun testOnCreate(){ + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.onCreate(savedInstanceState); + } + + /** + * Test onCreateView + */ + @Test + @Throws(Exception::class) + fun testOnCreateView() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + viewModelField.set(fragment, null) + fragment.onCreateView(layoutInflater, null, savedInstanceState) + } + + /** + * Test handleResult. + */ + @Test + fun testHandleResult(){ + val func = fragment.javaClass.getDeclaredMethod("handleResult", Result::class.java) + func.isAccessible = true + func.invoke(fragment, Result(CallbackStatus.SUCCESS, arrayListOf())) + func.invoke(fragment, Result(CallbackStatus.SUCCESS, arrayListOf(image,image))) + } + + /** + * Test getSpanCount. + */ + @Test + fun testGetSpanCount() { + val func = fragment.javaClass.getDeclaredMethod("getSpanCount") + func.isAccessible = true + assertEquals(3, func.invoke(fragment)) + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt new file mode 100644 index 000000000..cb7cf3a50 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt @@ -0,0 +1,218 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.whenever +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.customselector.database.UploadedStatus +import fr.free.nrw.commons.customselector.database.UploadedStatusDao +import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter +import fr.free.nrw.commons.filepicker.PickedFiles +import fr.free.nrw.commons.filepicker.UploadableFile +import fr.free.nrw.commons.media.MediaClient +import fr.free.nrw.commons.upload.FileProcessor +import fr.free.nrw.commons.upload.FileUtilsWrapper +import io.reactivex.Single +import junit.framework.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.* +import org.powermock.api.mockito.PowerMockito +import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.modules.junit4.PowerMockRunner +import org.powermock.reflect.Whitebox +import org.robolectric.annotation.Config +import java.io.File +import java.io.FileInputStream +import java.lang.Exception +import java.util.* +import kotlin.collections.HashMap + +/** + * Image Loader Test. + */ +@RunWith(PowerMockRunner::class) +@PrepareForTest(PickedFiles::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +class ImageLoaderTest { + + @Mock + private lateinit var uri:Uri + + @Mock + private lateinit var mediaClient: MediaClient + + @Mock + private lateinit var single: Single + + @Mock + private lateinit var fileProcessor: FileProcessor + + @Mock + private lateinit var fileUtilsWrapper: FileUtilsWrapper + + @Mock + private lateinit var uploadedStatusDao: UploadedStatusDao + + @Mock + private lateinit var holder: ImageAdapter.ImageViewHolder + + @Mock + private lateinit var context: Context + + @Mock + private lateinit var uploadableFile: UploadableFile + + @Mock + private lateinit var inputStream: FileInputStream + + @Mock + private lateinit var contentResolver: ContentResolver + + @Mock + private lateinit var image: Image; + + private lateinit var imageLoader: ImageLoader; + private var mapImageSHA1: HashMap = HashMap() + private var mapHolderImage : HashMap = HashMap() + private var mapResult: HashMap = HashMap() + + /** + * Setup before test. + */ + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + imageLoader = + ImageLoader(mediaClient, fileProcessor, fileUtilsWrapper, uploadedStatusDao, context) + + Whitebox.setInternalState(imageLoader, "mapImageSHA1", mapImageSHA1); + Whitebox.setInternalState(imageLoader, "mapHolderImage", mapHolderImage); + Whitebox.setInternalState(imageLoader, "mapResult", mapResult); + Whitebox.setInternalState(imageLoader, "context", context) + } + + /** + * Test queryAndSetView. + */ + @Test + fun testQueryAndSetView(){ + // TODO + imageLoader.queryAndSetView(holder,image) + } + + /** + * Test querySha1 + */ + @Test + fun testQuerySha1() { + val func = imageLoader.javaClass.getDeclaredMethod( + "querySHA1", + String::class.java + ) + func.isAccessible = true + + Mockito.`when`(single.blockingGet()).thenReturn(true) + Mockito.`when`(mediaClient.checkFileExistsUsingSha("testSha1")).thenReturn(single) + Mockito.`when`(fileUtilsWrapper.getSHA1(any())).thenReturn("testSha1") + + // test without saving in map. + func.invoke(imageLoader, "testSha1"); + + // test with map save. + mapResult["testSha1"] = ImageLoader.Result.FALSE + func.invoke(imageLoader, "testSha1"); + } + + /** + * Test getSha1 + */ + @Test + @Throws (Exception::class) + fun testGetSha1() { + val func = imageLoader.javaClass.getDeclaredMethod( + "getSHA1", + Image::class.java + ) + func.isAccessible = true + + PowerMockito.mockStatic(PickedFiles::class.java); + BDDMockito.given(PickedFiles.pickedExistingPicture(context, image.uri)) + .willReturn(UploadableFile(uri, File("ABC"))); + + whenever(fileUtilsWrapper.getFileInputStream("ABC")).thenReturn(inputStream) + whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1") + + Assert.assertEquals("testSha1", func.invoke(imageLoader, image)); + whenever(PickedFiles.pickedExistingPicture(context,Uri.parse("test"))).thenReturn(uploadableFile) + + mapImageSHA1[image] = "testSha2" + Assert.assertEquals("testSha2", func.invoke(imageLoader, image)); + } + + /** + * Test insertIntoUploaded Function. + */ + @Test + @Throws (Exception::class) + fun testInsertIntoUploaded() { + val func = imageLoader.javaClass.getDeclaredMethod( + "insertIntoUploaded", + String::class.java, + String::class.java, + Boolean::class.java, + Boolean::class.java) + func.isAccessible = true + + func.invoke(imageLoader, "", "", true, true) + } + + /** + * Test getImageSha1. + */ + @Test + @Throws (Exception::class) + fun testGetImageSHA1() { + val func = imageLoader.javaClass.getDeclaredMethod( + "getImageSHA1", + Uri::class.java) + func.isAccessible = true + + whenever(contentResolver.openInputStream(uri)).thenReturn(inputStream) + whenever(context.contentResolver).thenReturn(contentResolver) + whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1") + + Assert.assertEquals("testSha1", func.invoke(imageLoader,uri)) + } + + /** + * Test getResultFromUploadedStatus. + */ + @Test + @Throws (Exception::class) + fun testGetResultFromUploadedStatus() { + val func = imageLoader.javaClass.getDeclaredMethod( + "getResultFromUploadedStatus", + UploadedStatus::class.java) + func.isAccessible = true + + // test Result.TRUE + Assert.assertEquals(ImageLoader.Result.TRUE, + func.invoke(imageLoader, + UploadedStatus("", "", true, true))) + + // test Result.FALSE + Assert.assertEquals(ImageLoader.Result.FALSE, + func.invoke(imageLoader, + UploadedStatus("", "", false, false, Calendar.getInstance().time))) + + // test Result.INVALID + Assert.assertEquals(ImageLoader.Result.INVALID, + func.invoke(imageLoader, UploadedStatus("", "", false, false, Date(0)))) + + } +} \ No newline at end of file diff --git a/app/src/test/resources/imageLoaderTestFile b/app/src/test/resources/imageLoaderTestFile new file mode 100644 index 000000000..e69de29bb From 1a5bb1f62215b2ec181ea00ac5b498636b7a8149 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Wed, 21 Jul 2021 08:57:25 +0530 Subject: [PATCH 73/78] Image Loader Improvements (#4516) --- .../customselector/database/UploadedDao.kt | 24 ++- .../customselector/ui/adapter/ImageAdapter.kt | 6 +- .../ui/selector/ImageFragment.kt | 8 + .../customselector/ui/selector/ImageLoader.kt | 181 +++++++++++------- .../nrw/commons/upload/worker/UploadWorker.kt | 13 +- 5 files changed, 144 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt index d9f2fc55e..c0282c92c 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt @@ -50,39 +50,37 @@ abstract class UploadedStatusDao { /** * Asynchronous insert into uploaded status table. */ - fun insertUploaded(uploadedStatus: UploadedStatus) = runBlocking { - async { - uploadedStatus.lastUpdated = Calendar.getInstance().time as Date? - insert(uploadedStatus) - }.await() + suspend fun insertUploaded(uploadedStatus: UploadedStatus) { + uploadedStatus.lastUpdated = Calendar.getInstance().time as Date? + insert(uploadedStatus) } /** * Asynchronous delete from uploaded status table. */ - fun deleteUploaded(uploadedStatus: UploadedStatus) = runBlocking { - async { delete(uploadedStatus) } + suspend fun deleteUploaded(uploadedStatus: UploadedStatus) { + delete(uploadedStatus) } /** * Asynchronous update entry in uploaded status table. */ - fun updateUploaded(uploadedStatus: UploadedStatus) = runBlocking { - async { update(uploadedStatus) } + suspend fun updateUploaded(uploadedStatus: UploadedStatus) { + update(uploadedStatus) } /** * Asynchronous image sha1 query. */ - fun getUploadedFromImageSHA1(imageSHA1: String) = runBlocking { - async { getFromImageSHA1(imageSHA1) }.await() + suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus { + return getFromImageSHA1(imageSHA1) } /** * Asynchronous modified image sha1 query. */ - fun getUploadedFromModifiedImageSHA1(modifiedImageSHA1: String) = runBlocking { - async { getFromModifiedImageSHA1(modifiedImageSHA1) }.await() + suspend fun getUploadedFromModifiedImageSHA1(modifiedImageSHA1: String):UploadedStatus { + return getFromModifiedImageSHA1(modifiedImageSHA1) } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index 9029e03bc..ff41048f0 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -76,7 +76,7 @@ class ImageAdapter( else { holder.itemUnselected(); } - Glide.with(context).load(image.uri).into(holder.image) + Glide.with(context).load(image.uri).thumbnail(0.3f).into(holder.image) imageLoader.queryAndSetView(holder,image) holder.itemView.setOnClickListener { selectOrRemoveImage(holder, position) @@ -99,8 +99,8 @@ class ImageAdapter( if(holder.isItemUploaded()){ Toast.makeText(context,"Already Uploaded image", Toast.LENGTH_SHORT).show() } else { - selectedImages.add(images[position]) - notifyItemChanged(position, ImageSelectedOrUpdated()) + selectedImages.add(images[position]) + notifyItemChanged(position, ImageSelectedOrUpdated()) } } imageSelectListener.onSelectedImagesChanged(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 f1583c54f..cbb3fc442 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 @@ -148,4 +148,12 @@ class ImageFragment: CommonsDaggerSupportFragment() { return 3 // todo change span count depending on the device orientation and other factos. } + + /** + * OnDestroy Cleanup the imageLoader coroutine. + */ + override fun onDestroy() { + imageLoader?.cleanUP() + super.onDestroy() + } } \ No newline at end of file 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 5680cc775..a617b2d2a 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 @@ -11,10 +11,7 @@ import fr.free.nrw.commons.filepicker.PickedFiles import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.upload.FileProcessor import fr.free.nrw.commons.upload.FileUtilsWrapper -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import timber.log.Timber import java.io.IOException import java.net.UnknownHostException @@ -57,9 +54,17 @@ class ImageLoader @Inject constructor( /** * Maps to facilitate image query. */ - private var mapImageSHA1: HashMap = HashMap() + private var mapModifiedImageSHA1: HashMap = HashMap() private var mapHolderImage : HashMap = HashMap() private var mapResult: HashMap = HashMap() + private var mapImageSHA1: HashMap = HashMap() + + /** + * Coroutine Dispatchers and Scope. + */ + private var defaultDispatcher = Dispatchers.Default + private var ioDispatcher = Dispatchers.IO + private val scope = MainScope() /** * Query image and setUp the view. @@ -72,42 +77,43 @@ class ImageLoader @Inject constructor( mapHolderImage[holder] = image holder.itemNotUploaded() - CoroutineScope(Dispatchers.Main).launch { + scope.launch { - var result : Result = Result.NOTFOUND - withContext(Dispatchers.Default) { + var result: Result = Result.NOTFOUND + if (mapHolderImage[holder] != image) { + return@launch + } + + val imageSHA1 = getImageSHA1(image.uri) + val uploadedStatus = getFromUploaded(imageSHA1) + + val sha1 = uploadedStatus?.let { + result = getResultFromUploadedStatus(uploadedStatus) + uploadedStatus.modifiedImageSHA1 + } ?: run { if (mapHolderImage[holder] == image) { - val imageSHA1 = getImageSHA1(image.uri) - val uploadedStatus = uploadedStatusDao.getUploadedFromImageSHA1(imageSHA1) + getSHA1(image) + } else { + "" + } + } - val sha1 = uploadedStatus?.let { - result = getResultFromUploadedStatus(uploadedStatus) - uploadedStatus.modifiedImageSHA1 - } ?: run { - if(mapHolderImage[holder] == image) { - getSHA1(image) - } else { - "" - } - } + if (mapHolderImage[holder] != image) { + return@launch + } - if (mapHolderImage[holder] == image && - result in arrayOf(Result.NOTFOUND, Result.INVALID) && - sha1.isNotEmpty()) { - // Query original image. - result = querySHA1(imageSHA1) - if( result is Result.TRUE ) { - // Original image found. - insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false) - } - else { - // Original image not found, query modified image. - result = querySHA1(sha1) - if (result != Result.ERROR) { - insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE) - } - } + if (result in arrayOf(Result.NOTFOUND, Result.INVALID) && sha1.isNotEmpty()) { + // Query original image. + result = querySHA1(imageSHA1) + if (result is Result.TRUE) { + // Original image found. + insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false) + } else { + // Original image not found, query modified image. + result = querySHA1(sha1) + if (result != Result.ERROR) { + insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE) } } } @@ -122,25 +128,27 @@ class ImageLoader @Inject constructor( * * @return Query result. */ - private fun querySHA1(SHA1: String): Result { - mapResult[SHA1]?.let{ - return it - } - var result : Result = Result.FALSE - try { - if (mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()) { - mapResult[SHA1] = Result.TRUE - result = Result.TRUE + + private suspend fun querySHA1(SHA1: String): Result { + return withContext(ioDispatcher) { + mapResult[SHA1]?.let { + return@withContext it } - } catch (e: Exception) { - if (e is UnknownHostException) { - // Handle no network connection. - Timber.e(e, "Network Connection Error") + var result: Result = Result.FALSE + try { + if (mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()) { + mapResult[SHA1] = Result.TRUE + result = Result.TRUE + } + } catch (e: Exception) { + if (e is UnknownHostException) { + // Handle no network connection. + Timber.e(e, "Network Connection Error") + } + result = Result.ERROR + e.printStackTrace() } - result = Result.ERROR - e.printStackTrace() - } finally { - return result + result } } @@ -149,27 +157,48 @@ class ImageLoader @Inject constructor( * * @return sha1 of the image */ - private fun getSHA1(image: Image): String { - mapImageSHA1[image]?.let{ + private suspend fun getSHA1(image: Image): String { + mapModifiedImageSHA1[image]?.let{ return it } val sha1 = generateModifiedSHA1(image); - mapImageSHA1[image] = sha1; + mapModifiedImageSHA1[image] = sha1; return sha1; } + /** + * Get the uploaded status entry from the database. + */ + private suspend fun getFromUploaded(imageSha1:String): UploadedStatus?{ + return uploadedStatusDao.getUploadedFromImageSHA1(imageSha1) + } + /** * Insert into uploaded status table. */ - private fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){ - uploadedStatusDao.insertUploaded(UploadedStatus(imageSha1, modifiedImageSha1, imageResult, modifiedImageResult)) + private suspend fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){ + uploadedStatusDao.insertUploaded( + UploadedStatus( + imageSha1, + modifiedImageSha1, + imageResult, + modifiedImageResult + ) + ) } /** * Get image sha1 from uri, used to retrieve the original image sha1. */ - private fun getImageSHA1(uri: Uri): String { - return fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri)) + private suspend fun getImageSHA1(uri: Uri): String { + return withContext(ioDispatcher) { + mapImageSHA1[uri]?.let{ + return@withContext it + } + val result = fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri)) + mapImageSHA1[uri] = result + result + } } /** @@ -194,18 +223,28 @@ class ImageLoader @Inject constructor( * * @return modified sha1 */ - private fun generateModifiedSHA1(image: Image) : String { - val uploadableFile = PickedFiles.pickedExistingPicture(context, image.uri) - val exifInterface: ExifInterface? = try { - ExifInterface(uploadableFile.file!!) - } catch (e: IOException) { - Timber.e(e) - null + private suspend fun generateModifiedSHA1(image: Image) : String { + return withContext(defaultDispatcher) { + val uploadableFile = PickedFiles.pickedExistingPicture(context, image.uri) + val exifInterface: ExifInterface? = try { + ExifInterface(uploadableFile.file!!) + } catch (e: IOException) { + Timber.e(e) + null + } + fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact()) + val sha1 = + fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(uploadableFile.filePath)) + uploadableFile.file.delete() + sha1 } - fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact()) - val sha1 = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(uploadableFile.filePath)) - uploadableFile.file.delete() - return sha1 + } + + /** + * CleanUp function. + */ + fun cleanUP() { + scope.cancel() } /** 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 5c1ee6846..bb548e13e 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 @@ -28,9 +28,11 @@ import fr.free.nrw.commons.upload.UploadClient import fr.free.nrw.commons.upload.UploadResult import fr.free.nrw.commons.wikidata.WikidataEditService import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.util.* @@ -408,7 +410,16 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : contribution.contentUri?.let { val imageSha1 = fileUtilsWrapper.getSHA1(appContext.contentResolver.openInputStream(it)) val modifiedSha1 = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(contribution.localUri?.path)) - uploadedStatusDao.insertUploaded(UploadedStatus(imageSha1, modifiedSha1, imageSha1 == modifiedSha1, true)); + MainScope().launch { + uploadedStatusDao.insertUploaded( + UploadedStatus( + imageSha1, + modifiedSha1, + imageSha1 == modifiedSha1, + true + ) + ); + } } } From af3936ae16808654c28d7570efd26762314c7084 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Sat, 24 Jul 2021 14:09:59 +0530 Subject: [PATCH 74/78] ImageLoader Test Updated (#4517) --- app/build.gradle | 1 + .../customselector/database/UploadedDao.kt | 38 +--- .../customselector/ui/selector/ImageLoader.kt | 18 +- .../ui/selector/ImageLoaderTest.kt | 172 +++++++++--------- 4 files changed, 101 insertions(+), 128 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 501595fda..0ccc38f60 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -93,6 +93,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:5.3.1" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.3.1" testImplementation 'com.facebook.soloader:soloader:0.9.0' + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2" // Android testing androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt index c0282c92c..49a1f61c3 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt @@ -1,9 +1,7 @@ package fr.free.nrw.commons.customselector.database import androidx.room.* -import kotlinx.coroutines.runBlocking import java.util.* -import kotlinx.coroutines.* /** * UploadedStatusDao for Custom Selector. @@ -29,58 +27,30 @@ abstract class UploadedStatusDao { @Delete abstract suspend fun delete(uploadedStatus: UploadedStatus) - /** - * Get All entries from the uploaded status table. - */ - @Query("SELECT * FROM uploaded_table") - abstract suspend fun getAll() : List - /** * Query uploaded status with image sha1. */ @Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ") - abstract suspend 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 suspend fun getFromModifiedImageSHA1(modifiedImageSHA1 : String) : UploadedStatus + abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1 : String) : UploadedStatus? /** * Asynchronous insert into uploaded status table. */ suspend fun insertUploaded(uploadedStatus: UploadedStatus) { - uploadedStatus.lastUpdated = Calendar.getInstance().time as Date? + uploadedStatus.lastUpdated = Calendar.getInstance().time insert(uploadedStatus) } - /** - * Asynchronous delete from uploaded status table. - */ - suspend fun deleteUploaded(uploadedStatus: UploadedStatus) { - delete(uploadedStatus) - } - - /** - * Asynchronous update entry in uploaded status table. - */ - suspend fun updateUploaded(uploadedStatus: UploadedStatus) { - update(uploadedStatus) - } - /** * Asynchronous image sha1 query. */ - suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus { + suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus? { return getFromImageSHA1(imageSHA1) } - - /** - * Asynchronous modified image sha1 query. - */ - suspend fun getUploadedFromModifiedImageSHA1(modifiedImageSHA1: String):UploadedStatus { - return getFromModifiedImageSHA1(modifiedImageSHA1) - } - } \ No newline at end of file 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 a617b2d2a..3b5254f86 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 @@ -62,9 +62,9 @@ class ImageLoader @Inject constructor( /** * Coroutine Dispatchers and Scope. */ - private var defaultDispatcher = Dispatchers.Default - private var ioDispatcher = Dispatchers.IO - private val scope = MainScope() + private var defaultDispatcher : CoroutineDispatcher = Dispatchers.Default + private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO + private val scope : CoroutineScope = MainScope() /** * Query image and setUp the view. @@ -129,7 +129,7 @@ class ImageLoader @Inject constructor( * @return Query result. */ - private suspend fun querySHA1(SHA1: String): Result { + suspend fun querySHA1(SHA1: String): Result { return withContext(ioDispatcher) { mapResult[SHA1]?.let { return@withContext it @@ -157,7 +157,7 @@ class ImageLoader @Inject constructor( * * @return sha1 of the image */ - private suspend fun getSHA1(image: Image): String { + suspend fun getSHA1(image: Image): String { mapModifiedImageSHA1[image]?.let{ return it } @@ -169,14 +169,14 @@ class ImageLoader @Inject constructor( /** * Get the uploaded status entry from the database. */ - private suspend fun getFromUploaded(imageSha1:String): UploadedStatus?{ + suspend fun getFromUploaded(imageSha1:String): UploadedStatus? { return uploadedStatusDao.getUploadedFromImageSHA1(imageSha1) } /** * Insert into uploaded status table. */ - private suspend fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){ + suspend fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){ uploadedStatusDao.insertUploaded( UploadedStatus( imageSha1, @@ -190,7 +190,7 @@ class ImageLoader @Inject constructor( /** * Get image sha1 from uri, used to retrieve the original image sha1. */ - private suspend fun getImageSHA1(uri: Uri): String { + suspend fun getImageSHA1(uri: Uri): String { return withContext(ioDispatcher) { mapImageSHA1[uri]?.let{ return@withContext it @@ -204,7 +204,7 @@ class ImageLoader @Inject constructor( /** * Get result data from database. */ - private fun getResultFromUploadedStatus(uploadedStatus: UploadedStatus): Result { + fun getResultFromUploadedStatus(uploadedStatus: UploadedStatus): Result { if (uploadedStatus.imageResult || uploadedStatus.modifiedImageResult) { return Result.TRUE } else { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt index cb7cf3a50..fe26921e5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt @@ -3,8 +3,7 @@ package fr.free.nrw.commons.customselector.ui.selector import android.content.ContentResolver import android.content.Context import android.net.Uri -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.whenever +import com.nhaarman.mockitokotlin2.* import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.customselector.database.UploadedStatus import fr.free.nrw.commons.customselector.database.UploadedStatusDao @@ -17,6 +16,10 @@ import fr.free.nrw.commons.upload.FileProcessor import fr.free.nrw.commons.upload.FileUtilsWrapper import io.reactivex.Single import junit.framework.Assert +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.* +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -28,7 +31,6 @@ import org.powermock.reflect.Whitebox import org.robolectric.annotation.Config import java.io.File import java.io.FileInputStream -import java.lang.Exception import java.util.* import kotlin.collections.HashMap @@ -38,6 +40,7 @@ import kotlin.collections.HashMap @RunWith(PowerMockRunner::class) @PrepareForTest(PickedFiles::class) @Config(sdk = [21], application = TestCommonsApplication::class) +@ExperimentalCoroutinesApi class ImageLoaderTest { @Mock @@ -73,146 +76,145 @@ class ImageLoaderTest { @Mock private lateinit var contentResolver: ContentResolver - @Mock - private lateinit var image: Image; + @ExperimentalCoroutinesApi + private val testDispacher = TestCoroutineDispatcher() private lateinit var imageLoader: ImageLoader; - private var mapImageSHA1: HashMap = HashMap() + private var mapImageSHA1: HashMap = HashMap() private var mapHolderImage : HashMap = HashMap() private var mapResult: HashMap = HashMap() + private var mapModifiedImageSHA1: HashMap = HashMap() + private lateinit var image: Image; + private lateinit var uploadedStatus: UploadedStatus; /** * Setup before test. */ @Before + @ExperimentalCoroutinesApi fun setup() { + Dispatchers.setMain(testDispacher) MockitoAnnotations.initMocks(this) + imageLoader = ImageLoader(mediaClient, fileProcessor, fileUtilsWrapper, uploadedStatusDao, context) + uploadedStatus= UploadedStatus( + "testSha1", + "testSha1", + false, + false, + Calendar.getInstance().time + ) + image = Image(1, "test", uri, "test", 0, "test") Whitebox.setInternalState(imageLoader, "mapImageSHA1", mapImageSHA1); Whitebox.setInternalState(imageLoader, "mapHolderImage", mapHolderImage); + Whitebox.setInternalState(imageLoader, "mapModifiedImageSHA1", mapModifiedImageSHA1); Whitebox.setInternalState(imageLoader, "mapResult", mapResult); Whitebox.setInternalState(imageLoader, "context", context) + Whitebox.setInternalState(imageLoader, "ioDispatcher", testDispacher) + Whitebox.setInternalState(imageLoader, "defaultDispatcher", testDispacher) + + whenever(contentResolver.openInputStream(uri)).thenReturn(inputStream) + whenever(context.contentResolver).thenReturn(contentResolver) + whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1") } /** - * Test queryAndSetView. + * Reset Dispatchers. + */ + @After + @ExperimentalCoroutinesApi + fun tearDown() { + Dispatchers.resetMain() + testDispacher.cleanupTestCoroutines() + } + + /** + * Test queryAndSetView with upload Status as null. */ @Test - fun testQueryAndSetView(){ - // TODO - imageLoader.queryAndSetView(holder,image) + fun testQueryAndSetViewUploadedStatusNull() = testDispacher.runBlockingTest { + whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(null) + mapModifiedImageSHA1[image] = "testSha1" + mapImageSHA1[uri] = "testSha1" + + mapResult["testSha1"] = ImageLoader.Result.TRUE + imageLoader.queryAndSetView(holder, image) + + mapResult["testSha1"] = ImageLoader.Result.FALSE + imageLoader.queryAndSetView(holder, image) + } + + /** + * Test queryAndSetView with upload Status not null (ie retrieved from table) + */ + @Test + fun testQueryAndSetViewUploadedStatusNotNull() = testDispacher.runBlockingTest { + whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(uploadedStatus) + imageLoader.queryAndSetView(holder, image) } /** * Test querySha1 */ @Test - fun testQuerySha1() { - val func = imageLoader.javaClass.getDeclaredMethod( - "querySHA1", - String::class.java - ) - func.isAccessible = true + fun testQuerySha1() = testDispacher.runBlockingTest { - Mockito.`when`(single.blockingGet()).thenReturn(true) - Mockito.`when`(mediaClient.checkFileExistsUsingSha("testSha1")).thenReturn(single) - Mockito.`when`(fileUtilsWrapper.getSHA1(any())).thenReturn("testSha1") + whenever(single.blockingGet()).thenReturn(true) + whenever(mediaClient.checkFileExistsUsingSha("testSha1")).thenReturn(single) + whenever(fileUtilsWrapper.getSHA1(any())).thenReturn("testSha1") - // test without saving in map. - func.invoke(imageLoader, "testSha1"); - - // test with map save. - mapResult["testSha1"] = ImageLoader.Result.FALSE - func.invoke(imageLoader, "testSha1"); + imageLoader.querySHA1("testSha1") } /** * Test getSha1 */ @Test - @Throws (Exception::class) - fun testGetSha1() { - val func = imageLoader.javaClass.getDeclaredMethod( - "getSHA1", - Image::class.java - ) - func.isAccessible = true + @ExperimentalCoroutinesApi + fun testGetSha1() = testDispacher.runBlockingTest { - PowerMockito.mockStatic(PickedFiles::class.java); + PowerMockito.mockStatic(PickedFiles::class.java) BDDMockito.given(PickedFiles.pickedExistingPicture(context, image.uri)) - .willReturn(UploadableFile(uri, File("ABC"))); + .willReturn(UploadableFile(uri, File("ABC"))) + whenever(fileUtilsWrapper.getFileInputStream("ABC")).thenReturn(inputStream) whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1") - Assert.assertEquals("testSha1", func.invoke(imageLoader, image)); - whenever(PickedFiles.pickedExistingPicture(context,Uri.parse("test"))).thenReturn(uploadableFile) + Assert.assertEquals("testSha1", imageLoader.getSHA1(image)); + whenever(PickedFiles.pickedExistingPicture(context, Uri.parse("test"))).thenReturn( + uploadableFile + ) - mapImageSHA1[image] = "testSha2" - Assert.assertEquals("testSha2", func.invoke(imageLoader, image)); - } - - /** - * Test insertIntoUploaded Function. - */ - @Test - @Throws (Exception::class) - fun testInsertIntoUploaded() { - val func = imageLoader.javaClass.getDeclaredMethod( - "insertIntoUploaded", - String::class.java, - String::class.java, - Boolean::class.java, - Boolean::class.java) - func.isAccessible = true - - func.invoke(imageLoader, "", "", true, true) - } - - /** - * Test getImageSha1. - */ - @Test - @Throws (Exception::class) - fun testGetImageSHA1() { - val func = imageLoader.javaClass.getDeclaredMethod( - "getImageSHA1", - Uri::class.java) - func.isAccessible = true - - whenever(contentResolver.openInputStream(uri)).thenReturn(inputStream) - whenever(context.contentResolver).thenReturn(contentResolver) - whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1") - - Assert.assertEquals("testSha1", func.invoke(imageLoader,uri)) + mapModifiedImageSHA1[image] = "testSha2" + Assert.assertEquals("testSha2", imageLoader.getSHA1(image)); } /** * Test getResultFromUploadedStatus. */ @Test - @Throws (Exception::class) fun testGetResultFromUploadedStatus() { val func = imageLoader.javaClass.getDeclaredMethod( "getResultFromUploadedStatus", UploadedStatus::class.java) func.isAccessible = true - // test Result.TRUE - Assert.assertEquals(ImageLoader.Result.TRUE, - func.invoke(imageLoader, - UploadedStatus("", "", true, true))) - - // test Result.FALSE - Assert.assertEquals(ImageLoader.Result.FALSE, - func.invoke(imageLoader, - UploadedStatus("", "", false, false, Calendar.getInstance().time))) - // test Result.INVALID + uploadedStatus.lastUpdated = Date(0); Assert.assertEquals(ImageLoader.Result.INVALID, - func.invoke(imageLoader, UploadedStatus("", "", false, false, Date(0)))) + imageLoader.getResultFromUploadedStatus(uploadedStatus)) + // test Result.TRUE + uploadedStatus.imageResult = true; + Assert.assertEquals(ImageLoader.Result.TRUE, + imageLoader.getResultFromUploadedStatus(uploadedStatus)) + } + + @Test + fun testCleanUP() { + imageLoader.cleanUP() } } \ No newline at end of file From 9b00c9417ff23b4b68928158848d0845796fe948 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Mon, 26 Jul 2021 16:31:45 +0530 Subject: [PATCH 75/78] [GSoC] Improvement and bug Fixes (#4522) * Improvement and bug Fixes * fixed ellipsize --- .../ui/selector/CustomSelectorActivity.kt | 5 +- .../res/layout/activity_custom_selector.xml | 14 +-- .../res/layout/custom_selector_toolbar.xml | 85 ++++++++++--------- .../res/layout/item_custom_selector_image.xml | 1 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 58 insertions(+), 49 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 099c89a86..ec7855cb4 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 @@ -3,6 +3,7 @@ package fr.free.nrw.commons.customselector.ui.selector import android.app.Activity import android.content.Intent import android.os.Bundle +import android.view.View import android.widget.ImageButton import android.widget.TextView import androidx.lifecycle.ViewModelProvider @@ -96,7 +97,9 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL */ override fun onSelectedImagesChanged(selectedImages: ArrayList) { viewModel.selectedImages.value = selectedImages - // todo update selected images in view model. + + val done : ImageButton = findViewById(R.id.done) + done.visibility = if (selectedImages.isEmpty()) View.INVISIBLE else View.VISIBLE } /** diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml index 9587e7c0a..d96918fee 100644 --- a/app/src/main/res/layout/activity_custom_selector.xml +++ b/app/src/main/res/layout/activity_custom_selector.xml @@ -4,16 +4,10 @@ android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"> - - - + + - + + - - - + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_custom_selector_image.xml b/app/src/main/res/layout/item_custom_selector_image.xml index 021f463bc..f04a71922 100644 --- a/app/src/main/res/layout/item_custom_selector_image.xml +++ b/app/src/main/res/layout/item_custom_selector_image.xml @@ -40,6 +40,7 @@ app:layout_constraintDimensionRatio="H,1:1" android:textSize="11sp" android:textStyle="bold" + android:textColor="@color/black" android:layout_margin="@dimen/dimen_6" android:gravity="center|center_vertical" style="@style/TextAppearance.AppCompat.Small" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ab91a42e0..5419803ed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -640,5 +640,7 @@ Upload your first media by tapping on the add button. Check whether location is correct Custom Selector No Images + Done + Back From 267b6b2a1b912671bf565892676fec6c3775e633 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Tue, 27 Jul 2021 05:50:49 +0530 Subject: [PATCH 76/78] Saving selector state (#4526) --- .../listeners/FolderClickListener.kt | 4 +- .../ui/adapter/FolderAdapter.kt | 2 +- .../ui/selector/CustomSelectorActivity.kt | 65 ++++++++++++++++--- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt index 15b74c57e..cb32807f8 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt @@ -1,7 +1,5 @@ package fr.free.nrw.commons.customselector.listeners -import fr.free.nrw.commons.customselector.model.Folder - interface FolderClickListener { - fun onFolderClick(folder : Folder) + fun onFolderClick(folderId: Long, folderName: String) } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt index fb3e49794..67dcc789c 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -49,7 +49,7 @@ class FolderAdapter( holder.name.text = folder.name holder.count.text = count.toString() holder.itemView.setOnClickListener{ - itemClickListener.onFolderClick(folder) + itemClickListener.onFolderClick(folder.bucketId, folder.name) } //todo load image thumbnail. 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 ec7855cb4..3b8bee390 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 @@ -2,26 +2,43 @@ package fr.free.nrw.commons.customselector.ui.selector import android.app.Activity import android.content.Intent +import android.content.SharedPreferences import android.os.Bundle import android.view.View import android.widget.ImageButton import android.widget.TextView +import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProvider import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.listeners.ImageSelectListener -import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.model.Image import fr.free.nrw.commons.theme.BaseActivity import java.io.File import javax.inject.Inject -class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectListener { +class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectListener, FragmentManager.OnBackStackChangedListener { /** * View model. */ - private lateinit var viewModel: CustomSelectorViewModel + private lateinit var viewModel: CustomSelectorViewModel + + /** + * isImageFragmentOpen is true when the image fragment is in view. + */ + private var isImageFragmentOpen = false + + /** + * Current ImageFragment attributes. + */ + private var bucketId: Long = 0L + private lateinit var bucketName: String + + /** + * Pref for saving selector state. + */ + private lateinit var prefs: SharedPreferences /** * View Model Factory. @@ -35,9 +52,17 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL super.onCreate(savedInstanceState) setContentView(R.layout.activity_custom_selector) - viewModel = ViewModelProvider(this,customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java) + prefs = applicationContext.getSharedPreferences("CustomSelector", MODE_PRIVATE) + viewModel = ViewModelProvider(this, customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java) setupViews() + + // Open folder if saved in prefs. + if(prefs.contains("FolderId")){ + val lastOpenFolderId: Long = prefs.getLong("FolderId", 0L) + val lastOpenFolderName: String? = prefs.getString("FolderName", null) + lastOpenFolderName?.let { onFolderClick(lastOpenFolderId, it) } + } } /** @@ -49,8 +74,6 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL .commit() fetchData() setUpToolbar() - - // todo : open image fragment depending on the last user visit. } /** @@ -63,7 +86,7 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL /** * Change the title of the toolbar. */ - private fun changeTitle(title:String) { + private fun changeTitle(title: String) { val titleText = findViewById(R.id.title) if(titleText != null) { titleText.text = title @@ -84,12 +107,17 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL /** * override on folder click, change the toolbar title on folder click. */ - override fun onFolderClick(folder: Folder) { + override fun onFolderClick(folderId: Long, folderName: String) { supportFragmentManager.beginTransaction() - .add(R.id.fragment_container, ImageFragment.newInstance(folder.bucketId)) + .add(R.id.fragment_container, ImageFragment.newInstance(folderId)) .addToBackStack(null) .commit() - changeTitle(folder.name) + + changeTitle(folderName) + + bucketId = folderId + bucketName = folderName + isImageFragmentOpen = true } /** @@ -148,4 +176,21 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL } } + override fun onDestroy() { + if(isImageFragmentOpen){ + prefs.edit().putLong("FolderId", bucketId).putString("FolderName", bucketName).apply() + } else { + prefs.edit().remove("FolderId").remove("FolderName").apply() + } + super.onDestroy() + } + + /** + * Called whenever the contents of the back stack change. + */ + override fun onBackStackChanged() { + if(supportFragmentManager.backStackEntryCount == 0) { + isImageFragmentOpen = false + } + } } \ No newline at end of file From 658a7ec3d742c60077ef920389042c89ce3ae5d7 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Sun, 8 Aug 2021 18:38:11 +0530 Subject: [PATCH 77/78] [GSoC] Saved Image Fragment Scroll State (#4528) * Saved Image Fragment Scroll State * Fix delete image * Fixed Delete bug * Changed custom selector icon --- .../customselector/helper/ImageHelper.kt | 25 ++++---- .../listeners/FolderClickListener.kt | 2 +- .../ui/adapter/FolderAdapter.kt | 41 ++++++++++--- .../customselector/ui/adapter/ImageAdapter.kt | 54 +++++++++++------- .../ui/selector/CustomSelectorActivity.kt | 36 ++++++------ .../ui/selector/FolderFragment.kt | 13 ++++- .../ui/selector/ImageFragment.kt | 57 +++++++++++++++++-- .../customselector/ui/selector/ImageLoader.kt | 14 ++++- .../res/drawable/ic_custom_image_picker.xml | 3 + .../layout/fragment_contributions_list.xml | 22 +++---- .../layout/item_custom_selector_folder.xml | 2 - 11 files changed, 186 insertions(+), 83 deletions(-) create mode 100644 app/src/main/res/drawable/ic_custom_image_picker.xml diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt index 0a751d47b..1447cd2d7 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt @@ -1,19 +1,7 @@ package fr.free.nrw.commons.customselector.helper -import android.content.Context -import com.mapbox.android.core.FileUtils import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.model.Image -import fr.free.nrw.commons.filepicker.Constants -import timber.log.Timber -import java.io.* -import java.math.BigInteger -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import kotlin.collections.ArrayList -import kotlin.collections.HashMap -import kotlin.collections.LinkedHashMap - /** * Image Helper object, includes all the static functions required by custom selector. @@ -24,7 +12,7 @@ object ImageHelper { /** * Returns the list of folders from given image list. */ - fun folderListFromImages(images: List): List { + fun folderListFromImages(images: List): ArrayList { val folderMap: MutableMap = LinkedHashMap() for (image in images) { val bucketId = image.bucketId @@ -61,6 +49,17 @@ object ImageHelper { return list.indexOf(image) } + /** + * getIndex: Returns the index of image in given list. + */ + fun getIndexFromId(list: ArrayList, imageId: Long): Int { + for(i in list){ + if(i.id == imageId) + return list.indexOf(i) + } + return 0; + } + /** * Gets the list of indices from the master list. */ diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt index cb32807f8..e016a71ba 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt @@ -1,5 +1,5 @@ package fr.free.nrw.commons.customselector.listeners interface FolderClickListener { - fun onFolderClick(folderId: Long, folderName: String) + fun onFolderClick(folderId: Long, folderName: String, lastItemId: Long) } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt index 67dcc789c..93759bdf4 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -11,6 +11,7 @@ import com.bumptech.glide.Glide import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.model.Image class FolderAdapter( /** @@ -43,16 +44,38 @@ class FolderAdapter( */ override fun onBindViewHolder(holder: FolderViewHolder, position: Int) { val folder = folders[position] - val count = folder.images.size - val previewImage = folder.images[0] - Glide.with(context).load(previewImage.uri).into(holder.image) - holder.name.text = folder.name - holder.count.text = count.toString() - holder.itemView.setOnClickListener{ - itemClickListener.onFolderClick(folder.bucketId, folder.name) - } + val toBeRemoved = ArrayList() - //todo load image thumbnail. + for(image in folder.images) { + // Remove all the top images that do not exist anymore + if(context.contentResolver.getType(image.uri) == null){ + // File not found + toBeRemoved.add(image) + } else { + break + } + } + holder.image.setImageDrawable (null) + folder.images.removeAll(toBeRemoved) + val count = folder.images.size + + if(count == 0) { + // Folder is empty, remove folder from the adapter. + holder.itemView.post{ + val updatePosition = folders.indexOf(folder) + folders.removeAt(updatePosition) + notifyItemRemoved(updatePosition) + notifyItemRangeChanged(updatePosition, folders.size) + } + } else { + val previewImage = folder.images[0] + Glide.with(context).load(previewImage.uri).into(holder.image) + holder.name.text = folder.name + holder.count.text = count.toString() + holder.itemView.setOnClickListener { + itemClickListener.onFolderClick(folder.bucketId, folder.name, 0) + } + } } /** diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index ff41048f0..8225ab2dc 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -1,9 +1,8 @@ package fr.free.nrw.commons.customselector.ui.adapter import android.content.Context -import android.view.ViewGroup -import fr.free.nrw.commons.R import android.view.View +import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import android.widget.Toast @@ -11,6 +10,7 @@ import androidx.constraintlayout.widget.Group import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.Image @@ -59,7 +59,7 @@ class ImageAdapter( * Create View holder. */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { - val itemView = inflater.inflate(R.layout.item_custom_selector_image,parent, false) + val itemView = inflater.inflate(R.layout.item_custom_selector_image, parent, false) return ImageViewHolder(itemView) } @@ -68,36 +68,46 @@ class ImageAdapter( */ override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { val image=images[position] - val selectedIndex = ImageHelper.getIndex(selectedImages,image) - val isSelected = selectedIndex != -1 - if(isSelected){ - holder.itemSelected(selectedIndex+1) - } - else { - holder.itemUnselected(); - } - Glide.with(context).load(image.uri).thumbnail(0.3f).into(holder.image) - imageLoader.queryAndSetView(holder,image) - holder.itemView.setOnClickListener { - selectOrRemoveImage(holder, position) + holder.image.setImageDrawable (null) + if (context.contentResolver.getType(image.uri) == null) { + // Image does not exist anymore, update adapter. + holder.itemView.post { + val updatedPosition = images.indexOf(image) + images.remove(image) + notifyItemRemoved(updatedPosition) + notifyItemRangeChanged(updatedPosition, images.size) + } + } else { + val selectedIndex = ImageHelper.getIndex(selectedImages, image) + val isSelected = selectedIndex != -1 + if (isSelected) { + holder.itemSelected(selectedIndex + 1) + } else { + holder.itemUnselected(); + } + Glide.with(context).load(image.uri).thumbnail(0.3f).into(holder.image) + imageLoader.queryAndSetView(holder, image) + holder.itemView.setOnClickListener { + selectOrRemoveImage(holder, position) + } } } /** * Handle click event on an image, update counter on images. */ - private fun selectOrRemoveImage(holder:ImageViewHolder, position:Int){ - val clickedIndex = ImageHelper.getIndex(selectedImages,images[position]) + private fun selectOrRemoveImage(holder: ImageViewHolder, position: Int){ + val clickedIndex = ImageHelper.getIndex(selectedImages, images[position]) if (clickedIndex != -1) { selectedImages.removeAt(clickedIndex) - notifyItemChanged(position,ImageUnselected()) + notifyItemChanged(position, ImageUnselected()) val indexes = ImageHelper.getIndexList(selectedImages, images) for (index in indexes) { notifyItemChanged(index, ImageSelectedOrUpdated()) } } else { if(holder.isItemUploaded()){ - Toast.makeText(context,"Already Uploaded image", Toast.LENGTH_SHORT).show() + Toast.makeText(context, "Already Uploaded image", Toast.LENGTH_SHORT).show() } else { selectedImages.add(images[position]) notifyItemChanged(position, ImageSelectedOrUpdated()) @@ -109,7 +119,7 @@ class ImageAdapter( /** * Initialize the data set. */ - fun init(newImages:List) { + fun init(newImages: List) { val oldImageList:ArrayList = images val newImageList:ArrayList = ArrayList(newImages) val diffResult = DiffUtil.calculateDiff( @@ -128,6 +138,10 @@ class ImageAdapter( return images.size } + fun getImageIdAt(position: Int): Long { + return images.get(position).id + } + /** * Image view holder. */ 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 3b8bee390..972c16fc4 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 @@ -7,7 +7,6 @@ import android.os.Bundle import android.view.View import android.widget.ImageButton import android.widget.TextView -import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProvider import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.listeners.FolderClickListener @@ -17,7 +16,7 @@ import fr.free.nrw.commons.theme.BaseActivity import java.io.File import javax.inject.Inject -class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectListener, FragmentManager.OnBackStackChangedListener { +class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectListener { /** * View model. @@ -58,10 +57,11 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi setupViews() // Open folder if saved in prefs. - if(prefs.contains("FolderId")){ - val lastOpenFolderId: Long = prefs.getLong("FolderId", 0L) - val lastOpenFolderName: String? = prefs.getString("FolderName", null) - lastOpenFolderName?.let { onFolderClick(lastOpenFolderId, it) } + if(prefs.contains(FOLDER_ID)){ + val lastOpenFolderId: Long = prefs.getLong(FOLDER_ID, 0L) + val lastOpenFolderName: String? = prefs.getString(FOLDER_NAME, null) + val lastItemId: Long = prefs.getLong(ITEM_ID, 0) + lastOpenFolderName?.let { onFolderClick(lastOpenFolderId, it, lastItemId) } } } @@ -107,9 +107,9 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi /** * override on folder click, change the toolbar title on folder click. */ - override fun onFolderClick(folderId: Long, folderName: String) { + override fun onFolderClick(folderId: Long, folderName: String, lastItemId: Long) { supportFragmentManager.beginTransaction() - .add(R.id.fragment_container, ImageFragment.newInstance(folderId)) + .add(R.id.fragment_container, ImageFragment.newInstance(folderId, lastItemId)) .addToBackStack(null) .commit() @@ -172,25 +172,27 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi super.onBackPressed() val fragment = supportFragmentManager.findFragmentById(R.id.fragment_container) if(fragment != null && fragment is FolderFragment){ + isImageFragmentOpen = false changeTitle(getString(R.string.custom_selector_title)) } } + /** + * On activity destroy + * If image fragment is open, overwrite its attributes otherwise discard the values. + */ override fun onDestroy() { if(isImageFragmentOpen){ - prefs.edit().putLong("FolderId", bucketId).putString("FolderName", bucketName).apply() + prefs.edit().putLong(FOLDER_ID, bucketId).putString(FOLDER_NAME, bucketName).apply() } else { - prefs.edit().remove("FolderId").remove("FolderName").apply() + prefs.edit().remove(FOLDER_ID).remove(FOLDER_NAME).apply() } super.onDestroy() } - /** - * Called whenever the contents of the back stack change. - */ - override fun onBackStackChanged() { - if(supportFragmentManager.backStackEntryCount == 0) { - isImageFragmentOpen = false - } + companion object { + const val FOLDER_ID : String = "FolderId" + const val FOLDER_NAME : String = "FolderName" + const val ITEM_ID : String = "ItemId" } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt index e43c0798c..b1cd8ab37 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -14,6 +14,7 @@ import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.model.Result import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.CallbackStatus +import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter import fr.free.nrw.commons.di.CommonsDaggerSupportFragment import fr.free.nrw.commons.media.MediaClient @@ -55,6 +56,11 @@ class FolderFragment : CommonsDaggerSupportFragment() { */ private lateinit var gridLayoutManager: GridLayoutManager + /** + * Folder List. + */ + private lateinit var folders : ArrayList + /** * Companion newInstance. */ @@ -102,7 +108,7 @@ class FolderFragment : CommonsDaggerSupportFragment() { */ private fun handleResult(result: Result) { if(result.status is CallbackStatus.SUCCESS){ - val folders = ImageHelper.folderListFromImages(result.images) + folders = ImageHelper.folderListFromImages(result.images) folderAdapter.init(folders) folderAdapter.notifyDataSetChanged() selectorRV?.let { @@ -114,6 +120,11 @@ class FolderFragment : CommonsDaggerSupportFragment() { } } + override fun onResume() { + folderAdapter.notifyDataSetChanged() + super.onResume() + } + /** * Return Column count ie span count for grid view adapter. */ 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 cbb3fc442..b575e015b 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,8 @@ package fr.free.nrw.commons.customselector.ui.selector +import android.net.Uri import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -13,11 +15,15 @@ import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.CallbackStatus +import fr.free.nrw.commons.customselector.model.Image import fr.free.nrw.commons.customselector.model.Result import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter import fr.free.nrw.commons.di.CommonsDaggerSupportFragment -import kotlinx.android.synthetic.main.fragment_custom_selector.* +import fr.free.nrw.commons.theme.BaseActivity import kotlinx.android.synthetic.main.fragment_custom_selector.view.* +import java.io.File +import java.io.FileInputStream +import java.net.URI import javax.inject.Inject class ImageFragment: CommonsDaggerSupportFragment() { @@ -27,13 +33,18 @@ class ImageFragment: CommonsDaggerSupportFragment() { */ private var bucketId: Long? = null + /** + * Last ImageItem Id. + */ + private var lastItemId: Long? = null + /** * View model for images. */ private var viewModel: CustomSelectorViewModel? = null /** - * View Elements + * View Elements. */ private var selectorRV: RecyclerView? = null private var loader: ProgressBar? = null @@ -67,14 +78,16 @@ class ImageFragment: CommonsDaggerSupportFragment() { * BucketId args name */ const val BUCKET_ID = "BucketId" + const val LAST_ITEM_ID = "LastItemId" /** * newInstance from bucketId. */ - fun newInstance(bucketId: Long): ImageFragment { + fun newInstance(bucketId: Long, lastItemId: Long): ImageFragment { val fragment = ImageFragment() val args = Bundle() args.putLong(BUCKET_ID, bucketId) + args.putLong(LAST_ITEM_ID, lastItemId) fragment.arguments = args return fragment } @@ -87,6 +100,7 @@ class ImageFragment: CommonsDaggerSupportFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) bucketId = arguments?.getLong(BUCKET_ID) + lastItemId = arguments?.getLong(LAST_ITEM_ID, 0) viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java) } @@ -116,6 +130,8 @@ class ImageFragment: CommonsDaggerSupportFragment() { return root } + lateinit var filteredImages: ArrayList; + /** * Handle view model result. */ @@ -123,9 +139,14 @@ class ImageFragment: CommonsDaggerSupportFragment() { if(result.status is CallbackStatus.SUCCESS){ val images = result.images if(images.isNotEmpty()) { - imageAdapter.init(ImageHelper.filterImages(images,bucketId)) - selectorRV?.let{ + filteredImages = ImageHelper.filterImages(images, bucketId) + imageAdapter.init(filteredImages) + selectorRV?.let { it.visibility = View.VISIBLE + lastItemId?.let { pos -> + (it.layoutManager as GridLayoutManager) + .scrollToPosition(ImageHelper.getIndexFromId(filteredImages, pos)) + } } } else{ @@ -149,11 +170,35 @@ class ImageFragment: CommonsDaggerSupportFragment() { // todo change span count depending on the device orientation and other factos. } + override fun onResume() { + imageAdapter.notifyDataSetChanged() + super.onResume() + } + /** - * OnDestroy Cleanup the imageLoader coroutine. + * OnDestroy + * Cleanup the imageLoader coroutine. + * Save the Image Fragment state. */ override fun onDestroy() { imageLoader?.cleanUP() + + val position = (selectorRV?.layoutManager as GridLayoutManager) + .findFirstVisibleItemPosition() + + // Check for empty RecyclerView. + if (position != -1) { + context?.let { context -> + context.getSharedPreferences( + "CustomSelector", + BaseActivity.MODE_PRIVATE + )?.let { prefs -> + prefs.edit()?.let { editor -> + editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply() + } + } + } + } super.onDestroy() } } \ No newline at end of file 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 3b5254f86..73b2f1f79 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 @@ -13,6 +13,7 @@ import fr.free.nrw.commons.upload.FileProcessor import fr.free.nrw.commons.upload.FileUtilsWrapper import kotlinx.coroutines.* import timber.log.Timber +import java.io.FileNotFoundException import java.io.IOException import java.net.UnknownHostException import java.util.* @@ -86,6 +87,8 @@ class ImageLoader @Inject constructor( } val imageSHA1 = getImageSHA1(image.uri) + if(imageSHA1.isEmpty()) + return@launch val uploadedStatus = getFromUploaded(imageSHA1) val sha1 = uploadedStatus?.let { @@ -195,9 +198,14 @@ class ImageLoader @Inject constructor( mapImageSHA1[uri]?.let{ return@withContext it } - val result = fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri)) - mapImageSHA1[uri] = result - result + try { + val result = fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri)) + mapImageSHA1[uri] = result + result + } catch (e: FileNotFoundException){ + e.printStackTrace() + "" + } } } diff --git a/app/src/main/res/drawable/ic_custom_image_picker.xml b/app/src/main/res/drawable/ic_custom_image_picker.xml new file mode 100644 index 000000000..7dd39280a --- /dev/null +++ b/app/src/main/res/drawable/ic_custom_image_picker.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_contributions_list.xml b/app/src/main/res/layout/fragment_contributions_list.xml index e9852f49a..923cc8343 100644 --- a/app/src/main/res/layout/fragment_contributions_list.xml +++ b/app/src/main/res/layout/fragment_contributions_list.xml @@ -70,17 +70,17 @@ app:srcCompat="@drawable/ic_photo_white_24dp" /> + android:id="@+id/fab_custom_gallery" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:tint="@color/button_blue" + android:visibility="gone" + app:backgroundTint="@color/main_background_light" + app:useCompatPadding="true" + app:elevation="@dimen/tiny_margin" + app:fabSize="mini" + app:srcCompat="@drawable/ic_custom_image_picker" + android:background="@drawable/commons"/> Date: Mon, 9 Aug 2021 16:32:18 +0530 Subject: [PATCH 78/78] rebase fix --- app/src/main/res/values/attrs.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index fb61e8d18..f43772fb5 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -51,7 +51,6 @@ -