mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
Merge branch 'main' into Languages-order-preference-#5826-
This commit is contained in:
commit
c549144f11
146 changed files with 5313 additions and 4391 deletions
4
.github/workflows/android.yml
vendored
4
.github/workflows/android.yml
vendored
|
|
@ -89,7 +89,7 @@ jobs:
|
|||
run: bash ./gradlew assembleBetaDebug --stacktrace
|
||||
|
||||
- name: Upload betaDebug APK
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: betaDebugAPK
|
||||
path: app/build/outputs/apk/beta/debug/app-*.apk
|
||||
|
|
@ -98,7 +98,7 @@ jobs:
|
|||
run: bash ./gradlew assembleProdDebug --stacktrace
|
||||
|
||||
- name: Upload prodDebug APK
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: prodDebugAPK
|
||||
path: app/build/outputs/apk/prod/debug/app-*.apk
|
||||
|
|
|
|||
84
CHANGELOG.md
84
CHANGELOG.md
|
|
@ -1,5 +1,89 @@
|
|||
# Wikimedia Commons for Android
|
||||
|
||||
## v5.1.2
|
||||
|
||||
### What's changed
|
||||
|
||||
* Fix the broken category search in the explore screen
|
||||
|
||||
## v5.1.1
|
||||
|
||||
### What's changed
|
||||
|
||||
* Use Android's new EXIF interface to mitigate security issues in old
|
||||
EXIF interface.
|
||||
* Make the icon that helps view the upload queue always visible as it ensures
|
||||
that the queue accessible at all times.
|
||||
|
||||
## v5.1.0
|
||||
|
||||
### What's Changed
|
||||
|
||||
* Enhanced **upload queue management** in the Commons app for smoother, sequential
|
||||
processing, clearer progress tracking, prevention of stuck or duplicate
|
||||
uploads. As part of this improvement, the "Limited Connection mode" has been
|
||||
removed.
|
||||
* Added an option in "Nearby" feature enabling users to **provide feedback on
|
||||
Wikidata items**. Users can report if an item doesn’t exist, is at a different
|
||||
location, or has other issues, with submissions tagged for easy tracking and
|
||||
updates.
|
||||
* Improved the "Nearby" feature by splitting the query into two parts for faster
|
||||
loading and **better performance, especially in areas with dense amount of
|
||||
places**. This update also resolves issues with pins overlapping place names.
|
||||
* Upgraded AGP and **target/compile SDK to 34** and make necessary adjustments to
|
||||
the app such as adding **"Partial Access" support**. Also includes some minor
|
||||
refactoring, and replacement of deprecated circular progress bars.
|
||||
* Fixed an **UI issue where the 'Subcategories' and 'Parent Categories' tabs
|
||||
appeared blank** in the Category Details screen. Resolved by optimizing view
|
||||
binding handling in the parent fragments.
|
||||
* Fixed an issue where editing depictions removed all other structured data from
|
||||
images. Now, **only depictions are updated, preserving other associated data**.
|
||||
* Fixed **map centering** in the image upload flow to **use GPS EXIF tag location**
|
||||
from pictures and ensured "Show in map app" accurately reflects this location.
|
||||
* Fixed navigation **after uploading via Nearby by directing users to the Uploads
|
||||
activity** instead of returning to Nearby, preventing confusion about needing to
|
||||
upload again.
|
||||
|
||||
### Bug fixes and various changes
|
||||
|
||||
* Improved the "Nearby" feature to fetch labels based on the user's preferred
|
||||
language instead of defaulting to English.
|
||||
* Added a legend to the "Nearby" feature indicating pin statuses: red for items
|
||||
without pictures, green for those with pictures, and grey for items being
|
||||
checked. A floating action button now allows users to toggle the legend's
|
||||
visibility.
|
||||
* Fixed an issue where the "Nominate for deletion" option is shown to logged out
|
||||
users, preventing app errors and crashes.
|
||||
* Updated the regex pattern that filters categories with an year in it to also
|
||||
filter the 2020s.
|
||||
* Fix an issue where past depictions were not shown as suggestions, despite
|
||||
being saved correctly.
|
||||
* Fixed an issue in custom image picker where exiting the media preview showed
|
||||
only the first image and cleared selections. Now, previously selected images
|
||||
are restored correctly after exiting the preview. This was contributed.
|
||||
* Fixed an issue in custom image picker where scrolling behavior did not
|
||||
maintain position after exiting fullscreen preview, ensuring users remain at
|
||||
the same point in their image roll unless actioned images are filtered. This
|
||||
was contributed.
|
||||
* Fixed Nearby map not showing new pins on map move by removing the 2000m scroll
|
||||
threshold and adding an 800ms debounce for smoother pin updates when the map
|
||||
is moved. Queued searches are now canceled on fragment destruction.
|
||||
* Revised author information retrieval to emphasize the custom author name from
|
||||
the metadata instead of the default registered username.
|
||||
* Enhanced notification classification to properly identify "email" type
|
||||
notifications and prompting users to check their e-mail inbox when such
|
||||
notifications are clicked.
|
||||
* Resolved a bug in the language chooser that incorrectly greyed-out previously
|
||||
selected languages, ensuring only the current language is non-selectable during
|
||||
image upload.
|
||||
* Resolved pin color update issue in "Nearby" feature where the pin colour
|
||||
failed to be updated after a successful image upload.
|
||||
|
||||
What's listed here is only a subset of all the changes. Check the full-list of
|
||||
the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v5.0.2...v5.1.0).
|
||||
Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.1.0)
|
||||
for an exhaustive list of changes and the various contributors who contributed the same.
|
||||
|
||||
## v5.0.2
|
||||
|
||||
- Enhanced multi-upload functionality with user prompts to clarify that all images would share the
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ dependencies {
|
|||
implementation "com.google.android.material:material:1.12.0"
|
||||
implementation 'com.karumi:dexter:5.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
|
||||
|
||||
// Jetpack Compose
|
||||
def composeBom = platform('androidx.compose:compose-bom:2024.11.00')
|
||||
|
|
@ -87,6 +89,8 @@ dependencies {
|
|||
// Dependency injector
|
||||
implementation "com.google.dagger:dagger-android:$DAGGER_VERSION"
|
||||
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling'
|
||||
debugImplementation 'androidx.compose.ui:ui-test-manifest'
|
||||
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
||||
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||
annotationProcessor "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
||||
|
|
@ -208,8 +212,8 @@ android {
|
|||
defaultConfig {
|
||||
//applicationId 'fr.free.nrw.commons'
|
||||
|
||||
versionCode 1040
|
||||
versionName '5.0.2'
|
||||
versionCode 1043
|
||||
versionName '5.1.2'
|
||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||
|
||||
minSdkVersion 21
|
||||
|
|
@ -310,6 +314,7 @@ android {
|
|||
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\""
|
||||
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
|
||||
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\""
|
||||
buildConfigField "String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.org/wiki/\""
|
||||
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
||||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
|
||||
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\""
|
||||
|
|
@ -346,6 +351,7 @@ android {
|
|||
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\""
|
||||
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
|
||||
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\""
|
||||
buildConfigField "String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.beta.wmflabs.org/wiki/\""
|
||||
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
||||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
|
||||
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\""
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@
|
|||
android:theme="@style/LightAppTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:appComponentFactory">
|
||||
<activity
|
||||
android:name=".activity.SingleWebViewActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_single_web_view" />
|
||||
<activity
|
||||
android:name=".nearby.WikidataFeedback"
|
||||
android:exported="false" />
|
||||
|
|
@ -258,4 +262,4 @@
|
|||
android:required="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
package fr.free.nrw.commons.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection
|
||||
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* SingleWebViewActivity is a reusable activity webView based on a given url(initial url) and
|
||||
* closes itself when a specified success URL is reached to success url.
|
||||
*/
|
||||
class SingleWebViewActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var cookieJar: CommonsCookieJar
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val url = intent.getStringExtra(VANISH_ACCOUNT_URL)
|
||||
val successUrl = intent.getStringExtra(VANISH_ACCOUNT_SUCCESS_URL)
|
||||
if (url == null || successUrl == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
ApplicationlessInjection
|
||||
.getInstance(applicationContext)
|
||||
.commonsApplicationComponent
|
||||
.inject(this)
|
||||
setCookies(url)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier,
|
||||
title = { Text(getString(R.string.vanish_account)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
// Close the WebView Activity if the user taps the back button
|
||||
finish()
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
// TODO("Add contentDescription)
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
content = {
|
||||
WebViewComponent(
|
||||
url = url,
|
||||
successUrl = successUrl,
|
||||
onSuccess = {
|
||||
// TODO Redirect the user to login screen like we do when the user logout's
|
||||
finish()
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param url The initial URL which we are loading in the WebView.
|
||||
* @param successUrl The URL that, when reached, triggers the `onSuccess` callback.
|
||||
* @param onSuccess A callback that is invoked when the current url of webView is successUrl.
|
||||
* This is used when we want to close when the webView once a success url is hit.
|
||||
* @param modifier An optional [Modifier] to customize the layout or appearance of the WebView.
|
||||
*/
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
private fun WebViewComponent(
|
||||
url: String,
|
||||
successUrl: String,
|
||||
onSuccess: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val webView = remember { mutableStateOf<WebView?>(null) }
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = {
|
||||
WebView(it).apply {
|
||||
settings.apply {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
javaScriptCanOpenWindowsAutomatically = true
|
||||
|
||||
}
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): Boolean {
|
||||
|
||||
request?.url?.let { url ->
|
||||
Timber.d("URL Loading: $url")
|
||||
if (url.toString() == successUrl) {
|
||||
Timber.d("Success URL detected. Closing WebView.")
|
||||
onSuccess() // Close the activity
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
setCookies(url.orEmpty())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
webChromeClient = object : WebChromeClient() {
|
||||
override fun onConsoleMessage(message: ConsoleMessage): Boolean {
|
||||
Timber.d("Console: ${message.message()} -- From line ${message.lineNumber()} of ${message.sourceId()}")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
loadUrl(url)
|
||||
}
|
||||
},
|
||||
update = {
|
||||
webView.value = it
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets cookies for the given URL using the cookies stored in the `CommonsCookieJar`.
|
||||
*
|
||||
* @param url The URL for which cookies need to be set.
|
||||
*/
|
||||
private fun setCookies(url: String) {
|
||||
CookieManager.getInstance().let {
|
||||
val cookies = cookieJar.loadForRequest(url.toHttpUrl())
|
||||
for (cookie in cookies) {
|
||||
it.setCookie(url, cookie.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VANISH_ACCOUNT_URL = "VanishAccountUrl"
|
||||
private const val VANISH_ACCOUNT_SUCCESS_URL = "vanishAccountSuccessUrl"
|
||||
|
||||
/**
|
||||
* Launch the WebViewActivity with the specified URL and success URL.
|
||||
* @param context The context from which the activity is launched.
|
||||
* @param url The initial URL to load in the WebView.
|
||||
* @param successUrl The URL that triggers the WebView to close when matched.
|
||||
*/
|
||||
fun showWebView(
|
||||
context: Context,
|
||||
url: String,
|
||||
successUrl: String
|
||||
) {
|
||||
val intent = Intent(
|
||||
context,
|
||||
SingleWebViewActivity::class.java
|
||||
).apply {
|
||||
putExtra(VANISH_ACCOUNT_URL, url)
|
||||
putExtra(VANISH_ACCOUNT_SUCCESS_URL, successUrl)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,32 +184,32 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
|||
// if progressDialog is visible during the configuration change then store state as true else false so that
|
||||
// we maintain visibility of progressDialog after configuration change
|
||||
if (progressDialog != null && progressDialog!!.isShowing) {
|
||||
outState.putBoolean(saveProgressDialog, true)
|
||||
outState.putBoolean(SAVE_PROGRESS_DIALOG, true)
|
||||
} else {
|
||||
outState.putBoolean(saveProgressDialog, false)
|
||||
outState.putBoolean(SAVE_PROGRESS_DIALOG, false)
|
||||
}
|
||||
outState.putString(
|
||||
saveErrorMessage,
|
||||
SAVE_ERROR_MESSAGE,
|
||||
binding!!.errorMessage.text.toString()
|
||||
) //Save the errorMessage
|
||||
outState.putString(
|
||||
saveUsername,
|
||||
SAVE_USERNAME,
|
||||
binding!!.loginUsername.text.toString()
|
||||
) // Save the username
|
||||
outState.putString(
|
||||
savePassword,
|
||||
SAVE_PASSWORD,
|
||||
binding!!.loginPassword.text.toString()
|
||||
) // Save the password
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
binding!!.loginUsername.setText(savedInstanceState.getString(saveUsername))
|
||||
binding!!.loginPassword.setText(savedInstanceState.getString(savePassword))
|
||||
if (savedInstanceState.getBoolean(saveProgressDialog)) {
|
||||
binding!!.loginUsername.setText(savedInstanceState.getString(SAVE_USERNAME))
|
||||
binding!!.loginPassword.setText(savedInstanceState.getString(SAVE_PASSWORD))
|
||||
if (savedInstanceState.getBoolean(SAVE_PROGRESS_DIALOG)) {
|
||||
performLogin()
|
||||
}
|
||||
val errorMessage = savedInstanceState.getString(saveErrorMessage)
|
||||
val errorMessage = savedInstanceState.getString(SAVE_ERROR_MESSAGE)
|
||||
if (sessionManager.isUserLoggedIn) {
|
||||
showMessage(R.string.login_success, R.color.primaryDarkColor)
|
||||
} else {
|
||||
|
|
@ -396,9 +396,9 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
|||
fun startYourself(context: Context) =
|
||||
context.startActivity(Intent(context, LoginActivity::class.java))
|
||||
|
||||
const val saveProgressDialog: String = "ProgressDialog_state"
|
||||
const val saveErrorMessage: String = "errorMessage"
|
||||
const val saveUsername: String = "username"
|
||||
const val savePassword: String = "password"
|
||||
const val SAVE_PROGRESS_DIALOG: String = "ProgressDialog_state"
|
||||
const val SAVE_ERROR_MESSAGE: String = "errorMessage"
|
||||
const val SAVE_USERNAME: String = "username"
|
||||
const val SAVE_PASSWORD: String = "password"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,30 +127,64 @@ class CategoriesModel
|
|||
/**
|
||||
* Fetches details of every category associated with selected depictions, converts them into
|
||||
* CategoryItem and returns them in a list.
|
||||
* If a selected depiction has no categories, the categories in which its P18 belongs are
|
||||
* returned in the list.
|
||||
*
|
||||
* @param selectedDepictions selected DepictItems
|
||||
* @return List of CategoryItem associated with selected depictions
|
||||
*/
|
||||
private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>): Observable<MutableList<CategoryItem>>? =
|
||||
Observable
|
||||
.fromIterable(
|
||||
selectedDepictions.map { it.commonsCategories }.flatten(),
|
||||
).map { categoryItem ->
|
||||
categoryClient
|
||||
.getCategoriesByName(
|
||||
categoryItem.name,
|
||||
categoryItem.name,
|
||||
SEARCH_CATS_LIMIT,
|
||||
).map {
|
||||
CategoryItem(
|
||||
it[0].name,
|
||||
it[0].description,
|
||||
it[0].thumbnail,
|
||||
it[0].isSelected,
|
||||
)
|
||||
}.blockingGet()
|
||||
}.toList()
|
||||
.toObservable()
|
||||
private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>): Observable<MutableList<CategoryItem>>? {
|
||||
val observables = selectedDepictions.map { depictedItem ->
|
||||
if (depictedItem.commonsCategories.isEmpty()) {
|
||||
if (depictedItem.primaryImage == null) {
|
||||
return@map Observable.just(emptyList<CategoryItem>())
|
||||
}
|
||||
Observable.just(
|
||||
depictedItem.primaryImage
|
||||
).map { image ->
|
||||
categoryClient
|
||||
.getCategoriesOfImage(
|
||||
image,
|
||||
SEARCH_CATS_LIMIT,
|
||||
).map {
|
||||
it.map { category ->
|
||||
CategoryItem(
|
||||
category.name,
|
||||
category.description,
|
||||
category.thumbnail,
|
||||
category.isSelected,
|
||||
)
|
||||
}
|
||||
}.blockingGet()
|
||||
}.flatMapIterable { it }.toList()
|
||||
.toObservable()
|
||||
} else {
|
||||
Observable
|
||||
.fromIterable(
|
||||
depictedItem.commonsCategories,
|
||||
).map { categoryItem ->
|
||||
categoryClient
|
||||
.getCategoriesByName(
|
||||
categoryItem.name,
|
||||
categoryItem.name,
|
||||
SEARCH_CATS_LIMIT,
|
||||
).map {
|
||||
CategoryItem(
|
||||
it[0].name,
|
||||
it[0].description,
|
||||
it[0].thumbnail,
|
||||
it[0].isSelected,
|
||||
)
|
||||
}.blockingGet()
|
||||
}.toList()
|
||||
.toObservable()
|
||||
}
|
||||
}
|
||||
return Observable.concat(observables)
|
||||
.scan(mutableListOf<CategoryItem>()) { accumulator, currentList ->
|
||||
accumulator.apply { addAll(currentList) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches details of every category by their name, converts them into
|
||||
|
|
|
|||
|
|
@ -78,6 +78,24 @@ class CategoryClient
|
|||
),
|
||||
)
|
||||
|
||||
/**
|
||||
* Fetches categories belonging to an image (P18 of some wikidata entity).
|
||||
*
|
||||
* @param image P18 of some wikidata entity
|
||||
* @param itemLimit How many categories to return
|
||||
* @return Single Observable emitting the list of categories
|
||||
*/
|
||||
fun getCategoriesOfImage(
|
||||
image: String,
|
||||
itemLimit: Int,
|
||||
): Single<List<CategoryItem>> =
|
||||
responseMapper(
|
||||
categoryInterface.getCategoriesByTitles(
|
||||
"File:${image}",
|
||||
itemLimit,
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
* The method takes categoryName as input and returns a List of Subcategories
|
||||
* It uses the generator query API to get the subcategories in a category, 500 at a time.
|
||||
|
|
|
|||
|
|
@ -61,6 +61,21 @@ interface CategoryInterface {
|
|||
@Query("gacoffset") offset: Int,
|
||||
): Single<MwQueryResponse>
|
||||
|
||||
/**
|
||||
* Fetches non-hidden categories by titles.
|
||||
*
|
||||
* @param titles titles to fetch categories for (e.g. File:<P18 of a wikidata entity>)
|
||||
* @param itemLimit How many categories to return
|
||||
* @return MwQueryResponse
|
||||
*/
|
||||
@GET(
|
||||
"w/api.php?action=query&format=json&formatversion=2&generator=categories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70&gclshow=!hidden",
|
||||
)
|
||||
fun getCategoriesByTitles(
|
||||
@Query("titles") titles: String?,
|
||||
@Query("gcllimit") itemLimit: Int,
|
||||
): Single<MwQueryResponse>
|
||||
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50")
|
||||
fun getSubCategoryList(
|
||||
@Query("gcmtitle") categoryName: String,
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
package fr.free.nrw.commons.concurrency;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
|
||||
public class BackgroundPoolExceptionHandler implements ExceptionHandler {
|
||||
/**
|
||||
* If an exception occurs on a background thread, this handler will crash for debug builds
|
||||
* but fail silently for release builds.
|
||||
* @param t
|
||||
*/
|
||||
@Override
|
||||
public void onException(@NonNull final Throwable t) {
|
||||
//Crash for debug build
|
||||
if (BuildConfig.DEBUG) {
|
||||
Thread thread = new Thread(() -> {
|
||||
throw new RuntimeException(t);
|
||||
});
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package fr.free.nrw.commons.concurrency
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig
|
||||
|
||||
|
||||
class BackgroundPoolExceptionHandler : ExceptionHandler {
|
||||
/**
|
||||
* If an exception occurs on a background thread, this handler will crash for debug builds
|
||||
* but fail silently for release builds.
|
||||
* @param t
|
||||
*/
|
||||
override fun onException(t: Throwable) {
|
||||
// Crash for debug build
|
||||
if (BuildConfig.DEBUG) {
|
||||
val thread = Thread {
|
||||
throw RuntimeException(t)
|
||||
}
|
||||
thread.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
package fr.free.nrw.commons.concurrency;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
class ExceptionAwareThreadPoolExecutor extends ScheduledThreadPoolExecutor {
|
||||
|
||||
private final ExceptionHandler exceptionHandler;
|
||||
|
||||
public ExceptionAwareThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory,
|
||||
ExceptionHandler exceptionHandler) {
|
||||
super(corePoolSize, threadFactory);
|
||||
this.exceptionHandler = exceptionHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterExecute(Runnable r, Throwable t) {
|
||||
super.afterExecute(r, t);
|
||||
if (t == null && r instanceof Future<?>) {
|
||||
try {
|
||||
Future<?> future = (Future<?>) r;
|
||||
if (future.isDone()) future.get();
|
||||
} catch (CancellationException | InterruptedException e) {
|
||||
//ignore
|
||||
} catch (ExecutionException e) {
|
||||
t = e.getCause() != null ? e.getCause() : e;
|
||||
} catch (Exception e) {
|
||||
t = e;
|
||||
}
|
||||
}
|
||||
|
||||
if (t != null) {
|
||||
exceptionHandler.onException(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package fr.free.nrw.commons.concurrency
|
||||
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor
|
||||
import java.util.concurrent.ThreadFactory
|
||||
|
||||
|
||||
class ExceptionAwareThreadPoolExecutor(
|
||||
corePoolSize: Int,
|
||||
threadFactory: ThreadFactory,
|
||||
private val exceptionHandler: ExceptionHandler?
|
||||
) : ScheduledThreadPoolExecutor(corePoolSize, threadFactory) {
|
||||
|
||||
override fun afterExecute(r: Runnable, t: Throwable?) {
|
||||
super.afterExecute(r, t)
|
||||
var throwable = t
|
||||
|
||||
if (throwable == null && r is Future<*>) {
|
||||
try {
|
||||
if (r.isDone) {
|
||||
r.get()
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
// ignore
|
||||
} catch (e: InterruptedException) {
|
||||
// ignore
|
||||
} catch (e: ExecutionException) {
|
||||
throwable = e.cause ?: e
|
||||
} catch (e: Exception) {
|
||||
throwable = e
|
||||
}
|
||||
}
|
||||
|
||||
throwable?.let {
|
||||
exceptionHandler?.onException(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package fr.free.nrw.commons.concurrency;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface ExceptionHandler {
|
||||
void onException(@NonNull Throwable t);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package fr.free.nrw.commons.concurrency
|
||||
|
||||
interface ExceptionHandler {
|
||||
|
||||
fun onException(t: Throwable)
|
||||
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
package fr.free.nrw.commons.concurrency;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* This class is a thread pool which provides some additional features:
|
||||
* - it sets the thread priority to a value lower than foreground priority by default, or you can
|
||||
* supply your own priority
|
||||
* - it gives you a way to handle exceptions thrown in the thread pool
|
||||
*/
|
||||
|
||||
public class ThreadPoolService implements Executor {
|
||||
private final ScheduledThreadPoolExecutor backgroundPool;
|
||||
|
||||
private ThreadPoolService(final Builder b) {
|
||||
backgroundPool = new ExceptionAwareThreadPoolExecutor(b.poolSize,
|
||||
new ThreadFactory() {
|
||||
int count = 0;
|
||||
@Override
|
||||
public Thread newThread(@NonNull Runnable r) {
|
||||
count++;
|
||||
Thread t = new Thread(r, String.format("%s-%s", b.name, count));
|
||||
//If the priority is specified out of range, we set the thread priority to Thread.MIN_PRIORITY
|
||||
//It's done prevent IllegalArgumentException and to prevent setting of improper high priority for a less priority task
|
||||
t.setPriority(b.priority > Thread.MAX_PRIORITY || b.priority < Thread.MIN_PRIORITY ?
|
||||
Thread.MIN_PRIORITY : b.priority);
|
||||
return t;
|
||||
}
|
||||
}, b.exceptionHandler);
|
||||
}
|
||||
|
||||
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long time, TimeUnit timeUnit) {
|
||||
return backgroundPool.schedule(callable, time, timeUnit);
|
||||
}
|
||||
|
||||
public ScheduledFuture<?> schedule(Runnable runnable) {
|
||||
return schedule(runnable, 0, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public ScheduledFuture<?> schedule(Runnable runnable, long time, TimeUnit timeUnit) {
|
||||
return backgroundPool.schedule(runnable, time, timeUnit);
|
||||
}
|
||||
|
||||
public ScheduledFuture<?> scheduleAtFixedRate(final Runnable task, long initialDelay,
|
||||
long period, final TimeUnit timeUnit) {
|
||||
return backgroundPool.scheduleAtFixedRate(task, initialDelay, period, timeUnit);
|
||||
}
|
||||
|
||||
public ScheduledThreadPoolExecutor executor() {
|
||||
return backgroundPool;
|
||||
}
|
||||
|
||||
public void shutdown(){
|
||||
backgroundPool.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
backgroundPool.execute(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for {@link ThreadPoolService}
|
||||
*/
|
||||
public static class Builder {
|
||||
//Required
|
||||
private final String name;
|
||||
|
||||
//Optional
|
||||
private int poolSize = 1;
|
||||
private int priority = Thread.MIN_PRIORITY;
|
||||
private ExceptionHandler exceptionHandler = null;
|
||||
|
||||
/**
|
||||
* @param name the name of the threads in the service. if there are N threads,
|
||||
* the thread names will be like name-1, name-2, name-3,...,name-N
|
||||
*/
|
||||
public Builder(@NonNull String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param poolSize the number of threads to keep in the pool
|
||||
* @throws IllegalArgumentException if size of pool <=0
|
||||
*/
|
||||
public Builder setPoolSize(int poolSize) throws IllegalArgumentException {
|
||||
if (poolSize <= 0) {
|
||||
throw new IllegalArgumentException("Pool size must be grater than 0");
|
||||
}
|
||||
this.poolSize = poolSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param priority Priority of the threads in the service. You can supply a constant from
|
||||
* {@link java.lang.Thread} or
|
||||
* specify your own priority in the range 1(MIN_PRIORITY) to 10(MAX_PRIORITY)
|
||||
* By default, the priority is set to {@link java.lang.Thread#MIN_PRIORITY}
|
||||
*/
|
||||
public Builder setPriority(int priority) {
|
||||
this.priority = priority;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param handler The handler to use to handle exceptions in the service
|
||||
*/
|
||||
public Builder setExceptionHandler(ExceptionHandler handler) {
|
||||
this.exceptionHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ThreadPoolService build() {
|
||||
return new ThreadPoolService(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package fr.free.nrw.commons.concurrency
|
||||
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
/**
|
||||
* This class is a thread pool which provides some additional features:
|
||||
* - it sets the thread priority to a value lower than foreground priority by default, or you can
|
||||
* supply your own priority
|
||||
* - it gives you a way to handle exceptions thrown in the thread pool
|
||||
*/
|
||||
class ThreadPoolService private constructor(builder: Builder) : Executor {
|
||||
private val backgroundPool: ScheduledThreadPoolExecutor = ExceptionAwareThreadPoolExecutor(
|
||||
builder.poolSize,
|
||||
object : ThreadFactory {
|
||||
private var count = 0
|
||||
override fun newThread(r: Runnable): Thread {
|
||||
count++
|
||||
val t = Thread(r, "${builder.name}-$count")
|
||||
// If the priority is specified out of range, we set the thread priority to
|
||||
// Thread.MIN_PRIORITY
|
||||
// It's done to prevent IllegalArgumentException and to prevent setting of
|
||||
// improper high priority for a less priority task
|
||||
t.priority =
|
||||
if (
|
||||
builder.priority > Thread.MAX_PRIORITY
|
||||
||
|
||||
builder.priority < Thread.MIN_PRIORITY
|
||||
) {
|
||||
Thread.MIN_PRIORITY
|
||||
} else {
|
||||
builder.priority
|
||||
}
|
||||
return t
|
||||
}
|
||||
},
|
||||
builder.exceptionHandler
|
||||
)
|
||||
|
||||
fun <V> schedule(callable: Callable<V>, time: Long, timeUnit: TimeUnit): ScheduledFuture<V> {
|
||||
return backgroundPool.schedule(callable, time, timeUnit)
|
||||
}
|
||||
|
||||
fun schedule(runnable: Runnable): ScheduledFuture<*> {
|
||||
return schedule(runnable, 0, TimeUnit.SECONDS)
|
||||
}
|
||||
|
||||
fun schedule(runnable: Runnable, time: Long, timeUnit: TimeUnit): ScheduledFuture<*> {
|
||||
return backgroundPool.schedule(runnable, time, timeUnit)
|
||||
}
|
||||
|
||||
fun scheduleAtFixedRate(
|
||||
task: Runnable,
|
||||
initialDelay: Long,
|
||||
period: Long,
|
||||
timeUnit: TimeUnit
|
||||
): ScheduledFuture<*> {
|
||||
return backgroundPool.scheduleWithFixedDelay(task, initialDelay, period, timeUnit)
|
||||
}
|
||||
|
||||
fun executor(): ScheduledThreadPoolExecutor {
|
||||
return backgroundPool
|
||||
}
|
||||
|
||||
fun shutdown() {
|
||||
backgroundPool.shutdown()
|
||||
}
|
||||
|
||||
override fun execute(command: Runnable) {
|
||||
backgroundPool.execute(command)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for [ThreadPoolService]
|
||||
*/
|
||||
class Builder(val name: String) {
|
||||
var poolSize: Int = 1
|
||||
var priority: Int = Thread.MIN_PRIORITY
|
||||
var exceptionHandler: ExceptionHandler? = null
|
||||
|
||||
/**
|
||||
* @param poolSize the number of threads to keep in the pool
|
||||
* @throws IllegalArgumentException if size of pool <= 0
|
||||
*/
|
||||
fun setPoolSize(poolSize: Int): Builder {
|
||||
if (poolSize <= 0) {
|
||||
throw IllegalArgumentException("Pool size must be greater than 0")
|
||||
}
|
||||
this.poolSize = poolSize
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @param priority Priority of the threads in the service. You can supply a constant from
|
||||
* [java.lang.Thread] or
|
||||
* specify your own priority in the range 1(MIN_PRIORITY)
|
||||
* to 10(MAX_PRIORITY)
|
||||
* By default, the priority is set to [java.lang.Thread.MIN_PRIORITY]
|
||||
*/
|
||||
fun setPriority(priority: Int): Builder {
|
||||
this.priority = priority
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @param handler The handler to use to handle exceptions in the service
|
||||
*/
|
||||
fun setExceptionHandler(handler: ExceptionHandler): Builder {
|
||||
exceptionHandler = handler
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): ThreadPoolService {
|
||||
return ThreadPoolService(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@ data class Contribution constructor(
|
|||
*/
|
||||
fun formatDescriptions(descriptions: List<UploadMediaDetail>) =
|
||||
descriptions
|
||||
.filter { it.descriptionText.isNotEmpty() }
|
||||
.filter { !it.descriptionText.isNullOrEmpty() }
|
||||
.joinToString(separator = "") { "{{${it.languageCode}|1=${it.descriptionText}}}" }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,16 +61,16 @@ class CoordinateEditHelper @Inject constructor(
|
|||
/**
|
||||
* Replaces new coordinates
|
||||
* @param media to be added
|
||||
* @param Latitude to be added
|
||||
* @param Longitude to be added
|
||||
* @param Accuracy to be added
|
||||
* @param latitude to be added
|
||||
* @param longitude to be added
|
||||
* @param accuracy to be added
|
||||
* @return Observable<Boolean>
|
||||
*/
|
||||
private fun addCoordinates(
|
||||
media: Media,
|
||||
Latitude: String,
|
||||
Longitude: String,
|
||||
Accuracy: String
|
||||
latitude: String,
|
||||
longitude: String,
|
||||
accuracy: String
|
||||
): Observable<Boolean>? {
|
||||
Timber.d("thread is coordinates adding %s", Thread.currentThread().getName())
|
||||
val summary = "Adding Coordinates"
|
||||
|
|
@ -83,9 +83,9 @@ class CoordinateEditHelper @Inject constructor(
|
|||
.blockingGet()
|
||||
}
|
||||
|
||||
if (Latitude != null) {
|
||||
buffer.append("\n{{Location|").append(Latitude).append("|").append(Longitude)
|
||||
.append("|").append(Accuracy).append("}}")
|
||||
if (latitude != null) {
|
||||
buffer.append("\n{{Location|").append(latitude).append("|").append(longitude)
|
||||
.append("|").append(accuracy).append("}}")
|
||||
}
|
||||
|
||||
val editedLocation = buffer.toString()
|
||||
|
|
@ -141,7 +141,7 @@ class CoordinateEditHelper @Inject constructor(
|
|||
* @param media to be added
|
||||
* @param latitude to be added
|
||||
* @param longitude to be added
|
||||
* @param Accuracy to be added
|
||||
* @param accuracy to be added
|
||||
* @param result to be added
|
||||
* @return boolean
|
||||
*/
|
||||
|
|
@ -150,7 +150,7 @@ class CoordinateEditHelper @Inject constructor(
|
|||
media: Media,
|
||||
latitude: String,
|
||||
longitude: String,
|
||||
Accuracy: String,
|
||||
accuracy: String,
|
||||
result: Boolean
|
||||
): Boolean {
|
||||
val message: String
|
||||
|
|
@ -160,7 +160,7 @@ class CoordinateEditHelper @Inject constructor(
|
|||
media.coordinates = fr.free.nrw.commons.location.LatLng(
|
||||
latitude.toDouble(),
|
||||
longitude.toDouble(),
|
||||
Accuracy.toFloat()
|
||||
accuracy.toFloat()
|
||||
)
|
||||
title += ": " + context.getString(R.string.coordinates_edit_helper_show_edit_title_success)
|
||||
val coordinatesInMessage = StringBuilder()
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ object FolderDeletionHelper {
|
|||
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(context.getString(R.string.custom_selector_confirm_deletion_title))
|
||||
.setCancelable(false)
|
||||
.setMessage(
|
||||
context.getString(
|
||||
R.string.custom_selector_confirm_deletion_message,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.TreeMap
|
||||
import kotlin.collections.ArrayList
|
||||
|
|
@ -103,6 +104,18 @@ class ImageAdapter(
|
|||
*/
|
||||
private var imagePositionAsPerIncreasingOrder = 0
|
||||
|
||||
/**
|
||||
* Stores the number of images currently visible on the screen
|
||||
*/
|
||||
private val _currentImagesCount = MutableStateFlow(0)
|
||||
val currentImagesCount = _currentImagesCount
|
||||
|
||||
/**
|
||||
* Stores whether images are being loaded or not
|
||||
*/
|
||||
private val _isLoadingImages = MutableStateFlow(false)
|
||||
val isLoadingImages = _isLoadingImages
|
||||
|
||||
/**
|
||||
* Coroutine Dispatchers and Scope.
|
||||
*/
|
||||
|
|
@ -184,8 +197,12 @@ class ImageAdapter(
|
|||
// If the position is not already visited, that means the position is new then
|
||||
// finds the next actionable image position from all images
|
||||
if (!alreadyAddedPositions.contains(position)) {
|
||||
processThumbnailForActionedImage(holder, position, uploadingContributionList)
|
||||
|
||||
processThumbnailForActionedImage(
|
||||
holder,
|
||||
position,
|
||||
uploadingContributionList
|
||||
)
|
||||
_isLoadingImages.value = false
|
||||
// If the position is already visited, that means the image is already present
|
||||
// inside map, so it will fetch the image from the map and load in the holder
|
||||
} else {
|
||||
|
|
@ -231,6 +248,7 @@ class ImageAdapter(
|
|||
position: Int,
|
||||
uploadingContributionList: List<Contribution>,
|
||||
) {
|
||||
_isLoadingImages.value = true
|
||||
val next =
|
||||
imageLoader.nextActionableImage(
|
||||
allImages,
|
||||
|
|
@ -252,6 +270,7 @@ class ImageAdapter(
|
|||
actionableImagesMap[next] = allImages[next]
|
||||
alreadyAddedPositions.add(imagePositionAsPerIncreasingOrder)
|
||||
imagePositionAsPerIncreasingOrder++
|
||||
_currentImagesCount.value = imagePositionAsPerIncreasingOrder
|
||||
Glide
|
||||
.with(holder.image)
|
||||
.load(allImages[next].uri)
|
||||
|
|
@ -267,6 +286,7 @@ class ImageAdapter(
|
|||
reachedEndOfFolder = true
|
||||
notifyItemRemoved(position)
|
||||
}
|
||||
_isLoadingImages.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -372,6 +392,7 @@ class ImageAdapter(
|
|||
emptyMap: TreeMap<Int, Image>,
|
||||
uploadedImages: List<Contribution> = ArrayList(),
|
||||
) {
|
||||
_isLoadingImages.value = true
|
||||
allImages = fixedImages
|
||||
val oldImageList: ArrayList<Image> = images
|
||||
val newImageList: ArrayList<Image> = ArrayList(newImages)
|
||||
|
|
@ -382,6 +403,7 @@ class ImageAdapter(
|
|||
reachedEndOfFolder = false
|
||||
selectedImages = ArrayList()
|
||||
imagePositionAsPerIncreasingOrder = 0
|
||||
_currentImagesCount.value = imagePositionAsPerIncreasingOrder
|
||||
val diffResult =
|
||||
DiffUtil.calculateDiff(
|
||||
ImagesDiffCallback(oldImageList, newImageList),
|
||||
|
|
@ -441,6 +463,7 @@ class ImageAdapter(
|
|||
val entry = iterator.next()
|
||||
if (entry.value == image) {
|
||||
imagePositionAsPerIncreasingOrder -= 1
|
||||
_currentImagesCount.value = imagePositionAsPerIncreasingOrder
|
||||
iterator.remove()
|
||||
alreadyAddedPositions.removeAt(alreadyAddedPositions.size - 1)
|
||||
notifyItemRemoved(index)
|
||||
|
|
|
|||
|
|
@ -268,9 +268,10 @@ class CustomSelectorActivity :
|
|||
*/
|
||||
private fun showWelcomeDialog() {
|
||||
val dialog = Dialog(this)
|
||||
dialog.setCancelable(false)
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
dialog.setContentView(R.layout.custom_selector_info_dialog)
|
||||
(dialog.findViewById(R.id.btn_ok) as Button).setOnClickListener { dialog.dismiss() }
|
||||
(dialog.findViewById<Button>(R.id.btn_ok))?.setOnClickListener { dialog.dismiss() }
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
|
|
@ -683,10 +684,11 @@ class CustomSelectorActivity :
|
|||
*/
|
||||
private fun displayUploadLimitWarning() {
|
||||
val dialog = Dialog(this)
|
||||
dialog.setCancelable(false)
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
dialog.setContentView(R.layout.custom_selector_limit_dialog)
|
||||
(dialog.findViewById(R.id.btn_dismiss_limit_warning) as Button).setOnClickListener { dialog.dismiss() }
|
||||
(dialog.findViewById(R.id.upload_limit_warning) as TextView).text =
|
||||
(dialog.findViewById<Button>(R.id.btn_dismiss_limit_warning))?.setOnClickListener { dialog.dismiss() }
|
||||
(dialog.findViewById<TextView>(R.id.upload_limit_warning))?.text =
|
||||
resources.getString(
|
||||
R.string.custom_selector_over_limit_warning,
|
||||
uploadLimit,
|
||||
|
|
|
|||
|
|
@ -12,8 +12,12 @@ import android.widget.ProgressBar
|
|||
import android.widget.Switch
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import fr.free.nrw.commons.contributions.Contribution
|
||||
|
|
@ -38,6 +42,10 @@ import fr.free.nrw.commons.theme.BaseActivity
|
|||
import fr.free.nrw.commons.upload.FileProcessor
|
||||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.TreeMap
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.ArrayList
|
||||
|
|
@ -80,6 +88,12 @@ class ImageFragment :
|
|||
*/
|
||||
var allImages: ArrayList<Image> = ArrayList()
|
||||
|
||||
/**
|
||||
* Keeps track of switch state
|
||||
*/
|
||||
private val _switchState = MutableStateFlow(false)
|
||||
val switchState = _switchState.asStateFlow()
|
||||
|
||||
/**
|
||||
* View model Factory.
|
||||
*/
|
||||
|
|
@ -214,7 +228,11 @@ class ImageFragment :
|
|||
|
||||
switch = binding?.switchWidget
|
||||
switch?.visibility = View.VISIBLE
|
||||
switch?.setOnCheckedChangeListener { _, isChecked -> onChangeSwitchState(isChecked) }
|
||||
_switchState.value = switch?.isChecked ?: false
|
||||
switch?.setOnCheckedChangeListener { _, isChecked ->
|
||||
onChangeSwitchState(isChecked)
|
||||
_switchState.value = isChecked
|
||||
}
|
||||
selectorRV = binding?.selectorRv
|
||||
loader = binding?.loader
|
||||
progressLayout = binding?.progressLayout
|
||||
|
|
@ -234,6 +252,28 @@ class ImageFragment :
|
|||
return binding?.root
|
||||
}
|
||||
|
||||
/**
|
||||
* onViewCreated
|
||||
* Updates empty text view visibility based on image count, switch state, and loading status.
|
||||
*/
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
combine(
|
||||
imageAdapter.currentImagesCount,
|
||||
switchState,
|
||||
imageAdapter.isLoadingImages
|
||||
) { imageCount, isChecked, isLoadingImages ->
|
||||
Triple(imageCount, isChecked, isLoadingImages)
|
||||
}.collect { (imageCount, isChecked, isLoadingImages) ->
|
||||
binding?.allImagesUploadedOrMarked?.isVisible =
|
||||
!isLoadingImages && !isChecked && imageCount == 0 && (switch?.isVisible == true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChangeSwitchState(checked: Boolean) {
|
||||
if (checked) {
|
||||
showAlreadyActionedImages = true
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class DBOpenHelper(
|
|||
|
||||
companion object {
|
||||
private const val DATABASE_NAME = "commons.db"
|
||||
private const val DATABASE_VERSION = 20
|
||||
private const val DATABASE_VERSION = 21
|
||||
const val CONTRIBUTIONS_TABLE = "contributions"
|
||||
private const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS %s"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package fr.free.nrw.commons.description
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.speech.RecognizerIntent
|
||||
|
|
@ -72,7 +71,7 @@ class DescriptionEditActivity :
|
|||
|
||||
private lateinit var binding: ActivityDescriptionEditBinding
|
||||
|
||||
private var descriptionAndCaptions: ArrayList<UploadMediaDetail>? = null
|
||||
private var descriptionAndCaptions: MutableList<UploadMediaDetail>? = null
|
||||
|
||||
private val voiceInputResultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
|
|
@ -114,22 +113,18 @@ class DescriptionEditActivity :
|
|||
* Initializes the RecyclerView
|
||||
* @param descriptionAndCaptions list of description and caption
|
||||
*/
|
||||
private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) {
|
||||
private fun initRecyclerView(descriptionAndCaptions: MutableList<UploadMediaDetail>?) {
|
||||
uploadMediaDetailAdapter =
|
||||
UploadMediaDetailAdapter(
|
||||
this,
|
||||
savedLanguageValue,
|
||||
descriptionAndCaptions,
|
||||
descriptionAndCaptions ?: mutableListOf(),
|
||||
recentLanguagesDao,
|
||||
voiceInputResultLauncher
|
||||
)
|
||||
uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int ->
|
||||
showInfoAlert(
|
||||
titleStringID,
|
||||
messageStringId,
|
||||
)
|
||||
}
|
||||
uploadMediaDetailAdapter.setEventListener(this)
|
||||
|
||||
uploadMediaDetailAdapter.callback = UploadMediaDetailAdapter.Callback(::showInfoAlert)
|
||||
uploadMediaDetailAdapter.eventListener = this
|
||||
rvDescriptions = binding.rvDescriptionsCaptions
|
||||
rvDescriptions!!.layoutManager = LinearLayoutManager(this)
|
||||
rvDescriptions!!.adapter = uploadMediaDetailAdapter
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import dagger.android.AndroidInjectionModule
|
|||
import dagger.android.AndroidInjector
|
||||
import dagger.android.support.AndroidSupportInjectionModule
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.activity.SingleWebViewActivity
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.contributions.ContributionsModule
|
||||
import fr.free.nrw.commons.explore.SearchModule
|
||||
|
|
@ -51,6 +52,8 @@ interface CommonsApplicationComponent : AndroidInjector<ApplicationlessInjection
|
|||
|
||||
fun inject(activity: LoginActivity)
|
||||
|
||||
fun inject(activity: SingleWebViewActivity)
|
||||
|
||||
fun inject(fragment: SettingsFragment)
|
||||
|
||||
fun inject(fragment: MoreBottomSheetFragment)
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
|
|||
.setPositiveButton(android.R.string.yes,
|
||||
(dialog, which) -> setDeleteRecentPositiveButton(context, dialog))
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
|
@ -94,6 +95,7 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
|
|||
.setPositiveButton(getString(R.string.delete).toUpperCase(Locale.ROOT),
|
||||
((dialog, which) -> setDeletePositiveButton(context, dialog, position)))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ class FeedbackDialog(
|
|||
// 'SOFT_INPUT_ADJUST_RESIZE: Int' is deprecated. Deprecated in Java
|
||||
@Suppress("DEPRECATION")
|
||||
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
binding.btnCancel.setOnClickListener { dismiss() }
|
||||
binding.btnSubmitFeedback.setOnClickListener {
|
||||
try {
|
||||
submitFeedback()
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ import fr.free.nrw.commons.location.LocationPermissionsHelper
|
|||
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback
|
||||
import fr.free.nrw.commons.location.LocationServiceManager
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Companion.LAST_LOCATION
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Companion.LAST_ZOOM
|
||||
import fr.free.nrw.commons.utils.DialogUtil
|
||||
import fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
|
|
|
|||
|
|
@ -467,18 +467,35 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the ContributionsFragment that is potentially the parent, grandparent, etc
|
||||
* fragment of this fragment.
|
||||
*
|
||||
* @return The ContributionsFragment instance. If the ContributionsFragment instance could not
|
||||
* be found, null is returned.
|
||||
*/
|
||||
private fun getContributionsFragmentParent(): ContributionsFragment? {
|
||||
var fragment: Fragment? = this
|
||||
|
||||
while (fragment != null && fragment !is ContributionsFragment) {
|
||||
fragment = fragment.parentFragment
|
||||
}
|
||||
|
||||
if (fragment == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return fragment as ContributionsFragment
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (parentFragment != null && requireParentFragment().parentFragment != null) {
|
||||
// Added a check because, not necessarily, the parent fragment
|
||||
// will have a parent fragment, say in the case when MediaDetailPagerFragment
|
||||
// is directly started by the CategoryImagesActivity
|
||||
if (parentFragment is ContributionsFragment) {
|
||||
(((parentFragment as ContributionsFragment)
|
||||
.parentFragment) as ContributionsFragment).binding.cardViewNearby.visibility =
|
||||
View.GONE
|
||||
}
|
||||
|
||||
val contributionsFragment: ContributionsFragment? = this.getContributionsFragmentParent()
|
||||
if (contributionsFragment?.binding != null) {
|
||||
contributionsFragment.binding.cardViewNearby.visibility = View.GONE
|
||||
}
|
||||
|
||||
// detail provider is null when fragment is shown in review activity
|
||||
media = if (detailProvider != null) {
|
||||
detailProvider!!.getMediaAtPosition(index)
|
||||
|
|
@ -1737,10 +1754,11 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
|
|||
return
|
||||
}
|
||||
ProfileActivity.startYourself(
|
||||
activity,
|
||||
media!!.user,
|
||||
sessionManager.userName != media!!.user
|
||||
requireActivity(), // Ensure this is a non-null Activity context
|
||||
media?.user ?: "", // Provide a fallback value if media?.user is null
|
||||
sessionManager.userName != media?.user // This can remain as is, null check will apply
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
builder.setItems(R.array.report_violation_options, (dialog, which) -> {
|
||||
sendReportEmail(media, values[which]);
|
||||
});
|
||||
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {});
|
||||
builder.setCancelable(false);
|
||||
builder.show();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,10 @@ class MoreBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
override fun onFeedbackSubmit(feedback: Feedback) {
|
||||
uploadFeedback(feedback)
|
||||
}
|
||||
}).show()
|
||||
}).apply {
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ class MoreBottomSheetLoggedOutFragment : BottomSheetDialogFragment() {
|
|||
.setMessage(R.string.feedback_sharing_data_alert)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> sendFeedback() }
|
||||
.setNegativeButton(R.string.cancel){_,_ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import android.os.Parcel;
|
|||
import android.os.Parcelable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.Embedded;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.PrimaryKey;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
|
|
@ -24,6 +25,7 @@ public class Place implements Parcelable {
|
|||
public String name;
|
||||
private Label label;
|
||||
private String longDescription;
|
||||
@Embedded
|
||||
public LatLng location;
|
||||
@PrimaryKey @NonNull
|
||||
public String entityID;
|
||||
|
|
|
|||
|
|
@ -41,15 +41,18 @@ fun placeAdapterDelegate(
|
|||
showOrHideAndScrollToIfLast()
|
||||
onItemClick?.invoke(item)
|
||||
}
|
||||
root.setOnFocusChangeListener { view1: View?, hasFocus: Boolean ->
|
||||
root.setOnFocusChangeListener { _: View?, hasFocus: Boolean ->
|
||||
val parentView = root.parent.parent.parent as? RelativeLayout
|
||||
val bottomSheetBehavior = parentView?.let { BottomSheetBehavior.from(it) }
|
||||
|
||||
// Hide button layout if focus is lost, otherwise show it if it's not already visible
|
||||
if (!hasFocus && nearbyButtonLayout.buttonLayout.isShown) {
|
||||
nearbyButtonLayout.buttonLayout.visibility = GONE
|
||||
} else if (hasFocus && !nearbyButtonLayout.buttonLayout.isShown &&
|
||||
BottomSheetBehavior.from(root.parent.parent.parent as RelativeLayout).state !=
|
||||
BottomSheetBehavior.STATE_HIDDEN
|
||||
) {
|
||||
showOrHideAndScrollToIfLast()
|
||||
onItemClick?.invoke(item)
|
||||
} else if (hasFocus && !nearbyButtonLayout.buttonLayout.isShown) {
|
||||
if (bottomSheetBehavior?.state != BottomSheetBehavior.STATE_HIDDEN) {
|
||||
showOrHideAndScrollToIfLast()
|
||||
onItemClick?.invoke(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
nearbyButtonLayout.cameraButton.setOnClickListener { onCameraClicked(item, inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult) }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.room.Insert;
|
|||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
import io.reactivex.Completable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Data Access Object (DAO) for accessing the Place entity in the database.
|
||||
|
|
@ -32,6 +33,20 @@ public abstract class PlaceDao {
|
|||
@Query("SELECT * from place WHERE entityID=:entity")
|
||||
public abstract Place getPlace(String entity);
|
||||
|
||||
/**
|
||||
* Retrieves a list of places within the specified rectangular area.
|
||||
*
|
||||
* @param latBegin Latitudinal lower bound
|
||||
* @param lngBegin Longitudinal lower bound
|
||||
* @param latEnd Latitudinal upper bound, should be greater than `latBegin`
|
||||
* @param lngEnd Longitudinal upper bound, should be greater than `lngBegin`
|
||||
* @return The list of places within the specified rectangular geographical area.
|
||||
*/
|
||||
@Query("SELECT * from place WHERE name!='' AND latitude>=:latBegin AND longitude>=:lngBegin "
|
||||
+ "AND latitude<:latEnd AND longitude<:lngEnd")
|
||||
public abstract List<Place> fetchPlaces(double latBegin, double lngBegin,
|
||||
double latEnd, double lngEnd);
|
||||
|
||||
/**
|
||||
* Saves a Place object asynchronously into the database.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
package fr.free.nrw.commons.nearby;
|
||||
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import io.reactivex.Completable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* The LocalDataSource class for Places
|
||||
|
|
@ -26,6 +30,81 @@ public class PlacesLocalDataSource {
|
|||
return placeDao.getPlace(entityID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of places from the database within the geographical area
|
||||
* specified by map's opposite corners.
|
||||
*
|
||||
* @param mapBottomLeft Bottom left corner of the map.
|
||||
* @param mapTopRight Top right corner of the map.
|
||||
* @return The list of saved places within the map's view.
|
||||
*/
|
||||
public List<Place> fetchPlaces(final LatLng mapBottomLeft, final LatLng mapTopRight) {
|
||||
class Constraint {
|
||||
|
||||
final double latBegin;
|
||||
final double lngBegin;
|
||||
final double latEnd;
|
||||
final double lngEnd;
|
||||
|
||||
public Constraint(final double latBegin, final double lngBegin, final double latEnd,
|
||||
final double lngEnd) {
|
||||
this.latBegin = latBegin;
|
||||
this.lngBegin = lngBegin;
|
||||
this.latEnd = latEnd;
|
||||
this.lngEnd = lngEnd;
|
||||
}
|
||||
}
|
||||
|
||||
final List<Constraint> constraints = new ArrayList<>();
|
||||
|
||||
if (mapTopRight.getLatitude() < mapBottomLeft.getLatitude()) {
|
||||
if (mapTopRight.getLongitude() < mapBottomLeft.getLongitude()) {
|
||||
constraints.add(
|
||||
new Constraint(mapBottomLeft.getLatitude(), mapBottomLeft.getLongitude(), 90.0,
|
||||
180.0));
|
||||
constraints.add(new Constraint(mapBottomLeft.getLatitude(), -180.0, 90.0,
|
||||
mapTopRight.getLongitude()));
|
||||
constraints.add(
|
||||
new Constraint(-90.0, mapBottomLeft.getLongitude(), mapTopRight.getLatitude(),
|
||||
180.0));
|
||||
constraints.add(new Constraint(-90.0, -180.0, mapTopRight.getLatitude(),
|
||||
mapTopRight.getLongitude()));
|
||||
} else {
|
||||
constraints.add(
|
||||
new Constraint(mapBottomLeft.getLatitude(), mapBottomLeft.getLongitude(), 90.0,
|
||||
mapTopRight.getLongitude()));
|
||||
constraints.add(
|
||||
new Constraint(-90.0, mapBottomLeft.getLongitude(), mapTopRight.getLatitude(),
|
||||
mapTopRight.getLongitude()));
|
||||
}
|
||||
} else {
|
||||
if (mapTopRight.getLongitude() < mapBottomLeft.getLongitude()) {
|
||||
constraints.add(
|
||||
new Constraint(mapBottomLeft.getLatitude(), mapBottomLeft.getLongitude(),
|
||||
mapTopRight.getLatitude(), 180.0));
|
||||
constraints.add(
|
||||
new Constraint(mapBottomLeft.getLatitude(), -180.0, mapTopRight.getLatitude(),
|
||||
mapTopRight.getLongitude()));
|
||||
} else {
|
||||
constraints.add(
|
||||
new Constraint(mapBottomLeft.getLatitude(), mapBottomLeft.getLongitude(),
|
||||
mapTopRight.getLatitude(), mapTopRight.getLongitude()));
|
||||
}
|
||||
}
|
||||
|
||||
final List<Place> cachedPlaces = new ArrayList<>();
|
||||
for (final Constraint constraint : constraints) {
|
||||
cachedPlaces.addAll(placeDao.fetchPlaces(
|
||||
constraint.latBegin,
|
||||
constraint.lngBegin,
|
||||
constraint.latEnd,
|
||||
constraint.lngEnd
|
||||
));
|
||||
}
|
||||
|
||||
return cachedPlaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a Place object asynchronously into the database.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import fr.free.nrw.commons.contributions.Contribution;
|
|||
import fr.free.nrw.commons.location.LatLng;
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
|
|
@ -39,6 +40,17 @@ public class PlacesRepository {
|
|||
return localDataSource.fetchPlace(entityID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of places within the geographical area specified by map's opposite corners.
|
||||
*
|
||||
* @param mapBottomLeft Bottom left corner of the map.
|
||||
* @param mapTopRight Top right corner of the map.
|
||||
* @return The list of saved places within the map's view.
|
||||
*/
|
||||
public List<Place> fetchPlaces(final LatLng mapBottomLeft, final LatLng mapTopRight) {
|
||||
return localDataSource.fetchPlaces(mapBottomLeft, mapTopRight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the Nearby cache on an IO thread.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ class WikidataFeedback : BaseActivity() {
|
|||
}, { throwable: Throwable? ->
|
||||
Timber.e(throwable!!)
|
||||
})
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ public interface NearbyParentFragmentContract {
|
|||
|
||||
boolean isNetworkConnectionEstablished();
|
||||
|
||||
void updateSnackbar(boolean offlinePinsShown);
|
||||
|
||||
void listOptionMenuItemClicked();
|
||||
|
||||
void populatePlaces(LatLng currentLatLng);
|
||||
|
|
@ -91,6 +93,10 @@ public interface NearbyParentFragmentContract {
|
|||
|
||||
LatLng getMapFocus();
|
||||
|
||||
LatLng getScreenTopRight();
|
||||
|
||||
LatLng getScreenBottomLeft();
|
||||
|
||||
boolean isAdvancedQueryFragmentVisible();
|
||||
|
||||
void showHideAdvancedQueryFragment(boolean shouldShow);
|
||||
|
|
@ -122,8 +128,6 @@ public interface NearbyParentFragmentContract {
|
|||
void filterByMarkerType(List<Label> selectedLabels, int state, boolean filterForPlaceState,
|
||||
boolean filterForAllNoneType);
|
||||
|
||||
void updateMapMarkersToController(List<BaseMarker> baseMarkers);
|
||||
|
||||
void searchViewGainedFocus();
|
||||
|
||||
void setCheckboxUnknown();
|
||||
|
|
@ -131,5 +135,7 @@ public interface NearbyParentFragmentContract {
|
|||
void setAdvancedQuery(String query);
|
||||
|
||||
void toggleBookmarkedStatus(Place place);
|
||||
|
||||
void handleMapScrolled(LifecycleCoroutineScope scope, boolean isNetworkAvailable);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,8 @@ class CommonPlaceClickActions
|
|||
AlertDialog
|
||||
.Builder(activity)
|
||||
.setMessage(R.string.login_alert_message)
|
||||
.setCancelable(false)
|
||||
.setNegativeButton(R.string.cancel){_,_ -> }
|
||||
.setPositiveButton(R.string.login) { dialog, which ->
|
||||
setPositiveButton()
|
||||
}.show()
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import androidx.appcompat.app.AlertDialog.Builder;
|
|||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.lifecycle.LifecycleCoroutineScope;
|
||||
import androidx.lifecycle.LifecycleOwnerKt;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
|
|
@ -64,7 +65,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCa
|
|||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
import com.jakewharton.rxbinding3.appcompat.RxSearchView;
|
||||
import fr.free.nrw.commons.BaseMarker;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.CommonsApplication.BaseLogoutListener;
|
||||
import fr.free.nrw.commons.MapController.NearbyPlacesInfo;
|
||||
|
|
@ -92,6 +92,7 @@ import fr.free.nrw.commons.nearby.NearbyFilterSearchRecyclerViewAdapter;
|
|||
import fr.free.nrw.commons.nearby.NearbyFilterState;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.nearby.PlacesRepository;
|
||||
import fr.free.nrw.commons.nearby.Sitelinks;
|
||||
import fr.free.nrw.commons.nearby.WikidataFeedback;
|
||||
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
|
||||
import fr.free.nrw.commons.nearby.fragments.AdvanceQueryFragment.Callback;
|
||||
|
|
@ -208,6 +209,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
private boolean isNetworkErrorOccurred;
|
||||
private Snackbar snackbar;
|
||||
private View view;
|
||||
private LifecycleCoroutineScope scope;
|
||||
private NearbyParentFragmentPresenter presenter;
|
||||
private boolean isDarkTheme;
|
||||
private boolean isFABsExpanded;
|
||||
|
|
@ -231,10 +233,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
private Place nearestPlace;
|
||||
private volatile boolean stopQuery;
|
||||
|
||||
private boolean isSearchInProgress = false;
|
||||
private final Handler searchHandler = new Handler();
|
||||
private Runnable searchRunnable;
|
||||
private static final long SCROLL_DELAY = 800; // Delay for debounce of onscroll, in milliseconds.
|
||||
|
||||
private LatLng updatedLatLng;
|
||||
private boolean searchable;
|
||||
|
|
@ -341,6 +341,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
view = binding.getRoot();
|
||||
|
||||
initNetworkBroadCastReceiver();
|
||||
scope = LifecycleOwnerKt.getLifecycleScope(getViewLifecycleOwner());
|
||||
presenter = new NearbyParentFragmentPresenter(bookmarkLocationDao, placesRepository, nearbyController);
|
||||
progressDialog = new ProgressDialog(getActivity());
|
||||
progressDialog.setCancelable(false);
|
||||
|
|
@ -471,28 +472,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
binding.map.addMapListener(new MapListener() {
|
||||
@Override
|
||||
public boolean onScroll(ScrollEvent event) {
|
||||
|
||||
// Remove any pending search runnables
|
||||
searchHandler.removeCallbacks(searchRunnable);
|
||||
|
||||
// Set a runnable to call the Search after a delay
|
||||
searchRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isSearchInProgress) {
|
||||
isSearchInProgress = true; // search executing flag
|
||||
// Start Search
|
||||
try {
|
||||
presenter.searchInTheArea();
|
||||
} finally {
|
||||
isSearchInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// post runnable with configured SCROLL_DELAY
|
||||
searchHandler.postDelayed(searchRunnable, SCROLL_DELAY);
|
||||
|
||||
presenter.handleMapScrolled(scope, !isNetworkErrorOccurred);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1060,6 +1040,23 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internet unavailable snackbar to reflect whether cached pins are shown.
|
||||
*
|
||||
* @param offlinePinsShown Whether there are pins currently being shown on map.
|
||||
*/
|
||||
@Override
|
||||
public void updateSnackbar(final boolean offlinePinsShown) {
|
||||
if (!isNetworkErrorOccurred || snackbar == null) {
|
||||
return;
|
||||
}
|
||||
if (offlinePinsShown) {
|
||||
snackbar.setText(R.string.nearby_showing_pins_offline);
|
||||
} else {
|
||||
snackbar.setText(R.string.no_internet);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide or expand bottom sheet according to states of all sheets
|
||||
*/
|
||||
|
|
@ -1074,16 +1071,38 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the location of the top right corner of the map view.
|
||||
*
|
||||
* @return a `LatLng` object denoting the location of the top right corner of the map.
|
||||
*/
|
||||
@Override
|
||||
public LatLng getScreenTopRight() {
|
||||
final IGeoPoint screenTopRight = binding.map.getProjection()
|
||||
.fromPixels(binding.map.getWidth(), 0);
|
||||
return new LatLng(
|
||||
screenTopRight.getLatitude(), screenTopRight.getLongitude(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the location of the bottom left corner of the map view.
|
||||
*
|
||||
* @return a `LatLng` object denoting the location of the bottom left corner of the map.
|
||||
*/
|
||||
@Override
|
||||
public LatLng getScreenBottomLeft() {
|
||||
final IGeoPoint screenBottomLeft = binding.map.getProjection()
|
||||
.fromPixels(0, binding.map.getHeight());
|
||||
return new LatLng(
|
||||
screenBottomLeft.getLatitude(), screenBottomLeft.getLongitude(), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void populatePlaces(final LatLng currentLatLng) {
|
||||
IGeoPoint screenTopRight = binding.map.getProjection()
|
||||
.fromPixels(binding.map.getWidth(), 0);
|
||||
IGeoPoint screenBottomLeft = binding.map.getProjection()
|
||||
.fromPixels(0, binding.map.getHeight());
|
||||
LatLng screenTopRightLatLng = new LatLng(
|
||||
screenBottomLeft.getLatitude(), screenBottomLeft.getLongitude(), 0);
|
||||
LatLng screenBottomLeftLatLng = new LatLng(
|
||||
screenTopRight.getLatitude(), screenTopRight.getLongitude(), 0);
|
||||
// these two variables have historically been assigned values the opposite of what their
|
||||
// names imply, and quite some existing code depends on this fact
|
||||
LatLng screenTopRightLatLng = getScreenBottomLeft();
|
||||
LatLng screenBottomLeftLatLng = getScreenTopRight();
|
||||
|
||||
// When the nearby fragment is opened immediately upon app launch, the {screenTopRightLatLng}
|
||||
// and {screenBottomLeftLatLng} variables return {LatLng(0.0,0.0)} as output.
|
||||
|
|
@ -1136,14 +1155,10 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
populatePlaces(currentLatLng);
|
||||
return;
|
||||
}
|
||||
IGeoPoint screenTopRight = binding.map.getProjection()
|
||||
.fromPixels(binding.map.getWidth(), 0);
|
||||
IGeoPoint screenBottomLeft = binding.map.getProjection()
|
||||
.fromPixels(0, binding.map.getHeight());
|
||||
LatLng screenTopRightLatLng = new LatLng(
|
||||
screenBottomLeft.getLatitude(), screenBottomLeft.getLongitude(), 0);
|
||||
LatLng screenBottomLeftLatLng = new LatLng(
|
||||
screenTopRight.getLatitude(), screenTopRight.getLongitude(), 0);
|
||||
// these two variables have historically been assigned values the opposite of what their
|
||||
// names imply, and quite some existing code depends on this fact
|
||||
final LatLng screenTopRightLatLng = getScreenBottomLeft();
|
||||
final LatLng screenBottomLeftLatLng = getScreenTopRight();
|
||||
|
||||
if (currentLatLng.equals(lastFocusLocation) || lastFocusLocation == null
|
||||
|| recenterToUserLocation) { // Means we are checking around current location
|
||||
|
|
@ -1159,27 +1174,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
}
|
||||
|
||||
/**
|
||||
* Reloads the Nearby map
|
||||
* Clears all location markers, refreshes them, reinserts them into the map.
|
||||
*
|
||||
*/
|
||||
private void reloadMap() {
|
||||
clearAllMarkers(); // Clear the list of markers
|
||||
binding.map.getController().setZoom(ZOOM_LEVEL); // Reset the zoom level
|
||||
binding.map.getController().setCenter(lastMapFocus); // Recenter the focus
|
||||
if (locationPermissionsHelper.checkLocationPermission(getActivity())) {
|
||||
locationPermissionGranted(); // Reload map with user's location
|
||||
} else {
|
||||
startMapWithoutPermission(); // Reload map without user's location
|
||||
}
|
||||
binding.map.invalidate(); // Invalidate the map
|
||||
presenter.updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED); // Restart the map
|
||||
Timber.d("Reloaded Map Successfully");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clears the Nearby local cache and then calls for the map to be reloaded
|
||||
* Clears the Nearby local cache and then calls for pin details to be fetched afresh.
|
||||
*
|
||||
*/
|
||||
private void emptyCache() {
|
||||
|
|
@ -1188,7 +1183,22 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
placesRepository.clearCache()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.andThen(Completable.fromAction(this::reloadMap))
|
||||
.andThen(Completable.fromAction(() -> {
|
||||
// reload only the pin details, by making all loaded pins gray:
|
||||
ArrayList<MarkerPlaceGroup> newPlaceGroups = new ArrayList<>(
|
||||
NearbyController.markerLabelList.size());
|
||||
for (final MarkerPlaceGroup placeGroup : NearbyController.markerLabelList) {
|
||||
final Place place = new Place("", "", placeGroup.getPlace().getLabel(), "",
|
||||
placeGroup.getPlace().getLocation(), "",
|
||||
placeGroup.getPlace().siteLinks, "", placeGroup.getPlace().exists,
|
||||
placeGroup.getPlace().entityID);
|
||||
place.setDistance(placeGroup.getPlace().distance);
|
||||
place.setMonument(placeGroup.getPlace().isMonument());
|
||||
newPlaceGroups.add(
|
||||
new MarkerPlaceGroup(placeGroup.getIsBookmarked(), place));
|
||||
}
|
||||
presenter.loadPlacesDataAsync(newPlaceGroups, scope);
|
||||
}))
|
||||
.subscribe(
|
||||
() -> {
|
||||
Timber.d("Nearby Cache cleared successfully.");
|
||||
|
|
@ -1477,8 +1487,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
*/
|
||||
private void updateMapMarkers(final List<Place> nearbyPlaces, final LatLng curLatLng,
|
||||
final boolean shouldUpdateSelectedMarker) {
|
||||
presenter.updateMapMarkers(nearbyPlaces, curLatLng,
|
||||
LifecycleOwnerKt.getLifecycleScope(getViewLifecycleOwner()));
|
||||
presenter.updateMapMarkers(nearbyPlaces, curLatLng, scope);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1614,6 +1623,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
// prompt the user to login
|
||||
new Builder(getContext())
|
||||
.setMessage(R.string.login_alert_message)
|
||||
.setCancelable(false)
|
||||
.setNegativeButton(R.string.cancel, (dialog, which) -> {})
|
||||
.setPositiveButton(R.string.login, (dialog, which) -> {
|
||||
// logout of the app
|
||||
BaseLogoutListener logoutListener = new BaseLogoutListener(getActivity());
|
||||
|
|
@ -1859,26 +1870,31 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
place.getLongDescription()) : place.getLongDescription());
|
||||
}
|
||||
marker.setTextLabelFontSize(40);
|
||||
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_TOP);
|
||||
// anchorV is 21.707/28.0 as icon height is 28dp while the pin base is at 21.707dp from top
|
||||
marker.setAnchor(Marker.ANCHOR_CENTER, 0.77525f);
|
||||
marker.setOnMarkerClickListener((marker1, mapView) -> {
|
||||
if (clickedMarker != null) {
|
||||
clickedMarker.closeInfoWindow();
|
||||
}
|
||||
clickedMarker = marker1;
|
||||
binding.bottomSheetDetails.dataCircularProgress.setVisibility(View.VISIBLE);
|
||||
binding.bottomSheetDetails.icon.setVisibility(View.GONE);
|
||||
binding.bottomSheetDetails.wikiDataLl.setVisibility(View.GONE);
|
||||
if (Objects.equals(place.name, "")) {
|
||||
getPlaceData(place.getWikiDataEntityId(), place, marker1, isBookMarked);
|
||||
if (!isNetworkErrorOccurred) {
|
||||
binding.bottomSheetDetails.dataCircularProgress.setVisibility(View.VISIBLE);
|
||||
binding.bottomSheetDetails.icon.setVisibility(View.GONE);
|
||||
binding.bottomSheetDetails.wikiDataLl.setVisibility(View.GONE);
|
||||
if (Objects.equals(place.name, "")) {
|
||||
getPlaceData(place.getWikiDataEntityId(), place, marker1, isBookMarked);
|
||||
} else {
|
||||
marker.showInfoWindow();
|
||||
binding.bottomSheetDetails.dataCircularProgress.setVisibility(View.GONE);
|
||||
binding.bottomSheetDetails.icon.setVisibility(View.VISIBLE);
|
||||
binding.bottomSheetDetails.wikiDataLl.setVisibility(View.VISIBLE);
|
||||
passInfoToSheet(place);
|
||||
hideBottomSheet();
|
||||
}
|
||||
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
} else {
|
||||
marker.showInfoWindow();
|
||||
binding.bottomSheetDetails.dataCircularProgress.setVisibility(View.GONE);
|
||||
binding.bottomSheetDetails.icon.setVisibility(View.VISIBLE);
|
||||
binding.bottomSheetDetails.wikiDataLl.setVisibility(View.VISIBLE);
|
||||
passInfoToSheet(place);
|
||||
hideBottomSheet();
|
||||
}
|
||||
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
return true;
|
||||
});
|
||||
return marker;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import android.location.Location
|
|||
import android.view.View
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import fr.free.nrw.commons.BaseMarker
|
||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
|
|
@ -52,6 +51,10 @@ class NearbyParentFragmentPresenter
|
|||
|
||||
private var nearbyParentFragmentView: NearbyParentFragmentContract.View = DUMMY
|
||||
|
||||
private var placeSearchJob: Job? = null
|
||||
private var isSearchInProgress = false
|
||||
private var localPlaceSearchJob: Job? = null
|
||||
|
||||
private val clickedPlaces = CopyOnWriteArrayList<Place>()
|
||||
|
||||
/**
|
||||
|
|
@ -297,10 +300,27 @@ class NearbyParentFragmentPresenter
|
|||
}
|
||||
?: return
|
||||
|
||||
loadPlacesDataAyncJob?.cancel()
|
||||
lockUnlockNearby(false) // So that new location updates wont come
|
||||
nearbyParentFragmentView.setProgressBarVisibility(false)
|
||||
loadPlacesDataAsync(nearbyPlaceGroups, scope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the places' details from cache and Wikidata query, and update these details on the map
|
||||
* as and when they arrive.
|
||||
*
|
||||
* @param nearbyPlaceGroups The list of `MarkerPlaceGroup` objects to be rendered on the map.
|
||||
* Note that the supplied objects' `isBookmarked` property can be set false as the actual
|
||||
* value is retrieved from the bookmarks db eventually.
|
||||
* @param scope the lifecycle scope of `nearbyParentFragment`'s `viewLifecycleOwner`
|
||||
*
|
||||
* @see LoadPlacesAsyncOptions
|
||||
*/
|
||||
fun loadPlacesDataAsync(
|
||||
nearbyPlaceGroups: List<MarkerPlaceGroup>,
|
||||
scope: LifecycleCoroutineScope?
|
||||
) {
|
||||
loadPlacesDataAyncJob?.cancel()
|
||||
loadPlacesDataAyncJob = scope?.launch(Dispatchers.IO) {
|
||||
// clear past clicks and bookmarkChanged queues
|
||||
clickedPlaces.clear()
|
||||
|
|
@ -489,17 +509,52 @@ class NearbyParentFragmentPresenter
|
|||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
override fun updateMapMarkersToController(baseMarkers: MutableList<BaseMarker>) {
|
||||
NearbyController.markerLabelList.clear()
|
||||
for (i in baseMarkers.indices) {
|
||||
val nearbyBaseMarker = baseMarkers[i]
|
||||
NearbyController.markerLabelList.add(
|
||||
MarkerPlaceGroup(
|
||||
bookmarkLocationDao.findBookmarkLocation(nearbyBaseMarker.place),
|
||||
nearbyBaseMarker.place
|
||||
)
|
||||
)
|
||||
/**
|
||||
* Handles the map scroll user action for `NearbyParentFragment`
|
||||
*
|
||||
* @param scope The lifecycle scope of `nearbyParentFragment`'s `viewLifecycleOwner`
|
||||
* @param isNetworkAvailable Whether to load pins from the internet or from the cache.
|
||||
*/
|
||||
@Override
|
||||
override fun handleMapScrolled(scope: LifecycleCoroutineScope?, isNetworkAvailable: Boolean) {
|
||||
scope ?: return
|
||||
|
||||
placeSearchJob?.cancel()
|
||||
localPlaceSearchJob?.cancel()
|
||||
if (isNetworkAvailable) {
|
||||
placeSearchJob = scope.launch(Dispatchers.Main) {
|
||||
delay(SCROLL_DELAY)
|
||||
if (!isSearchInProgress) {
|
||||
isSearchInProgress = true; // search executing flag
|
||||
// Start Search
|
||||
try {
|
||||
searchInTheArea();
|
||||
} finally {
|
||||
isSearchInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loadPlacesDataAyncJob?.cancel()
|
||||
localPlaceSearchJob = scope.launch(Dispatchers.IO) {
|
||||
delay(LOCAL_SCROLL_DELAY)
|
||||
val mapFocus = nearbyParentFragmentView.mapFocus
|
||||
val markerPlaceGroups = placesRepository.fetchPlaces(
|
||||
nearbyParentFragmentView.screenBottomLeft,
|
||||
nearbyParentFragmentView.screenTopRight
|
||||
).sortedBy { it.getDistanceInDouble(mapFocus) }.take(NearbyController.MAX_RESULTS)
|
||||
.map {
|
||||
MarkerPlaceGroup(
|
||||
bookmarkLocationDao.findBookmarkLocation(it), it
|
||||
)
|
||||
}
|
||||
ensureActive()
|
||||
NearbyController.currentLocation = mapFocus
|
||||
schedulePlacesUpdate(markerPlaceGroups, force = true)
|
||||
withContext(Dispatchers.Main) {
|
||||
nearbyParentFragmentView.updateSnackbar(!markerPlaceGroups.isEmpty())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -575,6 +630,8 @@ class NearbyParentFragmentPresenter
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val SCROLL_DELAY = 800L; // Delay for debounce of onscroll, in milliseconds.
|
||||
private const val LOCAL_SCROLL_DELAY = 200L; // SCROLL_DELAY but for local db place search
|
||||
private val DUMMY = Proxy.newProxyInstance(
|
||||
NearbyParentFragmentContract.View::class.java.getClassLoader(),
|
||||
arrayOf<Class<*>>(NearbyParentFragmentContract.View::class.java),
|
||||
|
|
|
|||
|
|
@ -1,265 +0,0 @@
|
|||
package fr.free.nrw.commons.profile;
|
||||
|
||||
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.annotation.NonNull;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
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.contributions.ContributionsFragment;
|
||||
import fr.free.nrw.commons.databinding.ActivityProfileBinding;
|
||||
import fr.free.nrw.commons.profile.achievements.AchievementsFragment;
|
||||
import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* This activity will set two tabs, achievements and
|
||||
* each tab will have their own fragments
|
||||
*/
|
||||
public class ProfileActivity extends BaseActivity {
|
||||
|
||||
private FragmentManager supportFragmentManager;
|
||||
|
||||
public ActivityProfileBinding binding;
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
private ViewPagerAdapter viewPagerAdapter;
|
||||
private AchievementsFragment achievementsFragment;
|
||||
private LeaderboardFragment leaderboardFragment;
|
||||
|
||||
public static final String KEY_USERNAME ="username";
|
||||
public static final String KEY_SHOULD_SHOW_CONTRIBUTIONS ="shouldShowContributions";
|
||||
|
||||
String userName;
|
||||
private boolean shouldShowContributions;
|
||||
|
||||
ContributionsFragment contributionsFragment;
|
||||
|
||||
public void setScroll(boolean canScroll){
|
||||
binding.viewPager.setCanScroll(canScroll);
|
||||
}
|
||||
@Override
|
||||
protected void onRestoreInstanceState(final Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
userName = savedInstanceState.getString(KEY_USERNAME);
|
||||
shouldShowContributions = savedInstanceState.getBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
binding = ActivityProfileBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
setSupportActionBar(binding.toolbarBinding.toolbar);
|
||||
|
||||
|
||||
binding.toolbarBinding.toolbar.setNavigationOnClickListener(view -> {
|
||||
onSupportNavigateUp();
|
||||
});
|
||||
|
||||
userName = getIntent().getStringExtra(KEY_USERNAME);
|
||||
setTitle(userName);
|
||||
shouldShowContributions = getIntent().getBooleanExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, false);
|
||||
|
||||
supportFragmentManager = getSupportFragmentManager();
|
||||
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
|
||||
binding.viewPager.setAdapter(viewPagerAdapter);
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||
setTabs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate up event
|
||||
* @return boolean
|
||||
*/
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a way to change current activity to AchievementActivity
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
public static void startYourself(final Context context, final String userName,
|
||||
final boolean shouldShowContributions) {
|
||||
Intent intent = new Intent(context, ProfileActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
intent.putExtra(KEY_USERNAME, userName);
|
||||
intent.putExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tabs for the fragments
|
||||
*/
|
||||
private void setTabs() {
|
||||
List<Fragment> fragmentList = new ArrayList<>();
|
||||
List<String> titleList = new ArrayList<>();
|
||||
achievementsFragment = new AchievementsFragment();
|
||||
Bundle achievementsBundle = new Bundle();
|
||||
achievementsBundle.putString(KEY_USERNAME, userName);
|
||||
achievementsFragment.setArguments(achievementsBundle);
|
||||
fragmentList.add(achievementsFragment);
|
||||
|
||||
titleList.add(getResources().getString(R.string.achievements_tab_title).toUpperCase());
|
||||
leaderboardFragment = new LeaderboardFragment();
|
||||
Bundle leaderBoardBundle = new Bundle();
|
||||
leaderBoardBundle.putString(KEY_USERNAME, userName);
|
||||
leaderboardFragment.setArguments(leaderBoardBundle);
|
||||
|
||||
fragmentList.add(leaderboardFragment);
|
||||
titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase(Locale.ROOT));
|
||||
|
||||
contributionsFragment = new ContributionsFragment();
|
||||
Bundle contributionsListBundle = new Bundle();
|
||||
contributionsListBundle.putString(KEY_USERNAME, userName);
|
||||
contributionsFragment.setArguments(contributionsListBundle);
|
||||
fragmentList.add(contributionsFragment);
|
||||
titleList.add(getString(R.string.contributions_fragment).toUpperCase(Locale.ROOT));
|
||||
|
||||
viewPagerAdapter.setTabData(fragmentList, titleList);
|
||||
viewPagerAdapter.notifyDataSetChanged();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
getCompositeDisposable().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 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);
|
||||
DialogUtil.showAlertDialog(this,
|
||||
null,
|
||||
null,
|
||||
getString(R.string.about_translate_proceed),
|
||||
getString(R.string.cancel),
|
||||
() -> shareScreen(screenshot),
|
||||
() -> {},
|
||||
view
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
outState.putString(KEY_USERNAME, userName);
|
||||
outState.putBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// Checking if MediaDetailPagerFragment is visible, If visible then show ContributionListFragment else close the ProfileActivity
|
||||
if(contributionsFragment != null && contributionsFragment.getMediaDetailPagerFragment() != null && contributionsFragment.getMediaDetailPagerFragment().isVisible()) {
|
||||
contributionsFragment.backButtonClicked();
|
||||
binding.tabLayout.setVisibility(View.VISIBLE);
|
||||
}else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To set the visibility of tab layout
|
||||
* @param isVisible boolean
|
||||
*/
|
||||
public void setTabLayoutVisibility(boolean isVisible) {
|
||||
binding.tabLayout.setVisibility(isVisible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
229
app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt
Normal file
229
app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
package fr.free.nrw.commons.profile
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
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.contributions.ContributionsFragment
|
||||
import fr.free.nrw.commons.databinding.ActivityProfileBinding
|
||||
import fr.free.nrw.commons.profile.achievements.AchievementsFragment
|
||||
import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
import fr.free.nrw.commons.utils.DialogUtil
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This activity will set two tabs, achievements and
|
||||
* each tab will have their own fragments
|
||||
*/
|
||||
class ProfileActivity : BaseActivity() {
|
||||
|
||||
lateinit var binding: ActivityProfileBinding
|
||||
|
||||
@Inject
|
||||
lateinit var sessionManager: SessionManager
|
||||
|
||||
private lateinit var viewPagerAdapter: ViewPagerAdapter
|
||||
private lateinit var achievementsFragment: AchievementsFragment
|
||||
private lateinit var leaderboardFragment: LeaderboardFragment
|
||||
private lateinit var userName: String
|
||||
private var shouldShowContributions: Boolean = false
|
||||
private var contributionsFragment: ContributionsFragment? = null
|
||||
|
||||
fun setScroll(canScroll: Boolean) {
|
||||
binding.viewPager.setCanScroll(canScroll)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
savedInstanceState.let {
|
||||
userName = it.getString(KEY_USERNAME, "")
|
||||
shouldShowContributions = it.getBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityProfileBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.toolbarBinding.toolbar)
|
||||
|
||||
binding.toolbarBinding.toolbar.setNavigationOnClickListener {
|
||||
onSupportNavigateUp()
|
||||
}
|
||||
|
||||
userName = intent.getStringExtra(KEY_USERNAME) ?: ""
|
||||
title = userName
|
||||
shouldShowContributions = intent.getBooleanExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, false)
|
||||
|
||||
viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
|
||||
binding.viewPager.adapter = viewPagerAdapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
setTabs()
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setTabs() {
|
||||
val fragmentList = mutableListOf<Fragment>()
|
||||
val titleList = mutableListOf<String>()
|
||||
|
||||
// Add Achievements tab
|
||||
achievementsFragment = AchievementsFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(KEY_USERNAME, userName)
|
||||
}
|
||||
}
|
||||
fragmentList.add(achievementsFragment)
|
||||
titleList.add(resources.getString(R.string.achievements_tab_title).uppercase())
|
||||
|
||||
// Add Leaderboard tab
|
||||
leaderboardFragment = LeaderboardFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(KEY_USERNAME, userName)
|
||||
}
|
||||
}
|
||||
fragmentList.add(leaderboardFragment)
|
||||
titleList.add(resources.getString(R.string.leaderboard_tab_title).uppercase(Locale.ROOT))
|
||||
|
||||
// Add Contributions tab
|
||||
contributionsFragment = ContributionsFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(KEY_USERNAME, userName)
|
||||
}
|
||||
}
|
||||
contributionsFragment?.let {
|
||||
fragmentList.add(it)
|
||||
titleList.add(getString(R.string.contributions_fragment).uppercase(Locale.ROOT))
|
||||
}
|
||||
|
||||
viewPagerAdapter.setTabData(fragmentList, titleList)
|
||||
viewPagerAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
compositeDisposable.clear()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_about, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.share_app_icon -> {
|
||||
val rootView = window.decorView.findViewById<View>(android.R.id.content)
|
||||
val screenShot = Utils.getScreenShot(rootView)
|
||||
if (screenShot == null) {
|
||||
Log.e("ERROR", "ScreenShot is null")
|
||||
return false
|
||||
}
|
||||
showAlert(screenShot)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
fun showAlert(screenshot: Bitmap) {
|
||||
val view = layoutInflater.inflate(R.layout.image_alert_layout, null)
|
||||
val screenShotImage = view.findViewById<ImageView>(R.id.alert_image)
|
||||
val shareMessage = view.findViewById<TextView>(R.id.alert_text)
|
||||
|
||||
screenShotImage.setImageBitmap(screenshot)
|
||||
shareMessage.setText(R.string.achievements_share_message)
|
||||
|
||||
DialogUtil.showAlertDialog(
|
||||
this,
|
||||
null,
|
||||
null,
|
||||
getString(R.string.about_translate_proceed),
|
||||
getString(R.string.cancel),
|
||||
{ shareScreen(screenshot) },
|
||||
{},
|
||||
view
|
||||
)
|
||||
}
|
||||
|
||||
private fun shareScreen(bitmap: Bitmap) {
|
||||
try {
|
||||
val file = File(externalCacheDir, "screen.png")
|
||||
FileOutputStream(file).use { out ->
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
out.flush()
|
||||
}
|
||||
file.setReadable(true, false)
|
||||
|
||||
val fileUri = FileProvider.getUriForFile(
|
||||
applicationContext,
|
||||
"$packageName.provider",
|
||||
file
|
||||
)
|
||||
|
||||
grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
putExtra(Intent.EXTRA_STREAM, fileUri)
|
||||
type = "image/png"
|
||||
}
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_image_via)))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putString(KEY_USERNAME, userName)
|
||||
outState.putBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (contributionsFragment?.mediaDetailPagerFragment?.isVisible == true) {
|
||||
contributionsFragment?.backButtonClicked()
|
||||
binding.tabLayout.visibility = View.VISIBLE
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTabLayoutVisibility(isVisible: Boolean) {
|
||||
binding.tabLayout.visibility = if (isVisible) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_USERNAME = "username"
|
||||
const val KEY_SHOULD_SHOW_CONTRIBUTIONS = "shouldShowContributions"
|
||||
|
||||
@JvmStatic
|
||||
fun startYourself(context: Context, userName: String, shouldShowContributions: Boolean) {
|
||||
val intent = Intent(context, ProfileActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
putExtra(KEY_USERNAME, userName)
|
||||
putExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ class LeaderboardListAdapter : PagedListAdapter<LeaderboardList, ListViewHolder>
|
|||
if (view.context is ProfileActivity) {
|
||||
((view.context) as Activity).finish()
|
||||
}
|
||||
ProfileActivity.startYourself(view.context, item.username, true)
|
||||
ProfileActivity.startYourself(view.context, item.username?:"", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ class QuizActivity : AppCompatActivity() {
|
|||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setCancelable(false)
|
||||
.setNegativeButton(R.string.cancel){_,_ -> }
|
||||
.setPositiveButton(R.string.continue_message) { dialog, _ ->
|
||||
questionIndex++
|
||||
if (questionIndex == quiz.size) {
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ class QuizChecker @Inject constructor(
|
|||
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
private val UPLOAD_COUNT_THRESHOLD = 5
|
||||
private val REVERT_PERCENTAGE_FOR_MESSAGE = "50%"
|
||||
private val REVERT_SHARED_PREFERENCE = "revertCount"
|
||||
private val UPLOAD_SHARED_PREFERENCE = "uploadCount"
|
||||
private val uploadCountThreshold = 5
|
||||
private val revertPercentageForMessage = "50%"
|
||||
private val revertSharedPreference = "revertCount"
|
||||
private val uploadSharedPreference = "uploadCount"
|
||||
|
||||
/**
|
||||
* Initializes quiz check by calculating revert parameters and showing quiz if necessary
|
||||
|
|
@ -80,12 +80,12 @@ class QuizChecker @Inject constructor(
|
|||
*/
|
||||
private fun setTotalUploadCount(uploadCount: Int) {
|
||||
totalUploadCount = uploadCount - revertKvStore.getInt(
|
||||
UPLOAD_SHARED_PREFERENCE,
|
||||
uploadSharedPreference,
|
||||
0
|
||||
)
|
||||
if (totalUploadCount < 0) {
|
||||
totalUploadCount = 0
|
||||
revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0)
|
||||
revertKvStore.putInt(uploadSharedPreference, 0)
|
||||
}
|
||||
isUploadCountFetched = true
|
||||
}
|
||||
|
|
@ -112,10 +112,10 @@ class QuizChecker @Inject constructor(
|
|||
* @param revertCountFetched Count of deleted uploads
|
||||
*/
|
||||
private fun setRevertParameter(revertCountFetched: Int) {
|
||||
revertCount = revertCountFetched - revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0)
|
||||
revertCount = revertCountFetched - revertKvStore.getInt(revertSharedPreference, 0)
|
||||
if (revertCount < 0) {
|
||||
revertCount = 0
|
||||
revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0)
|
||||
revertKvStore.putInt(revertSharedPreference, 0)
|
||||
}
|
||||
isRevertCountFetched = true
|
||||
}
|
||||
|
|
@ -128,13 +128,13 @@ class QuizChecker @Inject constructor(
|
|||
setRevertCount()
|
||||
|
||||
if (revertCount < 0 || totalUploadCount < 0) {
|
||||
revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0)
|
||||
revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0)
|
||||
revertKvStore.putInt(revertSharedPreference, 0)
|
||||
revertKvStore.putInt(uploadSharedPreference, 0)
|
||||
return
|
||||
}
|
||||
|
||||
if (isRevertCountFetched && isUploadCountFetched &&
|
||||
totalUploadCount >= UPLOAD_COUNT_THRESHOLD &&
|
||||
totalUploadCount >= uploadCountThreshold &&
|
||||
(revertCount * 100) / totalUploadCount >= 50
|
||||
) {
|
||||
callQuiz(activity)
|
||||
|
|
@ -149,7 +149,7 @@ class QuizChecker @Inject constructor(
|
|||
DialogUtil.showAlertDialog(
|
||||
activity,
|
||||
activity.getString(R.string.quiz),
|
||||
activity.getString(R.string.quiz_alert_message, REVERT_PERCENTAGE_FOR_MESSAGE),
|
||||
activity.getString(R.string.quiz_alert_message, revertPercentageForMessage),
|
||||
activity.getString(R.string.about_translate_proceed),
|
||||
activity.getString(android.R.string.cancel),
|
||||
{ startQuizActivity(activity) },
|
||||
|
|
@ -161,11 +161,11 @@ class QuizChecker @Inject constructor(
|
|||
* Starts the quiz activity and updates preferences for revert and upload counts
|
||||
*/
|
||||
private fun startQuizActivity(activity: Activity) {
|
||||
val newRevertSharedPrefs = revertCount + revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0)
|
||||
revertKvStore.putInt(REVERT_SHARED_PREFERENCE, newRevertSharedPrefs)
|
||||
val newRevertSharedPrefs = revertCount + revertKvStore.getInt(revertSharedPreference, 0)
|
||||
revertKvStore.putInt(revertSharedPreference, newRevertSharedPrefs)
|
||||
|
||||
val newUploadCount = totalUploadCount + revertKvStore.getInt(UPLOAD_SHARED_PREFERENCE, 0)
|
||||
revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, newUploadCount)
|
||||
val newUploadCount = totalUploadCount + revertKvStore.getInt(uploadSharedPreference, 0)
|
||||
revertKvStore.putInt(uploadSharedPreference, newUploadCount)
|
||||
|
||||
val intent = Intent(activity, WelcomeActivity::class.java).apply {
|
||||
putExtra("isQuiz", true)
|
||||
|
|
|
|||
|
|
@ -14,11 +14,15 @@ class QuizController {
|
|||
|
||||
private val quiz: ArrayList<QuizQuestion> = ArrayList()
|
||||
|
||||
private val URL_FOR_SELFIE = "https://i.imgur.com/0fMYcpM.jpg"
|
||||
private val URL_FOR_TAJ_MAHAL = "https://upload.wikimedia.org/wikipedia/commons/1/15/Taj_Mahal-03.jpg"
|
||||
private val URL_FOR_BLURRY_IMAGE = "https://i.imgur.com/Kepb5jR.jpg"
|
||||
private val URL_FOR_SCREENSHOT = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Social_media_app_mockup_screenshot.svg/500px-Social_media_app_mockup_screenshot.svg.png"
|
||||
private val URL_FOR_EVENT = "https://upload.wikimedia.org/wikipedia/commons/5/51/HouseBuildingInNorthernVietnam.jpg"
|
||||
companion object{
|
||||
|
||||
const val URL_FOR_SELFIE = "https://i.imgur.com/0fMYcpM.jpg"
|
||||
const val URL_FOR_TAJ_MAHAL = "https://upload.wikimedia.org/wikipedia/commons/1/15/Taj_Mahal-03.jpg"
|
||||
const val URL_FOR_BLURRY_IMAGE = "https://i.imgur.com/Kepb5jR.jpg"
|
||||
const val URL_FOR_SCREENSHOT = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Social_media_app_mockup_screenshot.svg/500px-Social_media_app_mockup_screenshot.svg.png"
|
||||
const val URL_FOR_EVENT = "https://upload.wikimedia.org/wikipedia/commons/5/51/HouseBuildingInNorthernVietnam.jpg"
|
||||
}
|
||||
|
||||
|
||||
fun initialize(context: Context) {
|
||||
val q1 = QuizQuestion(
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ import fr.free.nrw.commons.contributions.MainActivity
|
|||
class QuizResultActivity : AppCompatActivity() {
|
||||
|
||||
private var binding: ActivityQuizResultBinding? = null
|
||||
private val NUMBER_OF_QUESTIONS = 5
|
||||
private val MULTIPLIER_TO_GET_PERCENTAGE = 20
|
||||
private val numberOfQuestions = 5
|
||||
private val multiplierToGetPercentage = 20
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
@ -67,9 +67,9 @@ class QuizResultActivity : AppCompatActivity() {
|
|||
*/
|
||||
@SuppressLint("StringFormatInvalid", "SetTextI18n")
|
||||
fun setScore(score: Int) {
|
||||
val scorePercent = score * MULTIPLIER_TO_GET_PERCENTAGE
|
||||
val scorePercent = score * multiplierToGetPercentage
|
||||
binding?.resultProgressBar?.progress = scorePercent
|
||||
binding?.tvResultProgress?.text = "$score / $NUMBER_OF_QUESTIONS"
|
||||
binding?.tvResultProgress?.text = "$score / $numberOfQuestions"
|
||||
val message = resources.getString(R.string.congratulatory_message_quiz, "$scorePercent%")
|
||||
binding?.congratulatoryMessage?.text = message
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import java.util.HashMap
|
|||
class RecentLanguagesAdapter constructor(
|
||||
context: Context,
|
||||
var recentLanguages: List<Language>,
|
||||
private val selectedLanguages: HashMap<*, String>,
|
||||
private val selectedLanguages: MutableMap<Int, String>,
|
||||
) : ArrayAdapter<String?>(context, R.layout.row_item_languages_spinner) {
|
||||
/**
|
||||
* Selected language code in UploadMediaDetailAdapter
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class UploadRepository @Inject constructor(
|
|||
*
|
||||
* @return
|
||||
*/
|
||||
fun buildContributions(): Observable<Contribution>? {
|
||||
fun buildContributions(): Observable<Contribution> {
|
||||
return uploadModel.buildContributions()
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ class UploadRepository @Inject constructor(
|
|||
* @return
|
||||
*/
|
||||
fun getUploads(): List<UploadItem> {
|
||||
return uploadModel.getUploads()
|
||||
return uploadModel.uploads
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -177,7 +177,7 @@ class UploadRepository @Inject constructor(
|
|||
place: Place?,
|
||||
similarImageInterface: SimilarImageInterface?,
|
||||
inAppPictureLocation: LatLng?
|
||||
): Observable<UploadItem>? {
|
||||
): Observable<UploadItem> {
|
||||
return uploadModel.preProcessImage(
|
||||
uploadableFile,
|
||||
place,
|
||||
|
|
@ -193,7 +193,7 @@ class UploadRepository @Inject constructor(
|
|||
* @param location Location of the image
|
||||
* @return Quality of UploadItem
|
||||
*/
|
||||
fun getImageQuality(uploadItem: UploadItem, location: LatLng?): Single<Int>? {
|
||||
fun getImageQuality(uploadItem: UploadItem, location: LatLng?): Single<Int> {
|
||||
return uploadModel.getImageQuality(uploadItem, location)
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +213,7 @@ class UploadRepository @Inject constructor(
|
|||
* @param uploadItem UploadItem whose caption is to be checked
|
||||
* @return Quality of caption of the UploadItem
|
||||
*/
|
||||
fun getCaptionQuality(uploadItem: UploadItem): Single<Int>? {
|
||||
fun getCaptionQuality(uploadItem: UploadItem): Single<Int> {
|
||||
return uploadModel.getCaptionQuality(uploadItem)
|
||||
}
|
||||
|
||||
|
|
@ -275,7 +275,7 @@ class UploadRepository @Inject constructor(
|
|||
* @param selectedExistingDepictions existing depicts
|
||||
*/
|
||||
fun setSelectedExistingDepictions(selectedExistingDepictions: List<String>) {
|
||||
uploadModel.selectedExistingDepictions = selectedExistingDepictions
|
||||
uploadModel.selectedExistingDepictions = selectedExistingDepictions.toMutableList()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -45,12 +45,12 @@ class ReviewActivity : BaseActivity() {
|
|||
private var hasNonHiddenCategories = false
|
||||
var media: Media? = null
|
||||
|
||||
private val SAVED_MEDIA = "saved_media"
|
||||
private val savedMedia = "saved_media"
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
media?.let {
|
||||
outState.putParcelable(SAVED_MEDIA, it)
|
||||
outState.putParcelable(savedMedia, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,8 +90,8 @@ class ReviewActivity : BaseActivity() {
|
|||
PorterDuff.Mode.SRC_IN
|
||||
)
|
||||
|
||||
if (savedInstanceState?.getParcelable<Media>(SAVED_MEDIA) != null) {
|
||||
updateImage(savedInstanceState.getParcelable(SAVED_MEDIA)!!)
|
||||
if (savedInstanceState?.getParcelable<Media>(savedMedia) != null) {
|
||||
updateImage(savedInstanceState.getParcelable(savedMedia)!!)
|
||||
setUpMediaDetailOnOrientation()
|
||||
} else {
|
||||
runRandomizer()
|
||||
|
|
@ -188,7 +188,7 @@ class ReviewActivity : BaseActivity() {
|
|||
return
|
||||
}
|
||||
|
||||
binding.reviewImageView.setImageURI(media.imageUrl)
|
||||
binding.reviewImageView.setImageURI(media.thumbUrl)
|
||||
|
||||
reviewController.onImageRefreshed(media) // filename is updated
|
||||
compositeDisposable.add(
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class ReviewImageFragment : CommonsDaggerSupportFragment() {
|
|||
lateinit var sessionManager: SessionManager
|
||||
|
||||
// Constant variable used to store user's key name for onSaveInstanceState method
|
||||
private val SAVED_USER = "saved_user"
|
||||
private val savedUser = "saved_user"
|
||||
|
||||
// Variable that stores the value of user
|
||||
private var user: String? = null
|
||||
|
|
@ -129,7 +129,7 @@ class ReviewImageFragment : CommonsDaggerSupportFragment() {
|
|||
question = getString(R.string.review_thanks)
|
||||
|
||||
user = reviewActivity.reviewController.firstRevision?.user()
|
||||
?: savedInstanceState?.getString(SAVED_USER)
|
||||
?: savedInstanceState?.getString(savedUser)
|
||||
|
||||
//if the user is null because of whatsoever reason, review will not be sent anyways
|
||||
if (!user.isNullOrEmpty()) {
|
||||
|
|
@ -172,7 +172,7 @@ class ReviewImageFragment : CommonsDaggerSupportFragment() {
|
|||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
//Save user name when configuration changes happen
|
||||
outState.putString(SAVED_USER, user)
|
||||
outState.putString(savedUser, user)
|
||||
}
|
||||
|
||||
private val reviewCallback: ReviewController.ReviewCallback
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
package fr.free.nrw.commons.settings
|
||||
|
||||
object Prefs {
|
||||
const val GLOBAL_PREFS = "fr.free.nrw.commons.preferences"
|
||||
|
||||
const val TRACKING_ENABLED = "eventLogging"
|
||||
const val DEFAULT_LICENSE = "defaultLicense"
|
||||
const val UPLOADS_SHOWING = "uploadsShowing"
|
||||
const val MANAGED_EXIF_TAGS = "managed_exif_tags"
|
||||
const val VANISHED_ACCOUNT = "vanishAccount"
|
||||
const val DESCRIPTION_LANGUAGE = "languageDescription"
|
||||
const val SECONDARY_LANGUAGES = "secondaryLanguages"
|
||||
const val APP_UI_LANGUAGE = "appUiLanguage"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import fr.free.nrw.commons.theme.BaseActivity
|
|||
class SettingsActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivitySettingsBinding
|
||||
// private var settingsDelegate: AppCompatDelegate? = null
|
||||
|
||||
/**
|
||||
* to be called when the activity starts
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package fr.free.nrw.commons.settings
|
||||
|
||||
import android.Manifest.permission
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
|
|
@ -11,9 +10,9 @@ import android.net.Uri
|
|||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
|
|
@ -21,6 +20,7 @@ import android.widget.Toast
|
|||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.Preference
|
||||
|
|
@ -34,8 +34,10 @@ import com.karumi.dexter.MultiplePermissionsReport
|
|||
import com.karumi.dexter.PermissionToken
|
||||
import com.karumi.dexter.listener.PermissionRequest
|
||||
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
|
||||
import fr.free.nrw.commons.BuildConfig.MOBILE_META_URL
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.Utils
|
||||
import fr.free.nrw.commons.activity.SingleWebViewActivity
|
||||
import fr.free.nrw.commons.campaigns.CampaignView
|
||||
import fr.free.nrw.commons.contributions.ContributionController
|
||||
import fr.free.nrw.commons.contributions.MainActivity
|
||||
|
|
@ -50,6 +52,7 @@ import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
|
|||
import fr.free.nrw.commons.upload.LanguagesAdapter
|
||||
import fr.free.nrw.commons.utils.DialogUtil
|
||||
import fr.free.nrw.commons.utils.PermissionUtils
|
||||
import fr.free.nrw.commons.utils.StringUtil
|
||||
import fr.free.nrw.commons.utils.ViewUtil
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
|
@ -73,6 +76,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
@Inject
|
||||
lateinit var locationManager: LocationServiceManager
|
||||
|
||||
private var vanishAccountPreference: Preference? = null
|
||||
private var themeListPreference: ListPreference? = null
|
||||
private var descriptionLanguageListPreference: Preference? = null
|
||||
private var descriptionSecondaryLanguagesListPreference: Preference? = null
|
||||
|
|
@ -82,8 +86,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
private var recentLanguagesTextView: TextView? = null
|
||||
private var separator: View? = null
|
||||
private var languageHistoryListView: ListView? = null
|
||||
|
||||
private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>
|
||||
private val GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content"
|
||||
|
||||
private val cameraPickLauncherForResult: ActivityResultLauncher<Intent> =
|
||||
registerForActivityResult(StartActivityForResult()) { result ->
|
||||
|
|
@ -117,6 +121,26 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
themeListPreference = findPreference(Prefs.KEY_THEME_VALUE)
|
||||
prepareTheme()
|
||||
|
||||
vanishAccountPreference = findPreference(Prefs.VANISHED_ACCOUNT)
|
||||
vanishAccountPreference?.setOnPreferenceClickListener {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.account_vanish_request_confirm_title)
|
||||
.setMessage(StringUtil.fromHtml(getString(R.string.account_vanish_request_confirm)))
|
||||
.setNegativeButton(R.string.cancel){ dialog,_ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setPositiveButton(R.string.vanish_account) { dialog, _ ->
|
||||
SingleWebViewActivity.showWebView(
|
||||
context = requireActivity(),
|
||||
url = VANISH_ACCOUNT_URL,
|
||||
successUrl = VANISH_ACCOUNT_SUCCESS_URL
|
||||
)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
true
|
||||
}
|
||||
|
||||
val multiSelectListPref: MultiSelectListPreference? = findPreference(
|
||||
Prefs.MANAGED_EXIF_TAGS
|
||||
)
|
||||
|
|
@ -132,7 +156,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
inAppCameraLocationPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
val isInAppCameraLocationTurnedOn = newValue as Boolean
|
||||
if (isInAppCameraLocationTurnedOn) {
|
||||
createDialogsAndHandleLocationPermissions(requireActivity())
|
||||
createDialogsAndHandleLocationPermissions()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
|
@ -255,6 +279,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
findPreference<Preference>("managed_exif_tags")?.isEnabled = false
|
||||
findPreference<Preference>("openDocumentPhotoPickerPref")?.isEnabled = false
|
||||
findPreference<Preference>("inAppCameraLocationPref")?.isEnabled = false
|
||||
findPreference<Preference>("vanishAccount")?.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +288,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
*
|
||||
* @param activity
|
||||
*/
|
||||
private fun createDialogsAndHandleLocationPermissions(activity: Activity) {
|
||||
private fun createDialogsAndHandleLocationPermissions() {
|
||||
inAppCameraLocationPermissionLauncher.launch(arrayOf(permission.ACCESS_FINE_LOCATION))
|
||||
}
|
||||
|
||||
|
|
@ -291,7 +316,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
return object : PreferenceGroupAdapter(preferenceScreen) {
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) {
|
||||
super.onBindViewHolder(holder, position)
|
||||
val preference = getItem(position)
|
||||
val iconFrame: View? = holder.itemView.findViewById(R.id.icon_frame)
|
||||
iconFrame?.visibility = View.GONE
|
||||
}
|
||||
|
|
@ -426,24 +450,16 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
|
||||
val dialog = Dialog(requireActivity())
|
||||
dialog.setContentView(R.layout.dialog_select_language)
|
||||
dialog.setCancelable(true)// Allow dialog to close with the back button
|
||||
dialog.setCancelable(false)
|
||||
dialog.window?.setLayout(
|
||||
(resources.displayMetrics.widthPixels * 0.90).toInt(),
|
||||
(resources.displayMetrics.heightPixels * 0.90).toInt()
|
||||
)
|
||||
// Handle back button explicitly to dismiss the dialog
|
||||
dialog.setOnKeyListener { _, keyCode, event ->
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
|
||||
dialog.dismiss() // Close the dialog when the back button is pressed
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
|
||||
val editText: EditText = dialog.findViewById(R.id.search_language)
|
||||
val listView: ListView = dialog.findViewById(R.id.language_list)
|
||||
val cancelButton = dialog.findViewById<Button>(R.id.cancel_button)
|
||||
languageHistoryListView = dialog.findViewById(R.id.language_history_list)
|
||||
recentLanguagesTextView = dialog.findViewById(R.id.recent_searches)
|
||||
separator = dialog.findViewById(R.id.separator)
|
||||
|
|
@ -452,6 +468,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
|
||||
listView.adapter = languagesAdapter
|
||||
|
||||
cancelButton.setOnClickListener { dialog.dismiss() }
|
||||
|
||||
editText.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, start: Int, count: Int, after: Int) {
|
||||
hideRecentLanguagesSection()
|
||||
|
|
@ -585,7 +603,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
editor.apply()
|
||||
}
|
||||
|
||||
@Suppress("LongLine")
|
||||
companion object {
|
||||
const val GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content"
|
||||
private const val VANISH_ACCOUNT_URL = "https://meta.m.wikimedia.org/wiki/Special:Contact/accountvanishapps"
|
||||
private const val VANISH_ACCOUNT_SUCCESS_URL = "https://meta.m.wikimedia.org/wiki/Special:GlobalVanishRequest/vanished"
|
||||
/**
|
||||
* Create Locale based on different types of language codes
|
||||
* @param languageCode
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
package fr.free.nrw.commons.ui.widget
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
||||
/**
|
||||
* A formatted dialog fragment
|
||||
* This class is used by NearbyInfoDialog
|
||||
*/
|
||||
abstract class OverlayDialog : DialogFragment() {
|
||||
|
||||
/**
|
||||
* Creates a DialogFragment with the correct style and theme
|
||||
* @param savedInstanceState bundle re-constructed from a previous saved state
|
||||
*/
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light)
|
||||
}
|
||||
|
||||
/**
|
||||
* When the view is created, sets the dialog layout to full screen
|
||||
*
|
||||
* @param view the view being used
|
||||
* @param savedInstanceState bundle re-constructed from a previous saved state
|
||||
*/
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
setDialogLayoutToFullScreen()
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dialog layout to fullscreen
|
||||
*/
|
||||
private fun setDialogLayoutToFullScreen() {
|
||||
val window = dialog?.window ?: return
|
||||
val wlp = window.attributes
|
||||
window.requestFeature(Window.FEATURE_NO_TITLE)
|
||||
wlp.gravity = Gravity.BOTTOM
|
||||
wlp.width = WindowManager.LayoutParams.MATCH_PARENT
|
||||
wlp.height = WindowManager.LayoutParams.MATCH_PARENT
|
||||
window.attributes = wlp
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds custom dialog container
|
||||
*
|
||||
* @param savedInstanceState the previously saved state
|
||||
* @return the dialog
|
||||
*/
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
package fr.free.nrw.commons.upload
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
|
|
@ -13,6 +16,7 @@ import androidx.paging.PagedListAdapter
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.facebook.imagepipeline.request.ImageRequest
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.contributions.Contribution
|
||||
import java.io.File
|
||||
|
|
@ -51,6 +55,24 @@ class FailedUploadsAdapter(
|
|||
position: Int,
|
||||
) {
|
||||
val item: Contribution? = getItem(position)
|
||||
val itemView = holder.itemView
|
||||
val clipboardManager =
|
||||
itemView.context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
val clip = ClipData.newPlainText(
|
||||
itemView.context.getString(R.string.caption),
|
||||
item?.media?.displayTitle
|
||||
)
|
||||
clipboardManager.setPrimaryClip(clip)
|
||||
Snackbar.make(
|
||||
itemView,
|
||||
itemView.context.getString(R.string.caption_copied_to_clipboard),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
true
|
||||
}
|
||||
|
||||
if (item != null) {
|
||||
holder.titleTextView.setText(item.media.displayTitle)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class FileProcessor
|
|||
* Processes filePath coordinates, either from EXIF data or user location
|
||||
*/
|
||||
fun processFileCoordinates(
|
||||
similarImageInterface: SimilarImageInterface,
|
||||
similarImageInterface: SimilarImageInterface?,
|
||||
filePath: String?,
|
||||
inAppPictureLocation: LatLng?,
|
||||
): ImageCoordinates {
|
||||
|
|
@ -146,7 +146,7 @@ class FileProcessor
|
|||
*/
|
||||
private fun findOtherImages(
|
||||
fileBeingProcessed: File,
|
||||
similarImageInterface: SimilarImageInterface,
|
||||
similarImageInterface: SimilarImageInterface?,
|
||||
) {
|
||||
val oneHundredAndTwentySeconds = 120 * 1000L
|
||||
// Time when the original image was created
|
||||
|
|
@ -161,7 +161,7 @@ class FileProcessor
|
|||
.map { Pair(it, readImageCoordinates(it)) }
|
||||
.firstOrNull { it.second?.decimalCoords != null }
|
||||
?.let { fileCoordinatesPair ->
|
||||
similarImageInterface.showSimilarImageFragment(
|
||||
similarImageInterface?.showSimilarImageFragment(
|
||||
fileBeingProcessed.path,
|
||||
fileCoordinatesPair.first.absolutePath,
|
||||
fileCoordinatesPair.second,
|
||||
|
|
|
|||
|
|
@ -49,9 +49,10 @@ class FileUtilsWrapper @Inject constructor(private val context: Context) {
|
|||
while ((bis.read(buffer).also { size = it }) > 0) {
|
||||
buffers.add(
|
||||
writeToFile(
|
||||
buffer.copyOf(size),
|
||||
buffer,
|
||||
file.name ?: "",
|
||||
getFileExt(file.name)
|
||||
getFileExt(file.name),
|
||||
size
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -67,7 +68,7 @@ class FileUtilsWrapper @Inject constructor(private val context: Context) {
|
|||
* Create a temp file containing the passed byte data.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
private fun writeToFile(data: ByteArray, fileName: String, fileExtension: String): File {
|
||||
private fun writeToFile(data: ByteArray, fileName: String, fileExtension: String, size: Int): File {
|
||||
val file = File.createTempFile(fileName, fileExtension, context.cacheDir)
|
||||
try {
|
||||
if (!file.exists()) {
|
||||
|
|
@ -75,7 +76,7 @@ class FileUtilsWrapper @Inject constructor(private val context: Context) {
|
|||
}
|
||||
|
||||
FileOutputStream(file).use { fos ->
|
||||
fos.write(data)
|
||||
fos.write(data, 0, size)
|
||||
}
|
||||
} catch (throwable: Exception) {
|
||||
Timber.e(throwable, "Failed to create file")
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP
|
|||
import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK
|
||||
import fr.free.nrw.commons.utils.ImageUtilsWrapper
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.Function
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import timber.log.Timber
|
||||
|
|
@ -26,7 +25,7 @@ class ImageProcessingService @Inject constructor(
|
|||
private val fileUtilsWrapper: FileUtilsWrapper,
|
||||
private val imageUtilsWrapper: ImageUtilsWrapper,
|
||||
private val readFBMD: ReadFBMD,
|
||||
private val EXIFReader: EXIFReader,
|
||||
private val exifReader: EXIFReader,
|
||||
private val mediaClient: MediaClient
|
||||
) {
|
||||
/**
|
||||
|
|
@ -94,7 +93,7 @@ class ImageProcessingService @Inject constructor(
|
|||
* the presence of some basic Exif metadata.
|
||||
*/
|
||||
private fun checkEXIF(filepath: String): Single<Int> =
|
||||
EXIFReader.processMetadata(filepath)
|
||||
exifReader.processMetadata(filepath)
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import java.util.Locale
|
|||
*/
|
||||
class LanguagesAdapter constructor(
|
||||
context: Context,
|
||||
private val selectedLanguages: HashMap<*, String>,
|
||||
private val selectedLanguages: MutableMap<Int, String>,
|
||||
) : ArrayAdapter<String?>(context, R.layout.row_item_languages_spinner) {
|
||||
companion object {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package fr.free.nrw.commons.upload
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
|
|
@ -13,6 +16,7 @@ import androidx.paging.PagedListAdapter
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.facebook.imagepipeline.request.ImageRequest
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.contributions.Contribution
|
||||
import java.io.File
|
||||
|
|
@ -99,6 +103,22 @@ class PendingUploadsAdapter(
|
|||
|
||||
fun bind(contribution: Contribution) {
|
||||
titleTextView.text = contribution.media.displayTitle
|
||||
val clipboardManager =
|
||||
itemView.context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
val clip = ClipData.newPlainText(
|
||||
itemView.context.getString(R.string.caption),
|
||||
titleTextView.text
|
||||
)
|
||||
clipboardManager.setPrimaryClip(clip)
|
||||
Snackbar.make(
|
||||
itemView,
|
||||
itemView.context.getString(R.string.caption_copied_to_clipboard),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
true
|
||||
}
|
||||
|
||||
val imageSource: String = contribution.localUri.toString()
|
||||
var imageRequest: ImageRequest? = null
|
||||
|
|
|
|||
|
|
@ -1,985 +0,0 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
|
||||
import static fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction;
|
||||
import static fr.free.nrw.commons.utils.PermissionUtils.getPERMISSIONS_STORAGE;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.contributions.ContributionController;
|
||||
import fr.free.nrw.commons.databinding.ActivityUploadBinding;
|
||||
import fr.free.nrw.commons.filepicker.Constants.RequestCodes;
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
||||
import fr.free.nrw.commons.kvstore.BasicKvStore;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.location.LocationPermissionsHelper;
|
||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||
import fr.free.nrw.commons.mwapi.UserClient;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.upload.UploadBaseFragment.Callback;
|
||||
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsFragment;
|
||||
import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter;
|
||||
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
||||
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;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class UploadActivity extends BaseActivity implements
|
||||
UploadContract.View, UploadBaseFragment.Callback, ThumbnailsAdapter.OnThumbnailDeletedListener {
|
||||
|
||||
@Inject
|
||||
ContributionController contributionController;
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
JsonKvStore directKvStore;
|
||||
@Inject
|
||||
UploadContract.UserActionListener presenter;
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
@Inject
|
||||
UserClient userClient;
|
||||
@Inject
|
||||
LocationServiceManager locationManager;
|
||||
|
||||
private boolean isTitleExpanded = true;
|
||||
|
||||
private CompositeDisposable compositeDisposable;
|
||||
private ProgressDialog progressDialog;
|
||||
private UploadImageAdapter uploadImagesAdapter;
|
||||
private List<UploadBaseFragment> fragments;
|
||||
private UploadCategoriesFragment uploadCategoriesFragment;
|
||||
private DepictsFragment depictsFragment;
|
||||
private MediaLicenseFragment mediaLicenseFragment;
|
||||
private ThumbnailsAdapter thumbnailsAdapter;
|
||||
BasicKvStore store;
|
||||
private Place place;
|
||||
private LatLng prevLocation;
|
||||
private LatLng currLocation;
|
||||
private static boolean uploadIsOfAPlace = false;
|
||||
private boolean isInAppCameraUpload;
|
||||
private List<UploadableFile> 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";
|
||||
public static final String LOCATION_BEFORE_IMAGE_CAPTURE = "user_location_before_image_capture";
|
||||
public static final String IN_APP_CAMERA_UPLOAD = "in_app_camera_upload";
|
||||
|
||||
/**
|
||||
* Stores all nearby places found and related users response for
|
||||
* each place while uploading media
|
||||
*/
|
||||
public static HashMap<Place,Boolean> nearbyPopupAnswers;
|
||||
|
||||
/**
|
||||
* A private boolean variable to control whether a permissions dialog should be shown
|
||||
* when necessary. Initially, it is set to `true`, indicating that the permissions dialog
|
||||
* should be displayed if permissions are missing and it is first time calling
|
||||
* `checkStoragePermissions` method.
|
||||
* This variable is used in the `checkStoragePermissions` method to determine whether to
|
||||
* show a permissions dialog to the user if the required permissions are not granted.
|
||||
* If `showPermissionsDialog` is set to `true` and the necessary permissions are missing,
|
||||
* a permissions dialog will be displayed to request the required permissions. If set
|
||||
* to `false`, the dialog won't be shown.
|
||||
*
|
||||
* @see UploadActivity#checkStoragePermissions()
|
||||
*/
|
||||
private boolean showPermissionsDialog = true;
|
||||
|
||||
/**
|
||||
* Whether fragments have been saved.
|
||||
*/
|
||||
private boolean isFragmentsSaved = false;
|
||||
|
||||
public static final String keyForCurrentUploadImagesSize = "CurrentUploadImagesSize";
|
||||
public static final String storeNameForCurrentUploadImagesSize = "CurrentUploadImageQualities";
|
||||
|
||||
private ActivityUploadBinding binding;
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
binding = ActivityUploadBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
/*
|
||||
If Configuration of device is changed then get the new fragments
|
||||
created by the system and populate the fragments ArrayList
|
||||
*/
|
||||
if (savedInstanceState != null) {
|
||||
isFragmentsSaved = true;
|
||||
final List<Fragment> fragmentList = getSupportFragmentManager().getFragments();
|
||||
fragments = new ArrayList<>();
|
||||
for (final Fragment fragment : fragmentList) {
|
||||
fragments.add((UploadBaseFragment) fragment);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
compositeDisposable = new CompositeDisposable();
|
||||
init();
|
||||
binding.rlContainerTitle.setOnClickListener(v -> onRlContainerTitleClicked());
|
||||
nearbyPopupAnswers = new HashMap<>();
|
||||
//getting the current dpi of the device and if it is less than 320dp i.e. overlapping
|
||||
//threshold, thumbnails automatically minimizes
|
||||
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||
final float dpi = (metrics.widthPixels)/(metrics.density);
|
||||
if (dpi<=321) {
|
||||
onRlContainerTitleClicked();
|
||||
}
|
||||
if (PermissionUtils.hasPermission(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) {
|
||||
locationManager.registerLocationManager();
|
||||
}
|
||||
locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
|
||||
locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER);
|
||||
store = new BasicKvStore(this, storeNameForCurrentUploadImagesSize);
|
||||
store.clearAll();
|
||||
checkStoragePermissions();
|
||||
|
||||
}
|
||||
|
||||
private void init() {
|
||||
initProgressDialog();
|
||||
initViewPager();
|
||||
initThumbnailsRecyclerView();
|
||||
//And init other things you need to
|
||||
}
|
||||
|
||||
private void initProgressDialog() {
|
||||
progressDialog = new ProgressDialog(this);
|
||||
progressDialog.setMessage(getString(R.string.please_wait));
|
||||
progressDialog.setCancelable(false);
|
||||
}
|
||||
|
||||
private void initThumbnailsRecyclerView() {
|
||||
binding.rvThumbnails.setLayoutManager(new LinearLayoutManager(this,
|
||||
LinearLayoutManager.HORIZONTAL, false));
|
||||
thumbnailsAdapter = new ThumbnailsAdapter(() -> currentSelectedPosition);
|
||||
thumbnailsAdapter.setOnThumbnailDeletedListener(this);
|
||||
binding.rvThumbnails.setAdapter(thumbnailsAdapter);
|
||||
|
||||
}
|
||||
|
||||
private void initViewPager() {
|
||||
uploadImagesAdapter = new UploadImageAdapter(getSupportFragmentManager());
|
||||
binding.vpUpload.setAdapter(uploadImagesAdapter);
|
||||
binding.vpUpload.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageScrolled(final int position, final float positionOffset,
|
||||
final int positionOffsetPixels) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(final int position) {
|
||||
currentSelectedPosition = position;
|
||||
if (position >= uploadableFiles.size()) {
|
||||
binding.cvContainerTopCard.setVisibility(View.GONE);
|
||||
} else {
|
||||
thumbnailsAdapter.notifyDataSetChanged();
|
||||
binding.cvContainerTopCard.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(final int state) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggedIn() {
|
||||
return sessionManager.isUserLoggedIn();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
presenter.onAttachView(this);
|
||||
if (!isLoggedIn()) {
|
||||
askUserToLogIn();
|
||||
}
|
||||
checkBlockStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes API call to check if user is blocked from Commons. If the user is blocked, a snackbar
|
||||
* is created to notify the user
|
||||
*/
|
||||
protected void checkBlockStatus() {
|
||||
compositeDisposable.add(userClient.isUserBlockedFromCommons()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.filter(result -> result)
|
||||
.subscribe(result -> DialogUtil.showAlertDialog(
|
||||
this,
|
||||
getString(R.string.block_notification_title),
|
||||
getString(R.string.block_notification),
|
||||
getString(R.string.ok),
|
||||
this::finish)));
|
||||
}
|
||||
|
||||
public void checkStoragePermissions() {
|
||||
// Check if all required permissions are granted
|
||||
final boolean hasAllPermissions = PermissionUtils.hasPermission(this, getPERMISSIONS_STORAGE());
|
||||
final boolean hasPartialAccess = PermissionUtils.hasPartialAccess(this);
|
||||
if (hasAllPermissions || hasPartialAccess) {
|
||||
// All required permissions are granted, so enable UI elements and perform actions
|
||||
receiveSharedItems();
|
||||
binding.cvContainerTopCard.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// Permissions are missing
|
||||
binding.cvContainerTopCard.setVisibility(View.INVISIBLE);
|
||||
if(showPermissionsDialog){
|
||||
checkPermissionsAndPerformAction(this,
|
||||
() -> {
|
||||
binding.cvContainerTopCard.setVisibility(View.VISIBLE);
|
||||
this.receiveSharedItems();
|
||||
},() -> {
|
||||
this.showPermissionsDialog = true;
|
||||
this.checkStoragePermissions();
|
||||
},
|
||||
R.string.storage_permission_title,
|
||||
R.string.write_storage_permission_rationale_for_image_share,
|
||||
getPERMISSIONS_STORAGE());
|
||||
}
|
||||
}
|
||||
/* If all permissions are not granted and a dialog is already showing on screen
|
||||
showPermissionsDialog will set to false making it not show dialog again onResume,
|
||||
but if user Denies any permission showPermissionsDialog will be to true
|
||||
and permissions dialog will be shown again.
|
||||
*/
|
||||
this.showPermissionsDialog = hasAllPermissions ;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
// Resetting setImageCancelled to false
|
||||
setImageCancelled(false);
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void returnToMainActivity() {
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* go to the uploadProgress activity to check the status of uploading
|
||||
*/
|
||||
@Override
|
||||
public void goToUploadProgressActivity() {
|
||||
startActivity(new Intent(this, UploadProgressActivity.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show/Hide the progress dialog
|
||||
*/
|
||||
@Override
|
||||
public void showProgress(final boolean shouldShow) {
|
||||
if (shouldShow) {
|
||||
if (!progressDialog.isShowing()) {
|
||||
progressDialog.show();
|
||||
}
|
||||
} else {
|
||||
if (progressDialog != null && !isFinishing()) {
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndexInViewFlipper(final UploadBaseFragment fragment) {
|
||||
return fragments.indexOf(fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalNumberOfSteps() {
|
||||
return fragments.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWLMUpload() {
|
||||
return place!=null && place.isMonument();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMessage(final int messageResourceId) {
|
||||
ViewUtil.showLongToast(this, messageResourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UploadableFile> getUploadableFiles() {
|
||||
return uploadableFiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showHideTopCard(final boolean shouldShow) {
|
||||
binding.llContainerTopCard.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUploadMediaDeleted(final int index) {
|
||||
fragments.remove(index);//Remove the corresponding fragment
|
||||
uploadableFiles.remove(index);//Remove the files from the list
|
||||
thumbnailsAdapter.notifyItemRemoved(index); //Notify the thumbnails adapter
|
||||
uploadImagesAdapter.notifyDataSetChanged(); //Notify the ViewPager
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTopCardTitle() {
|
||||
binding.tvTopCardTitle.setText(getResources()
|
||||
.getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void makeUploadRequest() {
|
||||
WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(),
|
||||
ExistingWorkPolicy.APPEND_OR_REPLACE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void askUserToLogIn() {
|
||||
Timber.d("current session is null, asking user to login");
|
||||
ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in));
|
||||
final Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class);
|
||||
startActivity(loginIntent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults) {
|
||||
boolean areAllGranted = false;
|
||||
if (requestCode == RequestCodes.STORAGE) {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.M) {
|
||||
for (int i = 0; i < grantResults.length; i++) {
|
||||
final String permission = permissions[i];
|
||||
areAllGranted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
|
||||
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
|
||||
final boolean showRationale = shouldShowRequestPermissionRationale(permission);
|
||||
if (!showRationale) {
|
||||
DialogUtil.showAlertDialog(this,
|
||||
getString(R.string.storage_permissions_denied),
|
||||
getString(R.string.unable_to_share_upload_item),
|
||||
getString(android.R.string.ok),
|
||||
this::finish);
|
||||
} else {
|
||||
DialogUtil.showAlertDialog(this,
|
||||
getString(R.string.storage_permission_title),
|
||||
getString(
|
||||
R.string.write_storage_permission_rationale_for_image_share),
|
||||
getString(android.R.string.ok),
|
||||
this::checkStoragePermissions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (areAllGranted) {
|
||||
receiveSharedItems();
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the flag indicating whether the upload is of a specific place.
|
||||
*
|
||||
* @param uploadOfAPlace a boolean value indicating whether the upload is of place.
|
||||
*/
|
||||
public static void setUploadIsOfAPlace(final boolean uploadOfAPlace) {
|
||||
uploadIsOfAPlace = uploadOfAPlace;
|
||||
}
|
||||
|
||||
private void receiveSharedItems() {
|
||||
final Intent intent = getIntent();
|
||||
final String action = intent.getAction();
|
||||
if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
||||
receiveExternalSharedItems();
|
||||
} else if (ACTION_INTERNAL_UPLOADS.equals(action)) {
|
||||
receiveInternalSharedItems();
|
||||
}
|
||||
|
||||
if (uploadableFiles == null || uploadableFiles.isEmpty()) {
|
||||
handleNullMedia();
|
||||
} else {
|
||||
//Show thumbnails
|
||||
if (uploadableFiles.size() > 1){
|
||||
if(!defaultKvStore.getBoolean("hasAlreadyLaunchedCategoriesDialog")){//If there is only file, no need to show the image thumbnails
|
||||
showAlertDialogForCategories();
|
||||
}
|
||||
if (uploadableFiles.size() > 3 &&
|
||||
!defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload")){
|
||||
showAlertForBattery();
|
||||
}
|
||||
thumbnailsAdapter.setUploadableFiles(uploadableFiles);
|
||||
} else {
|
||||
binding.llContainerTopCard.setVisibility(View.GONE);
|
||||
}
|
||||
binding.tvTopCardTitle.setText(getResources()
|
||||
.getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size()));
|
||||
|
||||
|
||||
if(fragments == null){
|
||||
fragments = new ArrayList<>();
|
||||
}
|
||||
|
||||
|
||||
for (final UploadableFile uploadableFile : uploadableFiles) {
|
||||
final UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment();
|
||||
|
||||
if (!uploadIsOfAPlace) {
|
||||
handleLocation();
|
||||
uploadMediaDetailFragment.setImageToBeUploaded(uploadableFile, place, currLocation);
|
||||
locationManager.unregisterLocationManager();
|
||||
} else {
|
||||
uploadMediaDetailFragment.setImageToBeUploaded(uploadableFile, place, currLocation);
|
||||
}
|
||||
|
||||
final UploadMediaDetailFragmentCallback uploadMediaDetailFragmentCallback = new UploadMediaDetailFragmentCallback() {
|
||||
@Override
|
||||
public void deletePictureAtIndex(final int index) {
|
||||
store.putInt(keyForCurrentUploadImagesSize,
|
||||
(store.getInt(keyForCurrentUploadImagesSize) - 1));
|
||||
presenter.deletePictureAtIndex(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the thumbnail of an UploadableFile at the specified index.
|
||||
* This method updates the list of uploadableFiles by replacing the UploadableFile
|
||||
* at the given index with a new UploadableFile created from the provided file path.
|
||||
* After updating the list, it notifies the RecyclerView's adapter to refresh its data,
|
||||
* ensuring that the thumbnail change is reflected in the UI.
|
||||
*
|
||||
* @param index The index of the UploadableFile to be updated.
|
||||
* @param filepath The file path of the new thumbnail image.
|
||||
*/
|
||||
@Override
|
||||
public void changeThumbnail(final int index, final String filepath) {
|
||||
uploadableFiles.remove(index);
|
||||
uploadableFiles.add(index, new UploadableFile(new File(filepath)));
|
||||
binding.rvThumbnails.getAdapter().notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNextButtonClicked(final int index) {
|
||||
UploadActivity.this.onNextButtonClicked(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreviousButtonClicked(final int index) {
|
||||
UploadActivity.this.onPreviousButtonClicked(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showProgress(final boolean shouldShow) {
|
||||
UploadActivity.this.showProgress(shouldShow);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndexInViewFlipper(final UploadBaseFragment fragment) {
|
||||
return fragments.indexOf(fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalNumberOfSteps() {
|
||||
return fragments.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWLMUpload() {
|
||||
return place!=null && place.isMonument();
|
||||
}
|
||||
};
|
||||
|
||||
if(isFragmentsSaved){
|
||||
final UploadMediaDetailFragment fragment = (UploadMediaDetailFragment) fragments.get(0);
|
||||
fragment.setCallback(uploadMediaDetailFragmentCallback);
|
||||
}else{
|
||||
uploadMediaDetailFragment.setCallback(uploadMediaDetailFragmentCallback);
|
||||
fragments.add(uploadMediaDetailFragment);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//If fragments are not created, create them and add them to the fragments ArrayList
|
||||
if(!isFragmentsSaved){
|
||||
uploadCategoriesFragment = new UploadCategoriesFragment();
|
||||
if (place != null) {
|
||||
final Bundle categoryBundle = new Bundle();
|
||||
categoryBundle.putString(SELECTED_NEARBY_PLACE_CATEGORY, place.getCategory());
|
||||
uploadCategoriesFragment.setArguments(categoryBundle);
|
||||
}
|
||||
|
||||
uploadCategoriesFragment.setCallback(this);
|
||||
|
||||
depictsFragment = new DepictsFragment();
|
||||
final Bundle placeBundle = new Bundle();
|
||||
placeBundle.putParcelable(SELECTED_NEARBY_PLACE, place);
|
||||
depictsFragment.setArguments(placeBundle);
|
||||
depictsFragment.setCallback(this);
|
||||
|
||||
mediaLicenseFragment = new MediaLicenseFragment();
|
||||
mediaLicenseFragment.setCallback(this);
|
||||
|
||||
fragments.add(depictsFragment);
|
||||
fragments.add(uploadCategoriesFragment);
|
||||
fragments.add(mediaLicenseFragment);
|
||||
|
||||
}else{
|
||||
for(int i=1;i<fragments.size();i++){
|
||||
fragments.get(i).setCallback(new Callback() {
|
||||
@Override
|
||||
public void onNextButtonClicked(final int index) {
|
||||
if (index < fragments.size() - 1) {
|
||||
binding.vpUpload.setCurrentItem(index + 1, false);
|
||||
fragments.get(index + 1).onBecameVisible();
|
||||
((LinearLayoutManager) binding.rvThumbnails.getLayoutManager())
|
||||
.scrollToPositionWithOffset((index > 0) ? index-1 : 0, 0);
|
||||
} else {
|
||||
presenter.handleSubmit();
|
||||
}
|
||||
|
||||
}
|
||||
@Override
|
||||
public void onPreviousButtonClicked(final int index) {
|
||||
if (index != 0) {
|
||||
binding.vpUpload.setCurrentItem(index - 1, true);
|
||||
fragments.get(index - 1).onBecameVisible();
|
||||
((LinearLayoutManager) binding.rvThumbnails.getLayoutManager())
|
||||
.scrollToPositionWithOffset((index > 3) ? index-2 : 0, 0);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void showProgress(final boolean shouldShow) {
|
||||
if (shouldShow) {
|
||||
if (!progressDialog.isShowing()) {
|
||||
progressDialog.show();
|
||||
}
|
||||
} else {
|
||||
if (progressDialog != null && !isFinishing()) {
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public int getIndexInViewFlipper(final UploadBaseFragment fragment) {
|
||||
return fragments.indexOf(fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalNumberOfSteps() {
|
||||
return fragments.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWLMUpload() {
|
||||
return place!=null && place.isMonument();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
uploadImagesAdapter.setFragments(fragments);
|
||||
binding.vpUpload.setOffscreenPageLimit(fragments.size());
|
||||
|
||||
}
|
||||
// Saving size of uploadableFiles
|
||||
store.putInt(keyForCurrentUploadImagesSize, uploadableFiles.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Users may uncheck Location tag from the Manage EXIF tags setting any time.
|
||||
* So, their location must not be shared in this case.
|
||||
*
|
||||
*/
|
||||
private boolean isLocationTagUncheckedInTheSettings() {
|
||||
final Set<String> prefExifTags = defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS);
|
||||
if (prefExifTags.contains(getString(R.string.exif_tag_location))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes current image when one image upload is cancelled, to highlight next image in the top thumbnail.
|
||||
* Fixes: <a href="https://github.com/commons-app/apps-android-commons/issues/5511">Issue</a>
|
||||
*
|
||||
* @param index Index of image to be removed
|
||||
* @param maxSize Max size of the {@code uploadableFiles}
|
||||
*/
|
||||
@Override
|
||||
public void highlightNextImageOnCancelledImage(final int index, final int maxSize) {
|
||||
if (binding.vpUpload != null && index < (maxSize)) {
|
||||
binding.vpUpload.setCurrentItem(index + 1, false);
|
||||
binding.vpUpload.setCurrentItem(index, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check if user has cancelled upload of any image in current upload
|
||||
* so that location compare doesn't show up again in same upload.
|
||||
* Fixes: <a href="https://github.com/commons-app/apps-android-commons/issues/5511">Issue</a>
|
||||
*
|
||||
* @param isCancelled Is true when user has cancelled upload of any image in current upload
|
||||
*/
|
||||
@Override
|
||||
public void setImageCancelled(final boolean isCancelled) {
|
||||
final BasicKvStore basicKvStore = new BasicKvStore(this,"IsAnyImageCancelled");
|
||||
basicKvStore.putBoolean("IsAnyImageCancelled", isCancelled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the difference between current location and
|
||||
* location recorded before capturing the image
|
||||
*
|
||||
*/
|
||||
private float getLocationDifference(final LatLng currLocation, final LatLng prevLocation) {
|
||||
if (prevLocation == null) {
|
||||
return 0.0f;
|
||||
}
|
||||
final float[] distance = new float[2];
|
||||
Location.distanceBetween(
|
||||
currLocation.getLatitude(), currLocation.getLongitude(),
|
||||
prevLocation.getLatitude(), prevLocation.getLongitude(), distance);
|
||||
return distance[0];
|
||||
}
|
||||
|
||||
private void receiveExternalSharedItems() {
|
||||
uploadableFiles = contributionController.handleExternalImagesPicked(this, getIntent());
|
||||
}
|
||||
|
||||
private void receiveInternalSharedItems() {
|
||||
final Intent intent = getIntent();
|
||||
|
||||
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);
|
||||
prevLocation = intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE);
|
||||
isInAppCameraUpload = intent.getBooleanExtra(IN_APP_CAMERA_UPLOAD, false);
|
||||
resetDirectPrefs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if multiple files selected or not.
|
||||
*/
|
||||
public boolean getIsMultipleFilesSelected() {
|
||||
return isMultipleFilesSelected;
|
||||
}
|
||||
|
||||
public void resetDirectPrefs() {
|
||||
directKvStore.remove(PLACE_OBJECT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle null URI from the received intent.
|
||||
* Current implementation will simply show a toast and finish the upload activity.
|
||||
*/
|
||||
private void handleNullMedia() {
|
||||
ViewUtil.showLongToast(this, R.string.error_processing_image);
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void showAlertDialog(final int messageResourceId, @NonNull final Runnable onPositiveClick) {
|
||||
DialogUtil.showAlertDialog(this,
|
||||
"",
|
||||
getString(messageResourceId),
|
||||
getString(R.string.ok),
|
||||
onPositiveClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNextButtonClicked(final int index) {
|
||||
if (index < fragments.size() - 1) {
|
||||
binding.vpUpload.setCurrentItem(index + 1, false);
|
||||
fragments.get(index + 1).onBecameVisible();
|
||||
((LinearLayoutManager) binding.rvThumbnails.getLayoutManager())
|
||||
.scrollToPositionWithOffset((index > 0) ? index - 1 : 0, 0);
|
||||
if (index < fragments.size() - 4) {
|
||||
// check image quality if next image exists
|
||||
presenter.checkImageQuality(index + 1);
|
||||
}
|
||||
} else {
|
||||
presenter.handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreviousButtonClicked(final int index) {
|
||||
if (index != 0) {
|
||||
binding.vpUpload.setCurrentItem(index - 1, true);
|
||||
fragments.get(index - 1).onBecameVisible();
|
||||
((LinearLayoutManager) binding.rvThumbnails.getLayoutManager())
|
||||
.scrollToPositionWithOffset((index > 3) ? index-2 : 0, 0);
|
||||
if ((index != 1) && ((index - 1) < uploadableFiles.size())) {
|
||||
// Shows the top card if it was hidden because of the last image being deleted and
|
||||
// now the user has hit previous button to go back to the media details
|
||||
showHideTopCard(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onThumbnailDeleted(final int position) {
|
||||
presenter.deletePictureAtIndex(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* The adapter used to show image upload intermediate fragments
|
||||
*/
|
||||
|
||||
|
||||
private static class UploadImageAdapter extends FragmentStatePagerAdapter {
|
||||
List<UploadBaseFragment> fragments;
|
||||
|
||||
public UploadImageAdapter(final FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
this.fragments = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void setFragments(final List<UploadBaseFragment> fragments) {
|
||||
this.fragments = fragments;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(final int position) {
|
||||
return fragments.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return fragments.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(@NonNull final Object item) {
|
||||
return PagerAdapter.POSITION_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void onRlContainerTitleClicked() {
|
||||
binding.rvThumbnails.setVisibility(isTitleExpanded ? View.GONE : View.VISIBLE);
|
||||
isTitleExpanded = !isTitleExpanded;
|
||||
binding.ibToggleTopCard.setRotation(binding.ibToggleTopCard.getRotation() + 180);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// Resetting all values in store by clearing them
|
||||
store.clearAll();
|
||||
presenter.onDetachView();
|
||||
compositeDisposable.clear();
|
||||
fragments = null;
|
||||
uploadImagesAdapter = null;
|
||||
if (mediaLicenseFragment != null) {
|
||||
mediaLicenseFragment.setCallback(null);
|
||||
}
|
||||
if (uploadCategoriesFragment != null) {
|
||||
uploadCategoriesFragment.setCallback(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the showPermissionDialog variable.
|
||||
*
|
||||
* @return {@code true} if Permission Dialog should be shown, {@code false} otherwise.
|
||||
*/
|
||||
public boolean isShowPermissionsDialog() {
|
||||
return showPermissionsDialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of the showPermissionDialog variable.
|
||||
*
|
||||
* @param showPermissionsDialog {@code true} to indicate to show
|
||||
* Permissions Dialog if permissions are missing, {@code false} otherwise.
|
||||
*/
|
||||
public void setShowPermissionsDialog(final boolean showPermissionsDialog) {
|
||||
this.showPermissionsDialog = showPermissionsDialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user uploads more than 1 file informs that
|
||||
* depictions/categories apply to all pictures of a multi upload.
|
||||
* This method takes no arguments and does not return any value.
|
||||
* It shows the AlertDialog and continues the flow of uploads.
|
||||
*/
|
||||
private void showAlertDialogForCategories() {
|
||||
UploadMediaPresenter.isCategoriesDialogShowing = true;
|
||||
// Inflate the custom layout
|
||||
final LayoutInflater inflater = getLayoutInflater();
|
||||
final View view = inflater.inflate(R.layout.activity_upload_categories_dialog, null);
|
||||
final CheckBox checkBox = view.findViewById(R.id.categories_checkbox);
|
||||
// Create the alert dialog
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(this)
|
||||
.setView(view)
|
||||
.setTitle(getString(R.string.multiple_files_depiction_header))
|
||||
.setMessage(getString(R.string.multiple_files_depiction))
|
||||
.setPositiveButton("OK", (dialog, which) -> {
|
||||
if (checkBox.isChecked()) {
|
||||
// Save the user's choice to not show the dialog again
|
||||
defaultKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", true);
|
||||
}
|
||||
presenter.checkImageQuality(0);
|
||||
|
||||
UploadMediaPresenter.isCategoriesDialogShowing = false;
|
||||
})
|
||||
.setNegativeButton("", null)
|
||||
.create();
|
||||
alertDialog.show();
|
||||
}
|
||||
|
||||
|
||||
/** Suggest users to turn battery optimisation off when uploading
|
||||
* more than a few files. That's because we have noticed that
|
||||
* many-files uploads have a much higher probability of failing
|
||||
* than uploads with less files. Show the dialog for Android 6
|
||||
* and above as the ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
|
||||
* intent was added in API level 23
|
||||
*/
|
||||
private void showAlertForBattery(){
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// When battery-optimisation dialog is shown don't show the image quality dialog
|
||||
UploadMediaPresenter.isBatteryDialogShowing = true;
|
||||
DialogUtil.showAlertDialog(
|
||||
this,
|
||||
getString(R.string.unrestricted_battery_mode),
|
||||
getString(R.string.suggest_unrestricted_mode),
|
||||
getString(R.string.title_activity_settings),
|
||||
getString(R.string.cancel),
|
||||
() -> {
|
||||
/* Since opening the right settings page might be device dependent, using
|
||||
https://github.com/WaseemSabir/BatteryPermissionHelper
|
||||
directly appeared like a promising idea.
|
||||
However, this simply closed the popup and did not make
|
||||
the settings page appear on a Pixel as well as a Xiaomi device.
|
||||
Used the standard intent instead of using this library as
|
||||
it shows a list of all the apps on the device and allows users to
|
||||
turn battery optimisation off.
|
||||
*/
|
||||
final Intent batteryOptimisationSettingsIntent = new Intent(
|
||||
Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
|
||||
startActivity(batteryOptimisationSettingsIntent);
|
||||
// calling checkImageQuality after battery dialog is interacted with
|
||||
// so that 2 dialogs do not pop up simultaneously
|
||||
|
||||
UploadMediaPresenter.isBatteryDialogShowing = false;
|
||||
},
|
||||
() -> {
|
||||
UploadMediaPresenter.isBatteryDialogShowing = false;
|
||||
}
|
||||
);
|
||||
defaultKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the permission for Location is turned on and certain
|
||||
* conditions are met, returns current location of the user.
|
||||
*/
|
||||
private void handleLocation(){
|
||||
final LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper(
|
||||
this, locationManager, null);
|
||||
if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
|
||||
currLocation = locationManager.getLastLocation();
|
||||
}
|
||||
|
||||
if (currLocation != null) {
|
||||
final float locationDifference = getLocationDifference(currLocation, prevLocation);
|
||||
final boolean isLocationTagUnchecked = isLocationTagUncheckedInTheSettings();
|
||||
/* Remove location if the user has unchecked the Location EXIF tag in the
|
||||
Manage EXIF Tags setting or turned "Record location for in-app shots" off.
|
||||
Also, location information is discarded if the difference between
|
||||
current location and location recorded just before capturing the image
|
||||
is greater than 100 meters */
|
||||
if (isLocationTagUnchecked || locationDifference > 100
|
||||
|| !defaultKvStore.getBoolean("inAppCameraLocationPref")
|
||||
|| !isInAppCameraUpload) {
|
||||
currLocation = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
995
app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt
Normal file
995
app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt
Normal file
|
|
@ -0,0 +1,995 @@
|
|||
package fr.free.nrw.commons.upload
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ProgressDialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.viewpager.widget.ViewPager.OnPageChangeListener
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.contributions.ContributionController
|
||||
import fr.free.nrw.commons.databinding.ActivityUploadBinding
|
||||
import fr.free.nrw.commons.filepicker.Constants.RequestCodes
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile
|
||||
import fr.free.nrw.commons.kvstore.BasicKvStore
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
import fr.free.nrw.commons.location.LocationPermissionsHelper
|
||||
import fr.free.nrw.commons.location.LocationServiceManager
|
||||
import fr.free.nrw.commons.mwapi.UserClient
|
||||
import fr.free.nrw.commons.nearby.Place
|
||||
import fr.free.nrw.commons.settings.Prefs
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
import fr.free.nrw.commons.upload.ThumbnailsAdapter.OnThumbnailDeletedListener
|
||||
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsFragment
|
||||
import fr.free.nrw.commons.upload.license.MediaLicenseFragment
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter
|
||||
import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest
|
||||
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||
import fr.free.nrw.commons.utils.PermissionUtils.PERMISSIONS_STORAGE
|
||||
import fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction
|
||||
import fr.free.nrw.commons.utils.PermissionUtils.hasPartialAccess
|
||||
import fr.free.nrw.commons.utils.PermissionUtils.hasPermission
|
||||
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
|
||||
import fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT
|
||||
import fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE
|
||||
import fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.Callback,
|
||||
OnThumbnailDeletedListener {
|
||||
@JvmField
|
||||
@Inject
|
||||
var contributionController: ContributionController? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
@field:Named("default_preferences")
|
||||
var directKvStore: JsonKvStore? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var presenter: UploadContract.UserActionListener? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var sessionManager: SessionManager? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var userClient: UserClient? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var locationManager: LocationServiceManager? = null
|
||||
|
||||
private var isTitleExpanded = true
|
||||
|
||||
private var progressDialog: ProgressDialog? = null
|
||||
private var uploadImagesAdapter: UploadImageAdapter? = null
|
||||
private var fragments: MutableList<UploadBaseFragment>? = null
|
||||
private var uploadCategoriesFragment: UploadCategoriesFragment? = null
|
||||
private var depictsFragment: DepictsFragment? = null
|
||||
private var mediaLicenseFragment: MediaLicenseFragment? = null
|
||||
private var thumbnailsAdapter: ThumbnailsAdapter? = null
|
||||
var store: BasicKvStore? = null
|
||||
private var place: Place? = null
|
||||
private var prevLocation: LatLng? = null
|
||||
private var currLocation: LatLng? = null
|
||||
private var isInAppCameraUpload = false
|
||||
private var uploadableFiles: MutableList<UploadableFile> = mutableListOf()
|
||||
private var currentSelectedPosition = 0
|
||||
|
||||
/**
|
||||
* Returns if multiple files selected or not.
|
||||
*/
|
||||
/*
|
||||
Checks for if multiple files selected
|
||||
*/
|
||||
var isMultipleFilesSelected: Boolean = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Get the value of the showPermissionDialog variable.
|
||||
*
|
||||
* @return `true` if Permission Dialog should be shown, `false` otherwise.
|
||||
*/
|
||||
/**
|
||||
* Set the value of the showPermissionDialog variable.
|
||||
*
|
||||
* @param showPermissionsDialog `true` to indicate to show
|
||||
* Permissions Dialog if permissions are missing, `false` otherwise.
|
||||
*/
|
||||
/**
|
||||
* A private boolean variable to control whether a permissions dialog should be shown
|
||||
* when necessary. Initially, it is set to `true`, indicating that the permissions dialog
|
||||
* should be displayed if permissions are missing and it is first time calling
|
||||
* `checkStoragePermissions` method.
|
||||
* This variable is used in the `checkStoragePermissions` method to determine whether to
|
||||
* show a permissions dialog to the user if the required permissions are not granted.
|
||||
* If `showPermissionsDialog` is set to `true` and the necessary permissions are missing,
|
||||
* a permissions dialog will be displayed to request the required permissions. If set
|
||||
* to `false`, the dialog won't be shown.
|
||||
*
|
||||
* @see UploadActivity.checkStoragePermissions
|
||||
*/
|
||||
var isShowPermissionsDialog: Boolean = true
|
||||
|
||||
/**
|
||||
* Whether fragments have been saved.
|
||||
*/
|
||||
private var isFragmentsSaved = false
|
||||
|
||||
override val totalNumberOfSteps: Int
|
||||
get() = fragments!!.size
|
||||
|
||||
override val isWLMUpload: Boolean
|
||||
get() = place != null && place!!.isMonument
|
||||
|
||||
/**
|
||||
* Users may uncheck Location tag from the Manage EXIF tags setting any time.
|
||||
* So, their location must not be shared in this case.
|
||||
*
|
||||
*/
|
||||
private val isLocationTagUncheckedInTheSettings: Boolean
|
||||
get() {
|
||||
val prefExifTags: Set<String> =
|
||||
defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS)
|
||||
return !prefExifTags.contains(getString(R.string.exif_tag_location))
|
||||
}
|
||||
|
||||
private var _binding: ActivityUploadBinding? = null
|
||||
private val binding: ActivityUploadBinding get() = _binding!!
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
_binding = ActivityUploadBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
/*
|
||||
If Configuration of device is changed then get the new fragments
|
||||
created by the system and populate the fragments ArrayList
|
||||
*/
|
||||
if (savedInstanceState != null) {
|
||||
isFragmentsSaved = true
|
||||
fragments = mutableListOf<UploadBaseFragment>().apply {
|
||||
supportFragmentManager.fragments.forEach { fragment ->
|
||||
add(fragment as UploadBaseFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init()
|
||||
binding.rlContainerTitle.setOnClickListener { v: View? -> onRlContainerTitleClicked() }
|
||||
nearbyPopupAnswers = mutableMapOf()
|
||||
//getting the current dpi of the device and if it is less than 320dp i.e. overlapping
|
||||
//threshold, thumbnails automatically minimizes
|
||||
val metrics = resources.displayMetrics
|
||||
val dpi = (metrics.widthPixels) / (metrics.density)
|
||||
if (dpi <= 321) {
|
||||
onRlContainerTitleClicked()
|
||||
}
|
||||
if (hasPermission(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION))) {
|
||||
locationManager!!.registerLocationManager()
|
||||
}
|
||||
locationManager!!.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER)
|
||||
locationManager!!.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
|
||||
store = BasicKvStore(this, storeNameForCurrentUploadImagesSize).apply {
|
||||
clearAll()
|
||||
}
|
||||
checkStoragePermissions()
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
initProgressDialog()
|
||||
initViewPager()
|
||||
initThumbnailsRecyclerView()
|
||||
//And init other things you need to
|
||||
}
|
||||
|
||||
private fun initProgressDialog() {
|
||||
progressDialog = ProgressDialog(this)
|
||||
progressDialog!!.setMessage(getString(R.string.please_wait))
|
||||
progressDialog!!.setCancelable(false)
|
||||
}
|
||||
|
||||
private fun initThumbnailsRecyclerView() {
|
||||
binding.rvThumbnails.layoutManager = LinearLayoutManager(
|
||||
this,
|
||||
LinearLayoutManager.HORIZONTAL, false
|
||||
)
|
||||
thumbnailsAdapter = ThumbnailsAdapter { currentSelectedPosition }
|
||||
thumbnailsAdapter!!.onThumbnailDeletedListener = this
|
||||
binding.rvThumbnails.adapter = thumbnailsAdapter
|
||||
}
|
||||
|
||||
private fun initViewPager() {
|
||||
uploadImagesAdapter = UploadImageAdapter(supportFragmentManager)
|
||||
binding.vpUpload.adapter = uploadImagesAdapter
|
||||
binding.vpUpload.addOnPageChangeListener(object : OnPageChangeListener {
|
||||
override fun onPageScrolled(
|
||||
position: Int, positionOffset: Float,
|
||||
positionOffsetPixels: Int
|
||||
) = Unit
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
currentSelectedPosition = position
|
||||
if (position >= uploadableFiles!!.size) {
|
||||
binding.cvContainerTopCard.visibility = View.GONE
|
||||
} else {
|
||||
thumbnailsAdapter!!.notifyDataSetChanged()
|
||||
binding.cvContainerTopCard.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageScrollStateChanged(state: Int) = Unit
|
||||
})
|
||||
}
|
||||
|
||||
override fun isLoggedIn(): Boolean = sessionManager!!.isUserLoggedIn
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
presenter!!.onAttachView(this)
|
||||
if (!isLoggedIn()) {
|
||||
askUserToLogIn()
|
||||
}
|
||||
checkBlockStatus()
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes API call to check if user is blocked from Commons. If the user is blocked, a snackbar
|
||||
* is created to notify the user
|
||||
*/
|
||||
protected fun checkBlockStatus() {
|
||||
compositeDisposable.add(
|
||||
userClient!!.isUserBlockedFromCommons()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.filter { result: Boolean? -> result!! }
|
||||
.subscribe { result: Boolean? ->
|
||||
showAlertDialog(
|
||||
this,
|
||||
getString(R.string.block_notification_title),
|
||||
getString(R.string.block_notification),
|
||||
getString(R.string.ok)
|
||||
) { finish() }
|
||||
})
|
||||
}
|
||||
|
||||
fun checkStoragePermissions() {
|
||||
// Check if all required permissions are granted
|
||||
val hasAllPermissions = hasPermission(this, PERMISSIONS_STORAGE)
|
||||
val hasPartialAccess = hasPartialAccess(this)
|
||||
if (hasAllPermissions || hasPartialAccess) {
|
||||
// All required permissions are granted, so enable UI elements and perform actions
|
||||
receiveSharedItems()
|
||||
binding.cvContainerTopCard.visibility = View.VISIBLE
|
||||
} else {
|
||||
// Permissions are missing
|
||||
binding.cvContainerTopCard.visibility = View.INVISIBLE
|
||||
if (isShowPermissionsDialog) {
|
||||
checkPermissionsAndPerformAction(
|
||||
this,
|
||||
Runnable {
|
||||
binding.cvContainerTopCard.visibility = View.VISIBLE
|
||||
receiveSharedItems()
|
||||
}, Runnable {
|
||||
isShowPermissionsDialog = true
|
||||
checkStoragePermissions()
|
||||
},
|
||||
R.string.storage_permission_title,
|
||||
R.string.write_storage_permission_rationale_for_image_share,
|
||||
*PERMISSIONS_STORAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
/* If all permissions are not granted and a dialog is already showing on screen
|
||||
showPermissionsDialog will set to false making it not show dialog again onResume,
|
||||
but if user Denies any permission showPermissionsDialog will be to true
|
||||
and permissions dialog will be shown again.
|
||||
*/
|
||||
isShowPermissionsDialog = hasAllPermissions
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
// Resetting setImageCancelled to false
|
||||
setImageCancelled(false)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun returnToMainActivity() = finish()
|
||||
|
||||
/**
|
||||
* go to the uploadProgress activity to check the status of uploading
|
||||
*/
|
||||
override fun goToUploadProgressActivity() =
|
||||
startActivity(Intent(this, UploadProgressActivity::class.java))
|
||||
|
||||
/**
|
||||
* Show/Hide the progress dialog
|
||||
*/
|
||||
override fun showProgress(shouldShow: Boolean) {
|
||||
if (shouldShow) {
|
||||
if (!progressDialog!!.isShowing) {
|
||||
progressDialog!!.show()
|
||||
}
|
||||
} else {
|
||||
if (progressDialog != null && !isFinishing) {
|
||||
progressDialog!!.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getIndexInViewFlipper(fragment: UploadBaseFragment?): Int =
|
||||
fragments!!.indexOf(fragment)
|
||||
|
||||
override fun showMessage(messageResourceId: Int) {
|
||||
showLongToast(this, messageResourceId)
|
||||
}
|
||||
|
||||
override fun getUploadableFiles(): List<UploadableFile>? {
|
||||
return uploadableFiles
|
||||
}
|
||||
|
||||
override fun showHideTopCard(shouldShow: Boolean) {
|
||||
binding.llContainerTopCard.visibility =
|
||||
if (shouldShow) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun onUploadMediaDeleted(index: Int) {
|
||||
fragments!!.removeAt(index) //Remove the corresponding fragment
|
||||
uploadableFiles.removeAt(index) //Remove the files from the list
|
||||
thumbnailsAdapter!!.notifyItemRemoved(index) //Notify the thumbnails adapter
|
||||
uploadImagesAdapter!!.notifyDataSetChanged() //Notify the ViewPager
|
||||
}
|
||||
|
||||
override fun updateTopCardTitle() {
|
||||
binding.tvTopCardTitle.text = resources
|
||||
.getQuantityString(
|
||||
R.plurals.upload_count_title,
|
||||
uploadableFiles!!.size,
|
||||
uploadableFiles!!.size
|
||||
)
|
||||
}
|
||||
|
||||
override fun makeUploadRequest() {
|
||||
makeOneTimeWorkRequest(
|
||||
applicationContext,
|
||||
ExistingWorkPolicy.APPEND_OR_REPLACE
|
||||
)
|
||||
}
|
||||
|
||||
override fun askUserToLogIn() {
|
||||
Timber.d("current session is null, asking user to login")
|
||||
showLongToast(this, getString(R.string.user_not_logged_in))
|
||||
val loginIntent = Intent(this@UploadActivity, LoginActivity::class.java)
|
||||
startActivity(loginIntent)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
var areAllGranted = false
|
||||
if (requestCode == RequestCodes.STORAGE) {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.M) {
|
||||
for (i in grantResults.indices) {
|
||||
val permission = permissions[i]
|
||||
areAllGranted = grantResults[i] == PackageManager.PERMISSION_GRANTED
|
||||
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
|
||||
val showRationale = shouldShowRequestPermissionRationale(permission)
|
||||
if (!showRationale) {
|
||||
showAlertDialog(
|
||||
this,
|
||||
getString(R.string.storage_permissions_denied),
|
||||
getString(R.string.unable_to_share_upload_item),
|
||||
getString(android.R.string.ok)
|
||||
) { finish() }
|
||||
} else {
|
||||
showAlertDialog(
|
||||
this,
|
||||
getString(R.string.storage_permission_title),
|
||||
getString(
|
||||
R.string.write_storage_permission_rationale_for_image_share
|
||||
),
|
||||
getString(android.R.string.ok)
|
||||
) { checkStoragePermissions() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (areAllGranted) {
|
||||
receiveSharedItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
private fun receiveSharedItems() {
|
||||
val intent = intent
|
||||
val action = intent.action
|
||||
if (Intent.ACTION_SEND == action || Intent.ACTION_SEND_MULTIPLE == action) {
|
||||
receiveExternalSharedItems()
|
||||
} else if (ContributionController.ACTION_INTERNAL_UPLOADS == action) {
|
||||
receiveInternalSharedItems()
|
||||
}
|
||||
|
||||
if (uploadableFiles == null || uploadableFiles!!.isEmpty()) {
|
||||
handleNullMedia()
|
||||
} else {
|
||||
//Show thumbnails
|
||||
if (uploadableFiles!!.size > 1) {
|
||||
if (!defaultKvStore.getBoolean("hasAlreadyLaunchedCategoriesDialog")) { //If there is only file, no need to show the image thumbnails
|
||||
showAlertDialogForCategories()
|
||||
}
|
||||
if (uploadableFiles!!.size > 3 &&
|
||||
!defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload")
|
||||
) {
|
||||
showAlertForBattery()
|
||||
}
|
||||
thumbnailsAdapter!!.uploadableFiles = uploadableFiles
|
||||
} else {
|
||||
binding.llContainerTopCard.visibility = View.GONE
|
||||
}
|
||||
binding.tvTopCardTitle.text = resources
|
||||
.getQuantityString(
|
||||
R.plurals.upload_count_title,
|
||||
uploadableFiles!!.size,
|
||||
uploadableFiles!!.size
|
||||
)
|
||||
|
||||
|
||||
if (fragments == null) {
|
||||
fragments = mutableListOf()
|
||||
}
|
||||
|
||||
|
||||
for (uploadableFile in uploadableFiles!!) {
|
||||
val uploadMediaDetailFragment = UploadMediaDetailFragment()
|
||||
|
||||
if (!uploadIsOfAPlace) {
|
||||
handleLocation()
|
||||
uploadMediaDetailFragment.setImageToBeUploaded(
|
||||
uploadableFile,
|
||||
place,
|
||||
currLocation
|
||||
)
|
||||
locationManager!!.unregisterLocationManager()
|
||||
} else {
|
||||
uploadMediaDetailFragment.setImageToBeUploaded(
|
||||
uploadableFile,
|
||||
place,
|
||||
currLocation
|
||||
)
|
||||
}
|
||||
|
||||
val uploadMediaDetailFragmentCallback: UploadMediaDetailFragmentCallback =
|
||||
object : UploadMediaDetailFragmentCallback {
|
||||
override fun deletePictureAtIndex(index: Int) {
|
||||
store!!.putInt(
|
||||
keyForCurrentUploadImagesSize,
|
||||
(store!!.getInt(keyForCurrentUploadImagesSize) - 1)
|
||||
)
|
||||
presenter!!.deletePictureAtIndex(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the thumbnail of an UploadableFile at the specified index.
|
||||
* This method updates the list of uploadableFiles by replacing the UploadableFile
|
||||
* at the given index with a new UploadableFile created from the provided file path.
|
||||
* After updating the list, it notifies the RecyclerView's adapter to refresh its data,
|
||||
* ensuring that the thumbnail change is reflected in the UI.
|
||||
*
|
||||
* @param index The index of the UploadableFile to be updated.
|
||||
* @param uri The file path of the new thumbnail image.
|
||||
*/
|
||||
override fun changeThumbnail(index: Int, uri: String) {
|
||||
uploadableFiles.removeAt(index)
|
||||
uploadableFiles.add(index, UploadableFile(File(uri)))
|
||||
binding.rvThumbnails.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onNextButtonClicked(index: Int) {
|
||||
this@UploadActivity.onNextButtonClicked(index)
|
||||
}
|
||||
|
||||
override fun onPreviousButtonClicked(index: Int) {
|
||||
this@UploadActivity.onPreviousButtonClicked(index)
|
||||
}
|
||||
|
||||
override fun showProgress(shouldShow: Boolean) {
|
||||
this@UploadActivity.showProgress(shouldShow)
|
||||
}
|
||||
|
||||
override fun getIndexInViewFlipper(fragment: UploadBaseFragment?): Int {
|
||||
return fragments!!.indexOf(fragment)
|
||||
}
|
||||
|
||||
override val totalNumberOfSteps: Int
|
||||
get() = fragments!!.size
|
||||
|
||||
override val isWLMUpload: Boolean
|
||||
get() = place != null && place!!.isMonument
|
||||
}
|
||||
|
||||
if (isFragmentsSaved) {
|
||||
val fragment = fragments!![0] as UploadMediaDetailFragment?
|
||||
fragment!!.fragmentCallback = uploadMediaDetailFragmentCallback
|
||||
} else {
|
||||
uploadMediaDetailFragment.fragmentCallback = uploadMediaDetailFragmentCallback
|
||||
fragments!!.add(uploadMediaDetailFragment)
|
||||
}
|
||||
}
|
||||
|
||||
//If fragments are not created, create them and add them to the fragments ArrayList
|
||||
if (!isFragmentsSaved) {
|
||||
uploadCategoriesFragment = UploadCategoriesFragment()
|
||||
if (place != null) {
|
||||
val categoryBundle = Bundle()
|
||||
categoryBundle.putString(SELECTED_NEARBY_PLACE_CATEGORY, place!!.category)
|
||||
uploadCategoriesFragment!!.arguments = categoryBundle
|
||||
}
|
||||
|
||||
uploadCategoriesFragment!!.callback = this
|
||||
|
||||
depictsFragment = DepictsFragment()
|
||||
val placeBundle = Bundle()
|
||||
placeBundle.putParcelable(SELECTED_NEARBY_PLACE, place)
|
||||
depictsFragment!!.arguments = placeBundle
|
||||
depictsFragment!!.callback = this
|
||||
|
||||
mediaLicenseFragment = MediaLicenseFragment()
|
||||
mediaLicenseFragment!!.callback = this
|
||||
|
||||
fragments!!.add(depictsFragment!!)
|
||||
fragments!!.add(uploadCategoriesFragment!!)
|
||||
fragments!!.add(mediaLicenseFragment!!)
|
||||
} else {
|
||||
for (i in 1 until fragments!!.size) {
|
||||
fragments!![i]!!.callback = object : UploadBaseFragment.Callback {
|
||||
override fun onNextButtonClicked(index: Int) {
|
||||
if (index < fragments!!.size - 1) {
|
||||
binding.vpUpload.setCurrentItem(index + 1, false)
|
||||
fragments!![index + 1]!!.onBecameVisible()
|
||||
(binding.rvThumbnails.layoutManager as LinearLayoutManager)
|
||||
.scrollToPositionWithOffset(
|
||||
if ((index > 0)) index - 1 else 0,
|
||||
0
|
||||
)
|
||||
} else {
|
||||
presenter!!.handleSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreviousButtonClicked(index: Int) {
|
||||
if (index != 0) {
|
||||
binding.vpUpload.setCurrentItem(index - 1, true)
|
||||
fragments!![index - 1]!!.onBecameVisible()
|
||||
(binding.rvThumbnails.layoutManager as LinearLayoutManager)
|
||||
.scrollToPositionWithOffset(
|
||||
if ((index > 3)) index - 2 else 0,
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showProgress(shouldShow: Boolean) {
|
||||
if (shouldShow) {
|
||||
if (!progressDialog!!.isShowing) {
|
||||
progressDialog!!.show()
|
||||
}
|
||||
} else {
|
||||
if (progressDialog != null && !isFinishing) {
|
||||
progressDialog!!.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getIndexInViewFlipper(fragment: UploadBaseFragment?): Int {
|
||||
return fragments!!.indexOf(fragment)
|
||||
}
|
||||
|
||||
override val totalNumberOfSteps: Int
|
||||
get() = fragments!!.size
|
||||
|
||||
override val isWLMUpload: Boolean
|
||||
get() = place != null && place!!.isMonument
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uploadImagesAdapter!!.fragments = fragments!!
|
||||
binding.vpUpload.offscreenPageLimit = fragments!!.size
|
||||
}
|
||||
// Saving size of uploadableFiles
|
||||
store!!.putInt(keyForCurrentUploadImagesSize, uploadableFiles!!.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes current image when one image upload is cancelled, to highlight next image in the top thumbnail.
|
||||
* Fixes: [Issue](https://github.com/commons-app/apps-android-commons/issues/5511)
|
||||
*
|
||||
* @param index Index of image to be removed
|
||||
* @param maxSize Max size of the `uploadableFiles`
|
||||
*/
|
||||
override fun highlightNextImageOnCancelledImage(index: Int, maxSize: Int) {
|
||||
if (index < maxSize) {
|
||||
binding.vpUpload.setCurrentItem(index + 1, false)
|
||||
binding.vpUpload.setCurrentItem(index, false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check if user has cancelled upload of any image in current upload
|
||||
* so that location compare doesn't show up again in same upload.
|
||||
* Fixes: [Issue](https://github.com/commons-app/apps-android-commons/issues/5511)
|
||||
*
|
||||
* @param isCancelled Is true when user has cancelled upload of any image in current upload
|
||||
*/
|
||||
override fun setImageCancelled(isCancelled: Boolean) {
|
||||
val basicKvStore = BasicKvStore(this, "IsAnyImageCancelled")
|
||||
basicKvStore.putBoolean("IsAnyImageCancelled", isCancelled)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the difference between current location and
|
||||
* location recorded before capturing the image
|
||||
*
|
||||
*/
|
||||
private fun getLocationDifference(currLocation: LatLng, prevLocation: LatLng?): Float {
|
||||
if (prevLocation == null) {
|
||||
return 0.0f
|
||||
}
|
||||
val distance = FloatArray(2)
|
||||
Location.distanceBetween(
|
||||
currLocation.latitude, currLocation.longitude,
|
||||
prevLocation.latitude, prevLocation.longitude, distance
|
||||
)
|
||||
return distance[0]
|
||||
}
|
||||
|
||||
private fun receiveExternalSharedItems() {
|
||||
uploadableFiles = contributionController!!.handleExternalImagesPicked(this, intent)
|
||||
}
|
||||
|
||||
private fun receiveInternalSharedItems() {
|
||||
val intent = intent
|
||||
Timber.d("Intent has EXTRA_FILES: ${EXTRA_FILES}")
|
||||
uploadableFiles = try {
|
||||
// Check if intent has the extra before trying to read it
|
||||
if (!intent.hasExtra(EXTRA_FILES)) {
|
||||
Timber.w("No EXTRA_FILES found in intent")
|
||||
mutableListOf()
|
||||
} else {
|
||||
// Try to get the files as Parcelable array
|
||||
val files = if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableArrayListExtra(EXTRA_FILES, UploadableFile::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableArrayListExtra<UploadableFile>(EXTRA_FILES)
|
||||
}
|
||||
|
||||
// Convert to mutable list or return empty list if null
|
||||
files?.toMutableList() ?: run {
|
||||
Timber.w("Files array was null")
|
||||
mutableListOf()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error reading files from intent")
|
||||
mutableListOf()
|
||||
}
|
||||
|
||||
// Log the result for debugging
|
||||
isMultipleFilesSelected = uploadableFiles.size > 1
|
||||
Timber.i("Received files count: ${uploadableFiles.size}")
|
||||
uploadableFiles.forEachIndexed { index, file ->
|
||||
Timber.d("File $index path: ${file.getFilePath()}")
|
||||
}
|
||||
|
||||
// Handle other extras with null safety
|
||||
place = try {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(PLACE_OBJECT, Place::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra(PLACE_OBJECT)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error reading place")
|
||||
null
|
||||
}
|
||||
|
||||
prevLocation = try {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE, LatLng::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error reading location")
|
||||
null
|
||||
}
|
||||
|
||||
isInAppCameraUpload = intent.getBooleanExtra(IN_APP_CAMERA_UPLOAD, false)
|
||||
resetDirectPrefs()
|
||||
}
|
||||
|
||||
fun resetDirectPrefs() = directKvStore!!.remove(PLACE_OBJECT)
|
||||
|
||||
/**
|
||||
* Handle null URI from the received intent.
|
||||
* Current implementation will simply show a toast and finish the upload activity.
|
||||
*/
|
||||
private fun handleNullMedia() {
|
||||
showLongToast(this, R.string.error_processing_image)
|
||||
finish()
|
||||
}
|
||||
|
||||
|
||||
override fun showAlertDialog(messageResourceId: Int, onPositiveClick: Runnable) {
|
||||
showAlertDialog(
|
||||
this,
|
||||
"",
|
||||
getString(messageResourceId),
|
||||
getString(R.string.ok),
|
||||
onPositiveClick
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNextButtonClicked(index: Int) {
|
||||
if (index < fragments!!.size - 1) {
|
||||
binding.vpUpload.setCurrentItem(index + 1, false)
|
||||
fragments!![index + 1]!!.onBecameVisible()
|
||||
(binding.rvThumbnails.layoutManager as LinearLayoutManager)
|
||||
.scrollToPositionWithOffset(if ((index > 0)) index - 1 else 0, 0)
|
||||
if (index < fragments!!.size - 4) {
|
||||
// check image quality if next image exists
|
||||
presenter!!.checkImageQuality(index + 1)
|
||||
}
|
||||
} else {
|
||||
presenter!!.handleSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreviousButtonClicked(index: Int) {
|
||||
if (index != 0) {
|
||||
binding.vpUpload.setCurrentItem(index - 1, true)
|
||||
fragments!![index - 1]!!.onBecameVisible()
|
||||
(binding.rvThumbnails.layoutManager as LinearLayoutManager)
|
||||
.scrollToPositionWithOffset(if ((index > 3)) index - 2 else 0, 0)
|
||||
if ((index != 1) && ((index - 1) < uploadableFiles!!.size)) {
|
||||
// Shows the top card if it was hidden because of the last image being deleted and
|
||||
// now the user has hit previous button to go back to the media details
|
||||
showHideTopCard(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onThumbnailDeleted(position: Int) = presenter!!.deletePictureAtIndex(position)
|
||||
|
||||
/**
|
||||
* The adapter used to show image upload intermediate fragments
|
||||
*/
|
||||
private class UploadImageAdapter(fragmentManager: FragmentManager) :
|
||||
FragmentStatePagerAdapter(fragmentManager) {
|
||||
var fragments: List<UploadBaseFragment> = mutableListOf()
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
return fragments[position]
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return fragments.size
|
||||
}
|
||||
|
||||
override fun getItemPosition(item: Any): Int {
|
||||
return POSITION_NONE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun onRlContainerTitleClicked() {
|
||||
binding.rvThumbnails.visibility =
|
||||
if (isTitleExpanded) View.GONE else View.VISIBLE
|
||||
isTitleExpanded = !isTitleExpanded
|
||||
binding.ibToggleTopCard.rotation = binding.ibToggleTopCard.rotation + 180
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// Resetting all values in store by clearing them
|
||||
store!!.clearAll()
|
||||
presenter!!.onDetachView()
|
||||
compositeDisposable.clear()
|
||||
fragments = null
|
||||
uploadImagesAdapter = null
|
||||
if (mediaLicenseFragment != null) {
|
||||
mediaLicenseFragment!!.callback = null
|
||||
}
|
||||
if (uploadCategoriesFragment != null) {
|
||||
uploadCategoriesFragment!!.callback = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the back button to make sure the user is prepared to lose their progress
|
||||
*/
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onBackPressed() {
|
||||
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
|
||||
) { finish() }
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user uploads more than 1 file informs that
|
||||
* depictions/categories apply to all pictures of a multi upload.
|
||||
* This method takes no arguments and does not return any value.
|
||||
* It shows the AlertDialog and continues the flow of uploads.
|
||||
*/
|
||||
private fun showAlertDialogForCategories() {
|
||||
UploadMediaPresenter.isCategoriesDialogShowing = true
|
||||
// Inflate the custom layout
|
||||
val inflater = layoutInflater
|
||||
val view = inflater.inflate(R.layout.activity_upload_categories_dialog, null)
|
||||
val checkBox = view.findViewById<CheckBox>(R.id.categories_checkbox)
|
||||
// Create the alert dialog
|
||||
val alertDialog = AlertDialog.Builder(this)
|
||||
.setView(view)
|
||||
.setTitle(getString(R.string.multiple_files_depiction_header))
|
||||
.setMessage(getString(R.string.multiple_files_depiction))
|
||||
.setPositiveButton("OK") { dialog: DialogInterface?, which: Int ->
|
||||
if (checkBox.isChecked) {
|
||||
// Save the user's choice to not show the dialog again
|
||||
defaultKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", true)
|
||||
}
|
||||
presenter!!.setupBasicKvStoreFactory { BasicKvStore(this@UploadActivity, it) }
|
||||
presenter!!.checkImageQuality(0)
|
||||
UploadMediaPresenter.isCategoriesDialogShowing = false
|
||||
}
|
||||
.setNegativeButton("", null)
|
||||
.create()
|
||||
alertDialog.show()
|
||||
}
|
||||
|
||||
|
||||
/** Suggest users to turn battery optimisation off when uploading
|
||||
* more than a few files. That's because we have noticed that
|
||||
* many-files uploads have a much higher probability of failing
|
||||
* than uploads with less files. Show the dialog for Android 6
|
||||
* and above as the ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
|
||||
* intent was added in API level 23
|
||||
*/
|
||||
private fun showAlertForBattery() {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.M) {
|
||||
// When battery-optimisation dialog is shown don't show the image quality dialog
|
||||
UploadMediaPresenter.isBatteryDialogShowing = true
|
||||
showAlertDialog(
|
||||
this,
|
||||
getString(R.string.unrestricted_battery_mode),
|
||||
getString(R.string.suggest_unrestricted_mode),
|
||||
getString(R.string.title_activity_settings),
|
||||
getString(R.string.cancel),
|
||||
{
|
||||
/* Since opening the right settings page might be device dependent, using
|
||||
https://github.com/WaseemSabir/BatteryPermissionHelper
|
||||
directly appeared like a promising idea.
|
||||
However, this simply closed the popup and did not make
|
||||
the settings page appear on a Pixel as well as a Xiaomi device.
|
||||
Used the standard intent instead of using this library as
|
||||
it shows a list of all the apps on the device and allows users to
|
||||
turn battery optimisation off.
|
||||
*/
|
||||
val batteryOptimisationSettingsIntent = Intent(
|
||||
Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
|
||||
)
|
||||
startActivity(batteryOptimisationSettingsIntent)
|
||||
|
||||
// calling checkImageQuality after battery dialog is interacted with
|
||||
// so that 2 dialogs do not pop up simultaneously
|
||||
UploadMediaPresenter.isBatteryDialogShowing = false
|
||||
},
|
||||
{
|
||||
UploadMediaPresenter.isBatteryDialogShowing = false
|
||||
}
|
||||
)
|
||||
defaultKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the permission for Location is turned on and certain
|
||||
* conditions are met, returns current location of the user.
|
||||
*/
|
||||
private fun handleLocation() {
|
||||
val locationPermissionsHelper = LocationPermissionsHelper(
|
||||
this, locationManager!!, null
|
||||
)
|
||||
if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
|
||||
currLocation = locationManager!!.getLastLocation()
|
||||
}
|
||||
|
||||
if (currLocation != null) {
|
||||
val locationDifference = getLocationDifference(currLocation!!, prevLocation)
|
||||
val isLocationTagUnchecked = isLocationTagUncheckedInTheSettings
|
||||
/* Remove location if the user has unchecked the Location EXIF tag in the
|
||||
Manage EXIF Tags setting or turned "Record location for in-app shots" off.
|
||||
Also, location information is discarded if the difference between
|
||||
current location and location recorded just before capturing the image
|
||||
is greater than 100 meters */
|
||||
if (isLocationTagUnchecked || locationDifference > 100 || !defaultKvStore.getBoolean("inAppCameraLocationPref")
|
||||
|| !isInAppCameraUpload
|
||||
) {
|
||||
currLocation = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var uploadIsOfAPlace = false
|
||||
const val EXTRA_FILES: String = "commons_image_extra"
|
||||
const val LOCATION_BEFORE_IMAGE_CAPTURE: String = "user_location_before_image_capture"
|
||||
const val IN_APP_CAMERA_UPLOAD: String = "in_app_camera_upload"
|
||||
|
||||
/**
|
||||
* Stores all nearby places found and related users response for
|
||||
* each place while uploading media
|
||||
*/
|
||||
@JvmField
|
||||
var nearbyPopupAnswers: MutableMap<Place, Boolean>? = null
|
||||
|
||||
const val keyForCurrentUploadImagesSize: String = "CurrentUploadImagesSize"
|
||||
const val storeNameForCurrentUploadImagesSize: String = "CurrentUploadImageQualities"
|
||||
|
||||
/**
|
||||
* Sets the flag indicating whether the upload is of a specific place.
|
||||
*
|
||||
* @param uploadOfAPlace a boolean value indicating whether the upload is of place.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun setUploadIsOfAPlace(uploadOfAPlace: Boolean) {
|
||||
uploadIsOfAPlace = uploadOfAPlace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
|||
abstract class UploadBaseFragment : CommonsDaggerSupportFragment() {
|
||||
var callback: Callback? = null
|
||||
|
||||
protected open fun onBecameVisible() = Unit
|
||||
open fun onBecameVisible() = Unit
|
||||
|
||||
interface Callback {
|
||||
val totalNumberOfSteps: Int
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.upload
|
|||
|
||||
import fr.free.nrw.commons.BasePresenter
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile
|
||||
import fr.free.nrw.commons.kvstore.BasicKvStore
|
||||
|
||||
/**
|
||||
* The contract using which the UplaodActivity would communicate with its presenter
|
||||
|
|
@ -73,5 +74,7 @@ interface UploadContract {
|
|||
* @param uploadItemIndex Index of next image, whose quality is to be checked
|
||||
*/
|
||||
fun checkImageQuality(uploadItemIndex: Int)
|
||||
|
||||
fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import fr.free.nrw.commons.Utils
|
|||
import fr.free.nrw.commons.filepicker.MimeTypeMapWrapper.Companion.getExtensionFromMimeType
|
||||
import fr.free.nrw.commons.nearby.Place
|
||||
import fr.free.nrw.commons.utils.ImageUtils
|
||||
import io.reactivex.subjects.BehaviorSubject
|
||||
|
||||
class UploadItem(
|
||||
var mediaUri: Uri?,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import kotlinx.parcelize.Parcelize
|
|||
* Holds a description of an item being uploaded by [UploadActivity]
|
||||
*/
|
||||
@Parcelize
|
||||
data class UploadMediaDetail constructor(
|
||||
data class UploadMediaDetail(
|
||||
/**
|
||||
* The language code ie. "en" or "fr".
|
||||
* @param languageCode The language code ie. "en" or "fr".
|
||||
|
|
@ -18,7 +18,7 @@ data class UploadMediaDetail constructor(
|
|||
* The description text for the item being uploaded.
|
||||
* @param descriptionText The description text.
|
||||
*/
|
||||
var descriptionText: String = "",
|
||||
var descriptionText: String? = "",
|
||||
/**
|
||||
* The caption text for the item being uploaded.
|
||||
* @param captionText The caption text.
|
||||
|
|
@ -27,10 +27,10 @@ data class UploadMediaDetail constructor(
|
|||
) : Parcelable {
|
||||
fun javaCopy() = copy()
|
||||
|
||||
constructor(place: Place) : this(
|
||||
place.language,
|
||||
place.longDescription,
|
||||
place.name,
|
||||
constructor(place: Place?) : this(
|
||||
place?.language,
|
||||
place?.longDescription,
|
||||
place?.name ?: "",
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,629 +0,0 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
import android.speech.RecognizerIntent;
|
||||
import android.text.Editable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.databinding.RowItemDescriptionBinding;
|
||||
import fr.free.nrw.commons.recentlanguages.Language;
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter;
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao;
|
||||
import fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText;
|
||||
import fr.free.nrw.commons.utils.AbstractTextWatcher;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class UploadMediaDetailAdapter extends
|
||||
RecyclerView.Adapter<UploadMediaDetailAdapter.ViewHolder> {
|
||||
|
||||
RecentLanguagesDao recentLanguagesDao;
|
||||
|
||||
private List<UploadMediaDetail> uploadMediaDetails;
|
||||
private Callback callback;
|
||||
private EventListener eventListener;
|
||||
|
||||
private HashMap<Integer, String> selectedLanguages;
|
||||
private final String savedLanguageValue;
|
||||
private TextView recentLanguagesTextView;
|
||||
private View separator;
|
||||
private ListView languageHistoryListView;
|
||||
private int currentPosition;
|
||||
private Fragment fragment;
|
||||
private Activity activity;
|
||||
private final ActivityResultLauncher<Intent> voiceInputResultLauncher;
|
||||
private SelectedVoiceIcon selectedVoiceIcon;
|
||||
|
||||
private RowItemDescriptionBinding binding;
|
||||
|
||||
public UploadMediaDetailAdapter(Fragment fragment, String savedLanguageValue,
|
||||
RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher<Intent> voiceInputResultLauncher) {
|
||||
uploadMediaDetails = new ArrayList<>();
|
||||
selectedLanguages = new HashMap<>();
|
||||
this.savedLanguageValue = savedLanguageValue;
|
||||
this.recentLanguagesDao = recentLanguagesDao;
|
||||
this.fragment = fragment;
|
||||
this.voiceInputResultLauncher = voiceInputResultLauncher;
|
||||
}
|
||||
|
||||
public UploadMediaDetailAdapter(Activity activity, final String savedLanguageValue,
|
||||
List<UploadMediaDetail> uploadMediaDetails, RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher<Intent> voiceInputResultLauncher) {
|
||||
this.uploadMediaDetails = uploadMediaDetails;
|
||||
selectedLanguages = new HashMap<>();
|
||||
this.savedLanguageValue = savedLanguageValue;
|
||||
this.recentLanguagesDao = recentLanguagesDao;
|
||||
this.activity = activity;
|
||||
this.voiceInputResultLauncher = voiceInputResultLauncher;
|
||||
}
|
||||
|
||||
public void setCallback(Callback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void setEventListener(EventListener eventListener) {
|
||||
this.eventListener = eventListener;
|
||||
}
|
||||
|
||||
public void setItems(List<UploadMediaDetail> uploadMediaDetails) {
|
||||
this.uploadMediaDetails = uploadMediaDetails;
|
||||
selectedLanguages = new HashMap<>();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public List<UploadMediaDetail> getItems() {
|
||||
return uploadMediaDetails;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
binding = RowItemDescriptionBinding.inflate(inflater, parent, false);
|
||||
return new ViewHolder(binding.getRoot());
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a workaround for a known bug by android here
|
||||
* https://issuetracker.google.com/issues/37095917 makes the edit text on second and subsequent
|
||||
* fragments inside an adapter receptive to long click for copy/paste options
|
||||
*
|
||||
* @param holder the view holder
|
||||
*/
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@NonNull final ViewHolder holder) {
|
||||
super.onViewAttachedToWindow(holder);
|
||||
holder.captionItemEditText.setEnabled(false);
|
||||
holder.captionItemEditText.setEnabled(true);
|
||||
holder.descItemEditText.setEnabled(false);
|
||||
holder.descItemEditText.setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return uploadMediaDetails.size();
|
||||
}
|
||||
|
||||
public void addDescription(UploadMediaDetail uploadMediaDetail) {
|
||||
selectedLanguages.put(uploadMediaDetails.size(), "en");
|
||||
this.uploadMediaDetails.add(uploadMediaDetail);
|
||||
notifyItemInserted(uploadMediaDetails.size());
|
||||
}
|
||||
|
||||
private void startSpeechInput(String locale) {
|
||||
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
|
||||
intent.putExtra(
|
||||
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
|
||||
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
|
||||
);
|
||||
intent.putExtra(
|
||||
RecognizerIntent.EXTRA_LANGUAGE,
|
||||
locale
|
||||
);
|
||||
|
||||
try {
|
||||
voiceInputResultLauncher.launch(intent);
|
||||
} catch (Exception e) {
|
||||
Timber.e(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the result of the speech input by processing the spoken text.
|
||||
* If the spoken text is not empty, it capitalizes the first letter of the spoken text
|
||||
* and updates the appropriate field (caption or description) of the current
|
||||
* UploadMediaDetail based on the selected voice icon.
|
||||
* Finally, it notifies the adapter that the data set has changed.
|
||||
*
|
||||
* @param spokenText the text input received from speech recognition.
|
||||
*/
|
||||
public void handleSpeechResult(String spokenText) {
|
||||
if (!spokenText.isEmpty()) {
|
||||
String spokenTextCapitalized =
|
||||
spokenText.substring(0, 1).toUpperCase() + spokenText.substring(1);
|
||||
if (currentPosition < uploadMediaDetails.size()) {
|
||||
UploadMediaDetail uploadMediaDetail = uploadMediaDetails.get(currentPosition);
|
||||
switch (selectedVoiceIcon) {
|
||||
case CAPTION:
|
||||
uploadMediaDetail.setCaptionText(spokenTextCapitalized);
|
||||
break;
|
||||
case DESCRIPTION:
|
||||
uploadMediaDetail.setDescriptionText(spokenTextCapitalized);
|
||||
break;
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove description based on position from the list and notifies the RecyclerView Adapter that
|
||||
* data in adapter has been removed at that particular position.
|
||||
*
|
||||
* @param uploadMediaDetail
|
||||
* @param position
|
||||
*/
|
||||
public void removeDescription(final UploadMediaDetail uploadMediaDetail, final int position) {
|
||||
selectedLanguages.remove(position);
|
||||
this.uploadMediaDetails.remove(uploadMediaDetail);
|
||||
int i = position + 1;
|
||||
while (selectedLanguages.containsKey(i)) {
|
||||
selectedLanguages.remove(i);
|
||||
i++;
|
||||
}
|
||||
notifyItemRemoved(position);
|
||||
notifyItemRangeChanged(position, uploadMediaDetails.size() - position);
|
||||
updateAddButtonVisibility();
|
||||
}
|
||||
|
||||
public class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
TextView descriptionLanguages ;
|
||||
|
||||
PasteSensitiveTextInputEditText descItemEditText;
|
||||
|
||||
TextInputLayout descInputLayout;
|
||||
|
||||
PasteSensitiveTextInputEditText captionItemEditText;
|
||||
|
||||
TextInputLayout captionInputLayout;
|
||||
|
||||
ImageView removeButton;
|
||||
|
||||
ImageView addButton;
|
||||
|
||||
ConstraintLayout clParent;
|
||||
|
||||
LinearLayout betterCaptionLinearLayout;
|
||||
|
||||
LinearLayout betterDescriptionLinearLayout;
|
||||
|
||||
private
|
||||
|
||||
AbstractTextWatcher captionListener;
|
||||
|
||||
AbstractTextWatcher descriptionListener;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
Timber.i("descItemEditText:" + descItemEditText);
|
||||
}
|
||||
|
||||
public void bind(int position) {
|
||||
UploadMediaDetail uploadMediaDetail = uploadMediaDetails.get(position);
|
||||
Timber.d("UploadMediaDetail is " + uploadMediaDetail);
|
||||
|
||||
descriptionLanguages = binding.descriptionLanguages;
|
||||
descItemEditText = binding.descriptionItemEditText;
|
||||
descInputLayout = binding.descriptionItemEditTextInputLayout;
|
||||
captionItemEditText = binding.captionItemEditText;
|
||||
captionInputLayout = binding.captionItemEditTextInputLayout;
|
||||
removeButton = binding.btnRemove;
|
||||
addButton = binding.btnAdd;
|
||||
clParent = binding.clParent;
|
||||
betterCaptionLinearLayout = binding.llWriteBetterCaption;
|
||||
betterDescriptionLinearLayout = binding.llWriteBetterDescription;
|
||||
|
||||
|
||||
descriptionLanguages.setFocusable(false);
|
||||
captionItemEditText.addTextChangedListener(new AbstractTextWatcher(
|
||||
value -> {
|
||||
if (position == 0) {
|
||||
eventListener.onPrimaryCaptionTextChange(value.length() != 0);
|
||||
}
|
||||
}));
|
||||
captionItemEditText.removeTextChangedListener(captionListener);
|
||||
descItemEditText.removeTextChangedListener(descriptionListener);
|
||||
captionItemEditText.setText(uploadMediaDetail.getCaptionText());
|
||||
descItemEditText.setText(uploadMediaDetail.getDescriptionText());
|
||||
captionInputLayout.setEndIconMode(TextInputLayout.END_ICON_CUSTOM);
|
||||
captionInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice);
|
||||
captionInputLayout.setEndIconOnClickListener(v -> {
|
||||
currentPosition = position;
|
||||
selectedVoiceIcon = SelectedVoiceIcon.CAPTION;
|
||||
startSpeechInput(descriptionLanguages.getText().toString());
|
||||
});
|
||||
descInputLayout.setEndIconMode(TextInputLayout.END_ICON_CUSTOM);
|
||||
descInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice);
|
||||
descInputLayout.setEndIconOnClickListener(v -> {
|
||||
currentPosition = position;
|
||||
selectedVoiceIcon = SelectedVoiceIcon.DESCRIPTION;
|
||||
startSpeechInput(descriptionLanguages.getText().toString());
|
||||
});
|
||||
|
||||
if (position == 0) {
|
||||
removeButton.setVisibility(View.GONE);
|
||||
betterCaptionLinearLayout.setVisibility(View.VISIBLE);
|
||||
betterCaptionLinearLayout.setOnClickListener(
|
||||
v -> callback.showAlert(R.string.media_detail_caption, R.string.caption_info));
|
||||
betterDescriptionLinearLayout.setVisibility(View.VISIBLE);
|
||||
betterDescriptionLinearLayout.setOnClickListener(
|
||||
v -> callback.showAlert(R.string.media_detail_description,
|
||||
R.string.description_info));
|
||||
Objects.requireNonNull(captionInputLayout.getEditText())
|
||||
.setFilters(new InputFilter[]{
|
||||
new UploadMediaDetailInputFilter()
|
||||
});
|
||||
} else {
|
||||
removeButton.setVisibility(View.VISIBLE);
|
||||
betterCaptionLinearLayout.setVisibility(View.GONE);
|
||||
betterDescriptionLinearLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
removeButton.setOnClickListener(v -> removeDescription(uploadMediaDetail, position));
|
||||
captionListener = new AbstractTextWatcher(
|
||||
captionText -> uploadMediaDetail.setCaptionText(
|
||||
convertIdeographicSpaceToLatinSpace(captionText.strip()))
|
||||
);
|
||||
descriptionListener = new AbstractTextWatcher(
|
||||
descriptionText -> uploadMediaDetail.setDescriptionText(descriptionText));
|
||||
captionItemEditText.addTextChangedListener(captionListener);
|
||||
initLanguage(position, uploadMediaDetail);
|
||||
|
||||
descItemEditText.addTextChangedListener(descriptionListener);
|
||||
initLanguage(position, uploadMediaDetail);
|
||||
|
||||
if (fragment != null) {
|
||||
FrameLayout.LayoutParams newLayoutParams = (FrameLayout.LayoutParams) clParent.getLayoutParams();
|
||||
newLayoutParams.topMargin = 0;
|
||||
newLayoutParams.leftMargin = 0;
|
||||
newLayoutParams.rightMargin = 0;
|
||||
newLayoutParams.bottomMargin = 0;
|
||||
clParent.setLayoutParams(newLayoutParams);
|
||||
}
|
||||
updateAddButtonVisibility();
|
||||
addButton.setOnClickListener(v -> eventListener.addLanguage());
|
||||
|
||||
//If the description was manually added by the user, it deserves focus, if not, let the user decide
|
||||
if (uploadMediaDetail.isManuallyAdded()) {
|
||||
captionItemEditText.requestFocus();
|
||||
} else {
|
||||
captionItemEditText.clearFocus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void initLanguage(int position, UploadMediaDetail description) {
|
||||
|
||||
final List<Language> recentLanguages = recentLanguagesDao.getRecentLanguages();
|
||||
|
||||
LanguagesAdapter languagesAdapter = new LanguagesAdapter(
|
||||
descriptionLanguages.getContext(),
|
||||
selectedLanguages
|
||||
);
|
||||
|
||||
descriptionLanguages.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Dialog dialog = new Dialog(view.getContext());
|
||||
dialog.setContentView(R.layout.dialog_select_language);
|
||||
dialog.setCancelable(false);
|
||||
dialog.getWindow().setLayout(
|
||||
(int) (view.getContext().getResources().getDisplayMetrics().widthPixels
|
||||
* 0.90),
|
||||
(int) (view.getContext().getResources().getDisplayMetrics().heightPixels
|
||||
* 0.90));
|
||||
dialog.show();
|
||||
|
||||
EditText editText = dialog.findViewById(R.id.search_language);
|
||||
ListView listView = dialog.findViewById(R.id.language_list);
|
||||
languageHistoryListView = dialog.findViewById(R.id.language_history_list);
|
||||
recentLanguagesTextView = dialog.findViewById(R.id.recent_searches);
|
||||
separator = dialog.findViewById(R.id.separator);
|
||||
setUpRecentLanguagesSection(recentLanguages);
|
||||
|
||||
listView.setAdapter(languagesAdapter);
|
||||
|
||||
editText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1,
|
||||
int i2) {
|
||||
hideRecentLanguagesSection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1,
|
||||
int i2) {
|
||||
languagesAdapter.getFilter().filter(charSequence);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
languageHistoryListView.setOnItemClickListener(
|
||||
(adapterView, view1, position, id) -> {
|
||||
onRecentLanguageClicked(dialog, adapterView, position, description);
|
||||
});
|
||||
|
||||
listView.setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int i,
|
||||
long l) {
|
||||
description.setSelectedLanguageIndex(i);
|
||||
String languageCode = ((LanguagesAdapter) adapterView.getAdapter())
|
||||
.getLanguageCode(i);
|
||||
description.setLanguageCode(languageCode);
|
||||
final String languageName
|
||||
= ((LanguagesAdapter) adapterView.getAdapter()).getLanguageName(i);
|
||||
final boolean isExists
|
||||
= recentLanguagesDao.findRecentLanguage(languageCode);
|
||||
if (isExists) {
|
||||
recentLanguagesDao.deleteRecentLanguage(languageCode);
|
||||
}
|
||||
recentLanguagesDao
|
||||
.addRecentLanguage(new Language(languageName, languageCode));
|
||||
|
||||
selectedLanguages.clear();
|
||||
selectedLanguages.put(position, languageCode);
|
||||
((LanguagesAdapter) adapterView
|
||||
.getAdapter()).setSelectedLangCode(languageCode);
|
||||
Timber.d("Description language code is: " + languageCode);
|
||||
descriptionLanguages.setText(languageCode);
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.setOnDismissListener(
|
||||
dialogInterface -> languagesAdapter.getFilter().filter(""));
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
if (description.getSelectedLanguageIndex() == -1) {
|
||||
if (!TextUtils.isEmpty(savedLanguageValue)) {
|
||||
// If user has chosen a default language from settings activity
|
||||
// savedLanguageValue is not null
|
||||
if (!TextUtils.isEmpty(description.getLanguageCode())) {
|
||||
descriptionLanguages.setText(description.getLanguageCode());
|
||||
selectedLanguages.remove(position);
|
||||
selectedLanguages.put(position, description.getLanguageCode());
|
||||
} else {
|
||||
description.setLanguageCode(savedLanguageValue);
|
||||
descriptionLanguages.setText(savedLanguageValue);
|
||||
selectedLanguages.remove(position);
|
||||
selectedLanguages.put(position, savedLanguageValue);
|
||||
}
|
||||
} else if (!TextUtils.isEmpty(description.getLanguageCode())) {
|
||||
descriptionLanguages.setText(description.getLanguageCode());
|
||||
selectedLanguages.remove(position);
|
||||
selectedLanguages.put(position, description.getLanguageCode());
|
||||
} else {
|
||||
//Checking whether Language Code attribute is null or not.
|
||||
if (uploadMediaDetails.get(position).getLanguageCode() != null) {
|
||||
//If it is not null that means it is fetching details from the previous
|
||||
// upload (i.e. when user has pressed copy previous caption & description)
|
||||
//hence providing same language code for the current upload.
|
||||
descriptionLanguages.setText(uploadMediaDetails.get(position)
|
||||
.getLanguageCode());
|
||||
selectedLanguages.remove(position);
|
||||
selectedLanguages.put(position, uploadMediaDetails.get(position)
|
||||
.getLanguageCode());
|
||||
} else {
|
||||
if (position == 0) {
|
||||
final int defaultLocaleIndex = languagesAdapter
|
||||
.getIndexOfUserDefaultLocale(descriptionLanguages
|
||||
.getContext());
|
||||
descriptionLanguages
|
||||
.setText(languagesAdapter.getLanguageCode(defaultLocaleIndex));
|
||||
description.setLanguageCode(
|
||||
languagesAdapter.getLanguageCode(defaultLocaleIndex));
|
||||
selectedLanguages.remove(position);
|
||||
selectedLanguages.put(position,
|
||||
languagesAdapter.getLanguageCode(defaultLocaleIndex));
|
||||
} else {
|
||||
description.setLanguageCode(languagesAdapter.getLanguageCode(0));
|
||||
descriptionLanguages.setText(languagesAdapter.getLanguageCode(0));
|
||||
selectedLanguages.remove(position);
|
||||
selectedLanguages.put(position, languagesAdapter.getLanguageCode(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
descriptionLanguages.setText(description.getLanguageCode());
|
||||
selectedLanguages.remove(position);
|
||||
selectedLanguages.put(position, description.getLanguageCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles click event for recent language section
|
||||
*/
|
||||
private void onRecentLanguageClicked(final Dialog dialog, final AdapterView<?> adapterView,
|
||||
final int position, final UploadMediaDetail description) {
|
||||
description.setSelectedLanguageIndex(position);
|
||||
final String languageCode = ((RecentLanguagesAdapter) adapterView.getAdapter())
|
||||
.getLanguageCode(position);
|
||||
description.setLanguageCode(languageCode);
|
||||
final String languageName = ((RecentLanguagesAdapter) adapterView.getAdapter())
|
||||
.getLanguageName(position);
|
||||
final boolean isExists = recentLanguagesDao.findRecentLanguage(languageCode);
|
||||
if (isExists) {
|
||||
recentLanguagesDao.deleteRecentLanguage(languageCode);
|
||||
}
|
||||
recentLanguagesDao.addRecentLanguage(new Language(languageName, languageCode));
|
||||
|
||||
selectedLanguages.clear();
|
||||
selectedLanguages.put(position, languageCode);
|
||||
((RecentLanguagesAdapter) adapterView
|
||||
.getAdapter()).setSelectedLangCode(languageCode);
|
||||
Timber.d("Description language code is: %s", languageCode);
|
||||
if (descriptionLanguages!=null) {
|
||||
descriptionLanguages.setText(languageCode);
|
||||
}
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides recent languages section
|
||||
*/
|
||||
private void hideRecentLanguagesSection() {
|
||||
languageHistoryListView.setVisibility(View.GONE);
|
||||
recentLanguagesTextView.setVisibility(View.GONE);
|
||||
separator.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up recent languages section
|
||||
*
|
||||
* @param recentLanguages recently used languages
|
||||
*/
|
||||
private void setUpRecentLanguagesSection(final List<Language> recentLanguages) {
|
||||
if (recentLanguages.isEmpty()) {
|
||||
languageHistoryListView.setVisibility(View.GONE);
|
||||
recentLanguagesTextView.setVisibility(View.GONE);
|
||||
separator.setVisibility(View.GONE);
|
||||
} else {
|
||||
if (recentLanguages.size() > 5) {
|
||||
for (int i = recentLanguages.size() - 1; i >= 5; i--) {
|
||||
recentLanguagesDao.deleteRecentLanguage(recentLanguages.get(i)
|
||||
.getLanguageCode());
|
||||
}
|
||||
}
|
||||
languageHistoryListView.setVisibility(View.VISIBLE);
|
||||
recentLanguagesTextView.setVisibility(View.VISIBLE);
|
||||
separator.setVisibility(View.VISIBLE);
|
||||
|
||||
if (descriptionLanguages!=null) {
|
||||
final RecentLanguagesAdapter recentLanguagesAdapter
|
||||
= new RecentLanguagesAdapter(
|
||||
descriptionLanguages.getContext(),
|
||||
recentLanguagesDao.getRecentLanguages(),
|
||||
selectedLanguages);
|
||||
languageHistoryListView.setAdapter(recentLanguagesAdapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Ideographic space to Latin space
|
||||
*
|
||||
* @param source the source text
|
||||
* @return a string with Latin spaces instead of Ideographic spaces
|
||||
*/
|
||||
public String convertIdeographicSpaceToLatinSpace(String source) {
|
||||
Pattern ideographicSpacePattern = Pattern.compile("\\x{3000}");
|
||||
return ideographicSpacePattern.matcher(source).replaceAll(" ");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the visibility of the "Add" button for all items in the RecyclerView except
|
||||
* the last item in RecyclerView
|
||||
*/
|
||||
private void updateAddButtonVisibility() {
|
||||
int lastItemPosition = getItemCount() - 1;
|
||||
// Hide Add Button for all items
|
||||
for (int i = 0; i < getItemCount(); i++) {
|
||||
if (fragment != null) {
|
||||
if (fragment.getView() != null) {
|
||||
ViewHolder holder = (ViewHolder) ((RecyclerView) fragment.getView()
|
||||
.findViewById(R.id.rv_descriptions)).findViewHolderForAdapterPosition(i);
|
||||
if (holder != null) {
|
||||
holder.addButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.activity != null) {
|
||||
ViewHolder holder = (ViewHolder) ((RecyclerView) activity.findViewById(
|
||||
R.id.rv_descriptions_captions)).findViewHolderForAdapterPosition(i);
|
||||
if (holder != null) {
|
||||
holder.addButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show Add Button for the last item
|
||||
if (fragment != null) {
|
||||
if (fragment.getView() != null) {
|
||||
ViewHolder lastItemHolder = (ViewHolder) ((RecyclerView) fragment.getView()
|
||||
.findViewById(R.id.rv_descriptions)).findViewHolderForAdapterPosition(
|
||||
lastItemPosition);
|
||||
if (lastItemHolder != null) {
|
||||
lastItemHolder.addButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.activity != null) {
|
||||
ViewHolder lastItemHolder = (ViewHolder) ((RecyclerView) activity
|
||||
.findViewById(R.id.rv_descriptions_captions)).findViewHolderForAdapterPosition(
|
||||
lastItemPosition);
|
||||
if (lastItemHolder != null) {
|
||||
lastItemHolder.addButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
|
||||
void showAlert(int mediaDetailDescription, int descriptionInfo);
|
||||
}
|
||||
|
||||
public interface EventListener {
|
||||
|
||||
void onPrimaryCaptionTextChange(boolean isNotEmpty);
|
||||
|
||||
void addLanguage();
|
||||
}
|
||||
|
||||
enum SelectedVoiceIcon {
|
||||
CAPTION,
|
||||
DESCRIPTION
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,563 @@
|
|||
package fr.free.nrw.commons.upload
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.speech.RecognizerIntent.ACTION_RECOGNIZE_SPEECH
|
||||
import android.speech.RecognizerIntent.EXTRA_LANGUAGE
|
||||
import android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL
|
||||
import android.speech.RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
|
||||
import android.text.Editable
|
||||
import android.text.InputFilter
|
||||
import android.text.TextUtils
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.databinding.RowItemDescriptionBinding
|
||||
import fr.free.nrw.commons.recentlanguages.Language
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
|
||||
import fr.free.nrw.commons.utils.AbstractTextWatcher
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class UploadMediaDetailAdapter : RecyclerView.Adapter<UploadMediaDetailAdapter.ViewHolder> {
|
||||
private var uploadMediaDetails: MutableList<UploadMediaDetail>
|
||||
private var selectedLanguages: MutableMap<Int, String>
|
||||
private val savedLanguageValue: String
|
||||
private var recentLanguagesTextView: TextView? = null
|
||||
private var separator: View? = null
|
||||
private var languageHistoryListView: ListView? = null
|
||||
private var currentPosition = 0
|
||||
private var fragment: Fragment? = null
|
||||
private var activity: Activity? = null
|
||||
private val voiceInputResultLauncher: ActivityResultLauncher<Intent>
|
||||
private var selectedVoiceIcon: SelectedVoiceIcon? = null
|
||||
var recentLanguagesDao: RecentLanguagesDao
|
||||
var callback: Callback? = null
|
||||
var eventListener: EventListener? = null
|
||||
var items: List<UploadMediaDetail>
|
||||
get() = uploadMediaDetails
|
||||
set(value) {
|
||||
uploadMediaDetails = value.toMutableList()
|
||||
selectedLanguages = mutableMapOf()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
||||
constructor(
|
||||
fragment: Fragment?,
|
||||
savedLanguageValue: String,
|
||||
recentLanguagesDao: RecentLanguagesDao,
|
||||
voiceInputResultLauncher: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
uploadMediaDetails = ArrayList()
|
||||
selectedLanguages = mutableMapOf()
|
||||
this.savedLanguageValue = savedLanguageValue
|
||||
this.recentLanguagesDao = recentLanguagesDao
|
||||
this.fragment = fragment
|
||||
this.voiceInputResultLauncher = voiceInputResultLauncher
|
||||
}
|
||||
|
||||
constructor(
|
||||
activity: Activity?,
|
||||
savedLanguageValue: String,
|
||||
uploadMediaDetails: MutableList<UploadMediaDetail>,
|
||||
recentLanguagesDao: RecentLanguagesDao,
|
||||
voiceInputResultLauncher: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
this.uploadMediaDetails = uploadMediaDetails
|
||||
selectedLanguages = HashMap()
|
||||
this.savedLanguageValue = savedLanguageValue
|
||||
this.recentLanguagesDao = recentLanguagesDao
|
||||
this.activity = activity
|
||||
this.voiceInputResultLauncher = voiceInputResultLauncher
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return ViewHolder(RowItemDescriptionBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a workaround for a known bug by android here
|
||||
* https://issuetracker.google.com/issues/37095917 makes the edit text on second and subsequent
|
||||
* fragments inside an adapter receptive to long click for copy/paste options
|
||||
*
|
||||
* @param holder the view holder
|
||||
*/
|
||||
override fun onViewAttachedToWindow(holder: ViewHolder) {
|
||||
super.onViewAttachedToWindow(holder)
|
||||
holder.binding.captionItemEditText.isEnabled = false
|
||||
holder.binding.captionItemEditText.isEnabled = true
|
||||
holder.binding.descriptionItemEditText.isEnabled = false
|
||||
holder.binding.descriptionItemEditText.isEnabled = true
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(position)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return uploadMediaDetails.size
|
||||
}
|
||||
|
||||
fun addDescription(uploadMediaDetail: UploadMediaDetail) {
|
||||
selectedLanguages[uploadMediaDetails.size] = "en"
|
||||
uploadMediaDetails.add(uploadMediaDetail)
|
||||
notifyItemInserted(uploadMediaDetails.size)
|
||||
}
|
||||
|
||||
private fun startSpeechInput(locale: String) {
|
||||
try {
|
||||
voiceInputResultLauncher.launch(Intent(ACTION_RECOGNIZE_SPEECH).apply {
|
||||
putExtra(EXTRA_LANGUAGE_MODEL, LANGUAGE_MODEL_FREE_FORM)
|
||||
putExtra(EXTRA_LANGUAGE, locale)
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the result of the speech input by processing the spoken text.
|
||||
* If the spoken text is not empty, it capitalizes the first letter of the spoken text
|
||||
* and updates the appropriate field (caption or description) of the current
|
||||
* UploadMediaDetail based on the selected voice icon.
|
||||
* Finally, it notifies the adapter that the data set has changed.
|
||||
*
|
||||
* @param spokenText the text input received from speech recognition.
|
||||
*/
|
||||
fun handleSpeechResult(spokenText: String) {
|
||||
if (spokenText.isNotEmpty()) {
|
||||
val spokenTextCapitalized =
|
||||
spokenText.substring(0, 1).uppercase(Locale.getDefault()) + spokenText.substring(1)
|
||||
if (currentPosition < uploadMediaDetails.size) {
|
||||
val uploadMediaDetail = uploadMediaDetails[currentPosition]
|
||||
when (selectedVoiceIcon) {
|
||||
SelectedVoiceIcon.CAPTION -> uploadMediaDetail.captionText =
|
||||
spokenTextCapitalized
|
||||
|
||||
SelectedVoiceIcon.DESCRIPTION -> uploadMediaDetail.descriptionText =
|
||||
spokenTextCapitalized
|
||||
|
||||
null -> {}
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove description based on position from the list and notifies the RecyclerView Adapter that
|
||||
* data in adapter has been removed at that particular position.
|
||||
*/
|
||||
fun removeDescription(uploadMediaDetail: UploadMediaDetail, position: Int) {
|
||||
selectedLanguages.remove(position)
|
||||
uploadMediaDetails.remove(uploadMediaDetail)
|
||||
var i = position + 1
|
||||
while (selectedLanguages.containsKey(i)) {
|
||||
selectedLanguages.remove(i)
|
||||
i++
|
||||
}
|
||||
notifyItemRemoved(position)
|
||||
notifyItemRangeChanged(position, uploadMediaDetails.size - position)
|
||||
updateAddButtonVisibility()
|
||||
}
|
||||
|
||||
inner class ViewHolder(val binding: RowItemDescriptionBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
var addButton: ImageView? = null
|
||||
|
||||
var clParent: ConstraintLayout? = null
|
||||
|
||||
var betterCaptionLinearLayout: LinearLayout? = null
|
||||
|
||||
var betterDescriptionLinearLayout: LinearLayout? = null
|
||||
|
||||
private var captionListener: AbstractTextWatcher? = null
|
||||
|
||||
var descriptionListener: AbstractTextWatcher? = null
|
||||
|
||||
fun bind(position: Int) {
|
||||
val uploadMediaDetail = uploadMediaDetails[position]
|
||||
Timber.d("UploadMediaDetail is %s", uploadMediaDetail)
|
||||
|
||||
addButton = binding.btnAdd
|
||||
clParent = binding.clParent
|
||||
betterCaptionLinearLayout = binding.llWriteBetterCaption
|
||||
betterDescriptionLinearLayout = binding.llWriteBetterDescription
|
||||
|
||||
|
||||
binding.descriptionLanguages.isFocusable = false
|
||||
binding.captionItemEditText.addTextChangedListener(AbstractTextWatcher { value: String ->
|
||||
if (position == 0) {
|
||||
eventListener!!.onPrimaryCaptionTextChange(value.length != 0)
|
||||
}
|
||||
})
|
||||
binding.captionItemEditText.removeTextChangedListener(captionListener)
|
||||
binding.descriptionItemEditText.removeTextChangedListener(descriptionListener)
|
||||
binding.captionItemEditText.setText(uploadMediaDetail.captionText)
|
||||
binding.descriptionItemEditText.setText(uploadMediaDetail.descriptionText)
|
||||
binding.captionItemEditTextInputLayout.endIconMode = TextInputLayout.END_ICON_CUSTOM
|
||||
binding.captionItemEditTextInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice)
|
||||
binding.captionItemEditTextInputLayout.setEndIconOnClickListener { v: View? ->
|
||||
currentPosition = position
|
||||
selectedVoiceIcon = SelectedVoiceIcon.CAPTION
|
||||
startSpeechInput(binding.descriptionLanguages.text.toString())
|
||||
}
|
||||
binding.descriptionItemEditTextInputLayout.endIconMode = TextInputLayout.END_ICON_CUSTOM
|
||||
binding.descriptionItemEditTextInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice)
|
||||
binding.descriptionItemEditTextInputLayout.setEndIconOnClickListener { v: View? ->
|
||||
currentPosition = position
|
||||
selectedVoiceIcon = SelectedVoiceIcon.DESCRIPTION
|
||||
startSpeechInput(binding.descriptionLanguages.text.toString())
|
||||
}
|
||||
|
||||
if (position == 0) {
|
||||
binding.btnRemove.visibility = View.GONE
|
||||
betterCaptionLinearLayout!!.visibility = View.VISIBLE
|
||||
betterCaptionLinearLayout!!.setOnClickListener { v: View? ->
|
||||
callback!!.showAlert(
|
||||
R.string.media_detail_caption,
|
||||
R.string.caption_info
|
||||
)
|
||||
}
|
||||
betterDescriptionLinearLayout!!.visibility = View.VISIBLE
|
||||
betterDescriptionLinearLayout!!.setOnClickListener { v: View? ->
|
||||
callback!!.showAlert(
|
||||
R.string.media_detail_description,
|
||||
R.string.description_info
|
||||
)
|
||||
}
|
||||
|
||||
binding.captionItemEditTextInputLayout.editText?.let {
|
||||
it.filters = arrayOf<InputFilter>(UploadMediaDetailInputFilter())
|
||||
}
|
||||
} else {
|
||||
binding.btnRemove.visibility = View.VISIBLE
|
||||
betterCaptionLinearLayout!!.visibility = View.GONE
|
||||
betterDescriptionLinearLayout!!.visibility = View.GONE
|
||||
}
|
||||
|
||||
binding.btnRemove.setOnClickListener { v: View? ->
|
||||
removeDescription(
|
||||
uploadMediaDetail,
|
||||
position
|
||||
)
|
||||
}
|
||||
captionListener = AbstractTextWatcher { captionText: String ->
|
||||
uploadMediaDetail.captionText =
|
||||
convertIdeographicSpaceToLatinSpace(captionText.trim())
|
||||
}
|
||||
descriptionListener = AbstractTextWatcher { value: String? ->
|
||||
uploadMediaDetail.descriptionText = value
|
||||
}
|
||||
binding.captionItemEditText.addTextChangedListener(captionListener)
|
||||
initLanguage(position, uploadMediaDetail)
|
||||
|
||||
binding.descriptionItemEditText.addTextChangedListener(descriptionListener)
|
||||
initLanguage(position, uploadMediaDetail)
|
||||
|
||||
if (fragment != null) {
|
||||
val newLayoutParams = clParent!!.layoutParams as FrameLayout.LayoutParams
|
||||
newLayoutParams.topMargin = 0
|
||||
newLayoutParams.leftMargin = 0
|
||||
newLayoutParams.rightMargin = 0
|
||||
newLayoutParams.bottomMargin = 0
|
||||
clParent!!.layoutParams = newLayoutParams
|
||||
}
|
||||
updateAddButtonVisibility()
|
||||
addButton!!.setOnClickListener { v: View? -> eventListener!!.addLanguage() }
|
||||
|
||||
//If the description was manually added by the user, it deserves focus, if not, let the user decide
|
||||
if (uploadMediaDetail.isManuallyAdded) {
|
||||
binding.captionItemEditText.requestFocus()
|
||||
} else {
|
||||
binding.captionItemEditText.clearFocus()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initLanguage(position: Int, description: UploadMediaDetail) {
|
||||
val recentLanguages = recentLanguagesDao.getRecentLanguages()
|
||||
|
||||
val languagesAdapter = LanguagesAdapter(
|
||||
binding.descriptionLanguages.context,
|
||||
selectedLanguages
|
||||
)
|
||||
|
||||
binding.descriptionLanguages.setOnClickListener { view ->
|
||||
val dialog = Dialog(view.context)
|
||||
dialog.setContentView(R.layout.dialog_select_language)
|
||||
dialog.setCancelable(false)
|
||||
dialog.window!!.setLayout(
|
||||
(view.context.resources.displayMetrics.widthPixels
|
||||
* 0.90).toInt(),
|
||||
(view.context.resources.displayMetrics.heightPixels
|
||||
* 0.90).toInt()
|
||||
)
|
||||
dialog.show()
|
||||
|
||||
val editText =
|
||||
dialog.findViewById<EditText>(R.id.search_language)
|
||||
val listView =
|
||||
dialog.findViewById<ListView>(R.id.language_list)
|
||||
languageHistoryListView =
|
||||
dialog.findViewById(R.id.language_history_list)
|
||||
recentLanguagesTextView =
|
||||
dialog.findViewById(R.id.recent_searches)
|
||||
separator =
|
||||
dialog.findViewById(R.id.separator)
|
||||
setUpRecentLanguagesSection(recentLanguages)
|
||||
|
||||
listView.adapter = languagesAdapter
|
||||
|
||||
editText.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) =
|
||||
hideRecentLanguagesSection()
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
|
||||
languagesAdapter.filter.filter(charSequence)
|
||||
}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) = Unit
|
||||
})
|
||||
|
||||
languageHistoryListView?.setOnItemClickListener { adapterView: AdapterView<*>, view1: View?, position: Int, id: Long ->
|
||||
onRecentLanguageClicked(dialog, adapterView, position, description)
|
||||
}
|
||||
|
||||
listView.onItemClickListener = OnItemClickListener { adapterView, _, i, l ->
|
||||
description.selectedLanguageIndex = i
|
||||
val languageCode = (adapterView.adapter as LanguagesAdapter).getLanguageCode(i)
|
||||
description.languageCode = languageCode
|
||||
val languageName = (adapterView.adapter as LanguagesAdapter).getLanguageName(i)
|
||||
val isExists = recentLanguagesDao.findRecentLanguage(languageCode)
|
||||
if (isExists) {
|
||||
recentLanguagesDao.deleteRecentLanguage(languageCode)
|
||||
}
|
||||
recentLanguagesDao.addRecentLanguage(Language(languageName, languageCode))
|
||||
|
||||
selectedLanguages.clear()
|
||||
selectedLanguages[position] = languageCode
|
||||
(adapterView.adapter as LanguagesAdapter).selectedLangCode = languageCode
|
||||
Timber.d("Description language code is: %s", languageCode)
|
||||
binding.descriptionLanguages.text = languageCode
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
languagesAdapter.filter.filter("")
|
||||
}
|
||||
}
|
||||
|
||||
if (description.selectedLanguageIndex == -1) {
|
||||
if (!TextUtils.isEmpty(savedLanguageValue)) {
|
||||
// If user has chosen a default language from settings activity
|
||||
// savedLanguageValue is not null
|
||||
if (!TextUtils.isEmpty(description.languageCode)) {
|
||||
binding.descriptionLanguages.text = description.languageCode
|
||||
selectedLanguages.remove(position)
|
||||
selectedLanguages[position] = description.languageCode!!
|
||||
} else {
|
||||
description.languageCode = savedLanguageValue
|
||||
binding.descriptionLanguages.text = savedLanguageValue
|
||||
selectedLanguages.remove(position)
|
||||
selectedLanguages[position] = savedLanguageValue
|
||||
}
|
||||
} else if (!TextUtils.isEmpty(description.languageCode)) {
|
||||
binding.descriptionLanguages.text = description.languageCode
|
||||
selectedLanguages.remove(position)
|
||||
selectedLanguages[position] = description.languageCode!!
|
||||
} else {
|
||||
//Checking whether Language Code attribute is null or not.
|
||||
if (uploadMediaDetails[position].languageCode != null) {
|
||||
//If it is not null that means it is fetching details from the previous
|
||||
// upload (i.e. when user has pressed copy previous caption & description)
|
||||
//hence providing same language code for the current upload.
|
||||
binding.descriptionLanguages.text = uploadMediaDetails[position]
|
||||
.languageCode
|
||||
selectedLanguages.remove(position)
|
||||
selectedLanguages[position] = uploadMediaDetails[position].languageCode!!
|
||||
} else {
|
||||
if (position == 0) {
|
||||
val defaultLocaleIndex = languagesAdapter.getIndexOfUserDefaultLocale(
|
||||
binding.descriptionLanguages.getContext())
|
||||
binding.descriptionLanguages.setText(languagesAdapter.getLanguageCode(defaultLocaleIndex))
|
||||
description.languageCode = languagesAdapter.getLanguageCode(defaultLocaleIndex)
|
||||
selectedLanguages.remove(position)
|
||||
selectedLanguages[position] =
|
||||
languagesAdapter.getLanguageCode(defaultLocaleIndex)
|
||||
} else {
|
||||
description.languageCode = languagesAdapter.getLanguageCode(0)
|
||||
binding.descriptionLanguages.text = languagesAdapter.getLanguageCode(0)
|
||||
selectedLanguages.remove(position)
|
||||
selectedLanguages[position] = languagesAdapter.getLanguageCode(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.descriptionLanguages.text = description.languageCode
|
||||
selectedLanguages.remove(position)
|
||||
description.languageCode?.let {
|
||||
selectedLanguages[position] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles click event for recent language section
|
||||
*/
|
||||
private fun onRecentLanguageClicked(
|
||||
dialog: Dialog, adapterView: AdapterView<*>,
|
||||
position: Int, description: UploadMediaDetail
|
||||
) {
|
||||
description.selectedLanguageIndex = position
|
||||
val languageCode = (adapterView.adapter as RecentLanguagesAdapter)
|
||||
.getLanguageCode(position)
|
||||
description.languageCode = languageCode
|
||||
val languageName = (adapterView.adapter as RecentLanguagesAdapter)
|
||||
.getLanguageName(position)
|
||||
val isExists = recentLanguagesDao.findRecentLanguage(languageCode)
|
||||
if (isExists) {
|
||||
recentLanguagesDao.deleteRecentLanguage(languageCode)
|
||||
}
|
||||
recentLanguagesDao.addRecentLanguage(Language(languageName, languageCode))
|
||||
|
||||
selectedLanguages.clear()
|
||||
selectedLanguages[position] = languageCode
|
||||
(adapterView
|
||||
.adapter as RecentLanguagesAdapter).selectedLangCode = languageCode
|
||||
Timber.d("Description language code is: %s", languageCode)
|
||||
binding.descriptionLanguages.text = languageCode
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides recent languages section
|
||||
*/
|
||||
private fun hideRecentLanguagesSection() {
|
||||
languageHistoryListView!!.visibility = View.GONE
|
||||
recentLanguagesTextView!!.visibility = View.GONE
|
||||
separator!!.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up recent languages section
|
||||
*
|
||||
* @param recentLanguages recently used languages
|
||||
*/
|
||||
private fun setUpRecentLanguagesSection(recentLanguages: List<Language>) {
|
||||
if (recentLanguages.isEmpty()) {
|
||||
languageHistoryListView!!.visibility = View.GONE
|
||||
recentLanguagesTextView!!.visibility = View.GONE
|
||||
separator!!.visibility = View.GONE
|
||||
} else {
|
||||
if (recentLanguages.size > 5) {
|
||||
for (i in recentLanguages.size - 1 downTo 5) {
|
||||
recentLanguagesDao.deleteRecentLanguage(
|
||||
recentLanguages[i]
|
||||
.languageCode
|
||||
)
|
||||
}
|
||||
}
|
||||
languageHistoryListView!!.visibility = View.VISIBLE
|
||||
recentLanguagesTextView!!.visibility = View.VISIBLE
|
||||
separator!!.visibility = View.VISIBLE
|
||||
|
||||
val recentLanguagesAdapter = RecentLanguagesAdapter(
|
||||
binding.descriptionLanguages.context,
|
||||
recentLanguagesDao.getRecentLanguages(),
|
||||
selectedLanguages
|
||||
)
|
||||
languageHistoryListView!!.adapter = recentLanguagesAdapter
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Ideographic space to Latin space
|
||||
*
|
||||
* @param source the source text
|
||||
* @return a string with Latin spaces instead of Ideographic spaces
|
||||
*/
|
||||
fun convertIdeographicSpaceToLatinSpace(source: String): String {
|
||||
val ideographicSpacePattern = Pattern.compile("\\x{3000}")
|
||||
return ideographicSpacePattern.matcher(source).replaceAll(" ")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the visibility of the "Add" button for all items in the RecyclerView except
|
||||
* the last item in RecyclerView
|
||||
*/
|
||||
private fun updateAddButtonVisibility() {
|
||||
val lastItemPosition = itemCount - 1
|
||||
// Hide Add Button for all items
|
||||
for (i in 0 until itemCount) {
|
||||
if (fragment != null) {
|
||||
if (fragment!!.view != null) {
|
||||
val holder = (fragment!!.requireView().findViewById<View>(R.id.rv_descriptions) as RecyclerView).findViewHolderForAdapterPosition(i) as ViewHolder?
|
||||
if (holder != null) {
|
||||
holder.addButton!!.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (activity != null) {
|
||||
val holder = (activity!!.findViewById<View>(R.id.rv_descriptions_captions) as RecyclerView).findViewHolderForAdapterPosition(i) as ViewHolder?
|
||||
if (holder != null) {
|
||||
holder.addButton!!.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show Add Button for the last item
|
||||
if (fragment != null) {
|
||||
if (fragment!!.view != null) {
|
||||
val lastItemHolder = (fragment!!.requireView().findViewById<View>(R.id.rv_descriptions) as RecyclerView).findViewHolderForAdapterPosition(lastItemPosition) as ViewHolder?
|
||||
if (lastItemHolder != null) {
|
||||
lastItemHolder.addButton!!.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (activity != null) {
|
||||
val lastItemHolder = (activity!!.findViewById<View>(R.id.rv_descriptions_captions) as RecyclerView).findViewHolderForAdapterPosition(lastItemPosition) as ViewHolder?
|
||||
if (lastItemHolder != null) {
|
||||
lastItemHolder.addButton!!.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun interface Callback {
|
||||
fun showAlert(mediaDetailDescription: Int, descriptionInfo: Int)
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
fun onPrimaryCaptionTextChange(isNotEmpty: Boolean)
|
||||
fun addLanguage()
|
||||
}
|
||||
|
||||
internal enum class SelectedVoiceIcon {
|
||||
CAPTION,
|
||||
DESCRIPTION
|
||||
}
|
||||
}
|
||||
|
|
@ -1,297 +0,0 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import timber.log.Timber;
|
||||
|
||||
@Singleton
|
||||
public class UploadModel {
|
||||
|
||||
private final JsonKvStore store;
|
||||
private final List<String> licenses;
|
||||
private final Context context;
|
||||
private String license;
|
||||
private final Map<String, String> licensesByName;
|
||||
private final List<UploadItem> items = new ArrayList<>();
|
||||
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
private final SessionManager sessionManager;
|
||||
private final FileProcessor fileProcessor;
|
||||
private final ImageProcessingService imageProcessingService;
|
||||
private final List<String> selectedCategories = new ArrayList<>();
|
||||
private final List<DepictedItem> selectedDepictions = new ArrayList<>();
|
||||
/**
|
||||
* Existing depicts which are selected
|
||||
*/
|
||||
private List<String> selectedExistingDepictions = new ArrayList<>();
|
||||
|
||||
@Inject
|
||||
UploadModel(@Named("licenses") final List<String> licenses,
|
||||
@Named("default_preferences") final JsonKvStore store,
|
||||
@Named("licenses_by_name") final Map<String, String> licensesByName,
|
||||
final Context context,
|
||||
final SessionManager sessionManager,
|
||||
final FileProcessor fileProcessor,
|
||||
final ImageProcessingService imageProcessingService) {
|
||||
this.licenses = licenses;
|
||||
this.store = store;
|
||||
this.license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
|
||||
this.licensesByName = licensesByName;
|
||||
this.context = context;
|
||||
this.sessionManager = sessionManager;
|
||||
this.fileProcessor = fileProcessor;
|
||||
this.imageProcessingService = imageProcessingService;
|
||||
}
|
||||
|
||||
/**
|
||||
* cleanup the resources, I am Singleton, preparing for fresh upload
|
||||
*/
|
||||
public void cleanUp() {
|
||||
compositeDisposable.clear();
|
||||
fileProcessor.cleanup();
|
||||
items.clear();
|
||||
selectedCategories.clear();
|
||||
selectedDepictions.clear();
|
||||
selectedExistingDepictions.clear();
|
||||
}
|
||||
|
||||
public void setSelectedCategories(List<String> selectedCategories) {
|
||||
this.selectedCategories.clear();
|
||||
this.selectedCategories.addAll(selectedCategories);
|
||||
}
|
||||
|
||||
/**
|
||||
* pre process a one item at a time
|
||||
*/
|
||||
public Observable<UploadItem> preProcessImage(final UploadableFile uploadableFile,
|
||||
final Place place,
|
||||
final SimilarImageInterface similarImageInterface,
|
||||
LatLng inAppPictureLocation) {
|
||||
return Observable.just(
|
||||
createAndAddUploadItem(uploadableFile, place, similarImageInterface, inAppPictureLocation));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls validateImage() of ImageProcessingService to check quality of image
|
||||
*
|
||||
* @param uploadItem UploadItem whose quality is to be checked
|
||||
* @param inAppPictureLocation In app picture location (if any)
|
||||
* @return Quality of UploadItem
|
||||
*/
|
||||
public Single<Integer> getImageQuality(final UploadItem uploadItem, LatLng inAppPictureLocation) {
|
||||
return imageProcessingService.validateImage(uploadItem, inAppPictureLocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls checkDuplicateImage() of ImageProcessingService to check if image is duplicate
|
||||
*
|
||||
* @param filePath file to be checked
|
||||
* @return IMAGE_DUPLICATE or IMAGE_OK
|
||||
*/
|
||||
public Single<Integer> checkDuplicateImage(String filePath){
|
||||
return imageProcessingService.checkDuplicateImage(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls validateCaption() of ImageProcessingService to check caption of image
|
||||
*
|
||||
* @param uploadItem UploadItem whose caption is to be checked
|
||||
* @return Quality of caption of the UploadItem
|
||||
*/
|
||||
public Single<Integer> getCaptionQuality(final UploadItem uploadItem) {
|
||||
return imageProcessingService.validateCaption(uploadItem);
|
||||
}
|
||||
|
||||
private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile,
|
||||
final Place place,
|
||||
final SimilarImageInterface similarImageInterface,
|
||||
LatLng inAppPictureLocation) {
|
||||
final UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile
|
||||
.getFileCreatedDate(context);
|
||||
long fileCreatedDate = -1;
|
||||
String createdTimestampSource = "";
|
||||
String fileCreatedDateString = "";
|
||||
if (dateTimeWithSource != null) {
|
||||
fileCreatedDate = dateTimeWithSource.getEpochDate();
|
||||
fileCreatedDateString = dateTimeWithSource.getDateString();
|
||||
createdTimestampSource = dateTimeWithSource.getSource();
|
||||
}
|
||||
Timber.d("File created date is %d", fileCreatedDate);
|
||||
final ImageCoordinates imageCoordinates = fileProcessor
|
||||
.processFileCoordinates(similarImageInterface, uploadableFile.getFilePath(),
|
||||
inAppPictureLocation);
|
||||
final UploadItem uploadItem = new UploadItem(
|
||||
Uri.parse(uploadableFile.getFilePath()),
|
||||
uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate,
|
||||
createdTimestampSource,
|
||||
uploadableFile.getContentUri(),
|
||||
fileCreatedDateString);
|
||||
|
||||
// If an uploadItem of the same uploadableFile has been created before, we return that.
|
||||
// This is to avoid multiple instances of uploadItem of same file passed around.
|
||||
if (items.contains(uploadItem)) {
|
||||
return items.get(items.indexOf(uploadItem));
|
||||
}
|
||||
|
||||
if (place != null) {
|
||||
uploadItem.getUploadMediaDetails().set(0, new UploadMediaDetail(place));
|
||||
}
|
||||
if (!items.contains(uploadItem)) {
|
||||
items.add(uploadItem);
|
||||
}
|
||||
return uploadItem;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
public List<UploadItem> getUploads() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public List<String> getLicenses() {
|
||||
return licenses;
|
||||
}
|
||||
|
||||
public String getSelectedLicense() {
|
||||
return license;
|
||||
}
|
||||
|
||||
public void setSelectedLicense(final String licenseName) {
|
||||
this.license = licensesByName.get(licenseName);
|
||||
store.putString(Prefs.DEFAULT_LICENSE, license);
|
||||
}
|
||||
|
||||
public Observable<Contribution> buildContributions() {
|
||||
return Observable.fromIterable(items).map(item ->
|
||||
{
|
||||
String imageSHA1 = FileUtils.INSTANCE.getSHA1(context.getContentResolver().openInputStream(item.getContentUri()));
|
||||
|
||||
final Contribution contribution = new Contribution(
|
||||
item, sessionManager, newListOf(selectedDepictions), newListOf(selectedCategories), imageSHA1);
|
||||
|
||||
contribution.setHasInvalidLocation(item.hasInvalidLocation());
|
||||
|
||||
Timber.d("Created timestamp while building contribution is %s, %s",
|
||||
item.getCreatedTimestamp(),
|
||||
new Date(item.getCreatedTimestamp()));
|
||||
|
||||
if (item.getCreatedTimestamp() != -1L) {
|
||||
contribution.setDateCreated(new Date(item.getCreatedTimestamp()));
|
||||
contribution.setDateCreatedSource(item.getCreatedTimestampSource());
|
||||
//Set the date only if you have it, else the upload service is gonna try it the other way
|
||||
}
|
||||
|
||||
if (contribution.getWikidataPlace() != null) {
|
||||
if (item.isWLMUpload()) {
|
||||
contribution.getWikidataPlace().setMonumentUpload(true);
|
||||
} else {
|
||||
contribution.getWikidataPlace().setMonumentUpload(false);
|
||||
}
|
||||
}
|
||||
contribution.setCountryCode(item.getCountryCode());
|
||||
return contribution;
|
||||
});
|
||||
}
|
||||
|
||||
public void deletePicture(final String filePath) {
|
||||
final Iterator<UploadItem> iterator = items.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().getMediaUri().toString().contains(filePath)) {
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (items.isEmpty()) {
|
||||
cleanUp();
|
||||
}
|
||||
}
|
||||
|
||||
public List<UploadItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void onDepictItemClicked(DepictedItem depictedItem, Media media) {
|
||||
if (media == null) {
|
||||
if (depictedItem.isSelected()) {
|
||||
selectedDepictions.add(depictedItem);
|
||||
} else {
|
||||
selectedDepictions.remove(depictedItem);
|
||||
}
|
||||
} else {
|
||||
if (depictedItem.isSelected()) {
|
||||
if (media.getDepictionIds().contains(depictedItem.getId())) {
|
||||
selectedExistingDepictions.add(depictedItem.getId());
|
||||
} else {
|
||||
selectedDepictions.add(depictedItem);
|
||||
}
|
||||
} else {
|
||||
if (media.getDepictionIds().contains(depictedItem.getId())) {
|
||||
selectedExistingDepictions.remove(depictedItem.getId());
|
||||
if (!media.getDepictionIds().contains(depictedItem.getId())) {
|
||||
final List<String> depictsList = new ArrayList<>();
|
||||
depictsList.add(depictedItem.getId());
|
||||
depictsList.addAll(media.getDepictionIds());
|
||||
media.setDepictionIds(depictsList);
|
||||
}
|
||||
} else {
|
||||
selectedDepictions.remove(depictedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private <T> List<T> newListOf(final List<T> items) {
|
||||
return items != null ? new ArrayList<>(items) : new ArrayList<>();
|
||||
}
|
||||
|
||||
public void useSimilarPictureCoordinates(final ImageCoordinates imageCoordinates, final int uploadItemIndex) {
|
||||
fileProcessor.prePopulateCategoriesAndDepictionsBy(imageCoordinates);
|
||||
items.get(uploadItemIndex).setGpsCoords(imageCoordinates);
|
||||
}
|
||||
|
||||
public List<DepictedItem> getSelectedDepictions() {
|
||||
return selectedDepictions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides selected existing depicts
|
||||
*
|
||||
* @return selected existing depicts
|
||||
*/
|
||||
public List<String> getSelectedExistingDepictions() {
|
||||
return selectedExistingDepictions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize existing depicts
|
||||
*
|
||||
* @param selectedExistingDepictions existing depicts
|
||||
*/
|
||||
public void setSelectedExistingDepictions(final List<String> selectedExistingDepictions) {
|
||||
this.selectedExistingDepictions = selectedExistingDepictions;
|
||||
}
|
||||
}
|
||||
242
app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt
Normal file
242
app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
package fr.free.nrw.commons.upload
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.contributions.Contribution
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
import fr.free.nrw.commons.nearby.Place
|
||||
import fr.free.nrw.commons.settings.Prefs
|
||||
import fr.free.nrw.commons.upload.FileUtils.getSHA1
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import timber.log.Timber
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class UploadModel @Inject internal constructor(
|
||||
@param:Named("licenses") val licenses: List<String>,
|
||||
@param:Named("default_preferences") val store: JsonKvStore,
|
||||
@param:Named("licenses_by_name") val licensesByName: Map<String, String>,
|
||||
val context: Context,
|
||||
val sessionManager: SessionManager,
|
||||
val fileProcessor: FileProcessor,
|
||||
val imageProcessingService: ImageProcessingService
|
||||
) {
|
||||
var license: String? = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3)
|
||||
val items: MutableList<UploadItem> = mutableListOf()
|
||||
val compositeDisposable: CompositeDisposable = CompositeDisposable()
|
||||
val selectedCategories: MutableList<String> = mutableListOf()
|
||||
val selectedDepictions: MutableList<DepictedItem> = mutableListOf()
|
||||
|
||||
/**
|
||||
* Existing depicts which are selected
|
||||
*/
|
||||
var selectedExistingDepictions: MutableList<String> = mutableListOf()
|
||||
val count: Int
|
||||
get() = items.size
|
||||
|
||||
val uploads: List<UploadItem>
|
||||
get() = items
|
||||
|
||||
var selectedLicense: String?
|
||||
get() = license
|
||||
set(licenseName) {
|
||||
license = licensesByName[licenseName]
|
||||
if (license == null) {
|
||||
store.remove(Prefs.DEFAULT_LICENSE)
|
||||
} else {
|
||||
store.putString(Prefs.DEFAULT_LICENSE, license!!)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* cleanup the resources, I am Singleton, preparing for fresh upload
|
||||
*/
|
||||
fun cleanUp() {
|
||||
compositeDisposable.clear()
|
||||
fileProcessor.cleanup()
|
||||
items.clear()
|
||||
selectedCategories.clear()
|
||||
selectedDepictions.clear()
|
||||
selectedExistingDepictions.clear()
|
||||
}
|
||||
|
||||
fun setSelectedCategories(categories: List<String>) {
|
||||
selectedCategories.clear()
|
||||
selectedCategories.addAll(categories)
|
||||
}
|
||||
|
||||
/**
|
||||
* pre process a one item at a time
|
||||
*/
|
||||
fun preProcessImage(
|
||||
uploadableFile: UploadableFile?,
|
||||
place: Place?,
|
||||
similarImageInterface: SimilarImageInterface?,
|
||||
inAppPictureLocation: LatLng?
|
||||
): Observable<UploadItem> = Observable.just(
|
||||
createAndAddUploadItem(uploadableFile, place, similarImageInterface, inAppPictureLocation)
|
||||
)
|
||||
|
||||
/**
|
||||
* Calls validateImage() of ImageProcessingService to check quality of image
|
||||
*
|
||||
* @param uploadItem UploadItem whose quality is to be checked
|
||||
* @param inAppPictureLocation In app picture location (if any)
|
||||
* @return Quality of UploadItem
|
||||
*/
|
||||
fun getImageQuality(uploadItem: UploadItem, inAppPictureLocation: LatLng?): Single<Int> =
|
||||
imageProcessingService.validateImage(uploadItem, inAppPictureLocation)
|
||||
|
||||
/**
|
||||
* Calls checkDuplicateImage() of ImageProcessingService to check if image is duplicate
|
||||
*
|
||||
* @param filePath file to be checked
|
||||
* @return IMAGE_DUPLICATE or IMAGE_OK
|
||||
*/
|
||||
fun checkDuplicateImage(filePath: String?): Single<Int> =
|
||||
imageProcessingService.checkDuplicateImage(filePath)
|
||||
|
||||
/**
|
||||
* Calls validateCaption() of ImageProcessingService to check caption of image
|
||||
*
|
||||
* @param uploadItem UploadItem whose caption is to be checked
|
||||
* @return Quality of caption of the UploadItem
|
||||
*/
|
||||
fun getCaptionQuality(uploadItem: UploadItem): Single<Int> =
|
||||
imageProcessingService.validateCaption(uploadItem)
|
||||
|
||||
private fun createAndAddUploadItem(
|
||||
uploadableFile: UploadableFile?,
|
||||
place: Place?,
|
||||
similarImageInterface: SimilarImageInterface?,
|
||||
inAppPictureLocation: LatLng?
|
||||
): UploadItem {
|
||||
val dateTimeWithSource = uploadableFile?.getFileCreatedDate(context)
|
||||
var fileCreatedDate: Long = -1
|
||||
var createdTimestampSource = ""
|
||||
var fileCreatedDateString: String? = ""
|
||||
if (dateTimeWithSource != null) {
|
||||
fileCreatedDate = dateTimeWithSource.epochDate
|
||||
fileCreatedDateString = dateTimeWithSource.dateString
|
||||
createdTimestampSource = dateTimeWithSource.source
|
||||
}
|
||||
Timber.d("File created date is %d", fileCreatedDate)
|
||||
val imageCoordinates = fileProcessor
|
||||
.processFileCoordinates(
|
||||
similarImageInterface, uploadableFile?.getFilePath(),
|
||||
inAppPictureLocation
|
||||
)
|
||||
val uploadItem = UploadItem(
|
||||
Uri.parse(uploadableFile?.getFilePath()),
|
||||
uploadableFile?.getMimeType(context), imageCoordinates, place, fileCreatedDate,
|
||||
createdTimestampSource,
|
||||
uploadableFile?.contentUri,
|
||||
fileCreatedDateString
|
||||
)
|
||||
|
||||
// If an uploadItem of the same uploadableFile has been created before, we return that.
|
||||
// This is to avoid multiple instances of uploadItem of same file passed around.
|
||||
if (items.contains(uploadItem)) {
|
||||
return items[items.indexOf(uploadItem)]
|
||||
}
|
||||
|
||||
uploadItem.uploadMediaDetails[0] = UploadMediaDetail(place)
|
||||
if (!items.contains(uploadItem)) {
|
||||
items.add(uploadItem)
|
||||
}
|
||||
return uploadItem
|
||||
}
|
||||
|
||||
fun buildContributions(): Observable<Contribution> {
|
||||
return Observable.fromIterable(items).map { item: UploadItem ->
|
||||
val imageSHA1 = getSHA1(
|
||||
context.contentResolver.openInputStream(item.contentUri!!)!!
|
||||
)
|
||||
val contribution = Contribution(
|
||||
item,
|
||||
sessionManager,
|
||||
buildList { addAll(selectedDepictions) },
|
||||
buildList { addAll(selectedCategories) },
|
||||
imageSHA1
|
||||
)
|
||||
|
||||
contribution.setHasInvalidLocation(item.hasInvalidLocation())
|
||||
|
||||
Timber.d(
|
||||
"Created timestamp while building contribution is %s, %s",
|
||||
item.createdTimestamp,
|
||||
item.createdTimestamp?.let { Date(it) }
|
||||
)
|
||||
|
||||
if (item.createdTimestamp != -1L) {
|
||||
contribution.dateCreated = item.createdTimestamp?.let { Date(it) }
|
||||
contribution.dateCreatedSource = item.createdTimestampSource
|
||||
//Set the date only if you have it, else the upload service is gonna try it the other way
|
||||
}
|
||||
|
||||
if (contribution.wikidataPlace != null) {
|
||||
contribution.wikidataPlace!!.isMonumentUpload = item.isWLMUpload
|
||||
}
|
||||
contribution.countryCode = item.countryCode
|
||||
contribution
|
||||
}
|
||||
}
|
||||
|
||||
fun deletePicture(filePath: String) {
|
||||
val iterator = items.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().mediaUri.toString().contains(filePath)) {
|
||||
iterator.remove()
|
||||
break
|
||||
}
|
||||
}
|
||||
if (items.isEmpty()) {
|
||||
cleanUp()
|
||||
}
|
||||
}
|
||||
|
||||
fun onDepictItemClicked(depictedItem: DepictedItem, media: Media?) {
|
||||
if (media == null) {
|
||||
if (depictedItem.isSelected) {
|
||||
selectedDepictions.add(depictedItem)
|
||||
} else {
|
||||
selectedDepictions.remove(depictedItem)
|
||||
}
|
||||
} else {
|
||||
if (depictedItem.isSelected) {
|
||||
if (media.depictionIds.contains(depictedItem.id)) {
|
||||
selectedExistingDepictions.add(depictedItem.id)
|
||||
} else {
|
||||
selectedDepictions.add(depictedItem)
|
||||
}
|
||||
} else {
|
||||
if (media.depictionIds.contains(depictedItem.id)) {
|
||||
selectedExistingDepictions.remove(depictedItem.id)
|
||||
if (!media.depictionIds.contains(depictedItem.id)) {
|
||||
media.depictionIds = mutableListOf<String>().apply {
|
||||
add(depictedItem.id)
|
||||
addAll(media.depictionIds)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectedDepictions.remove(depictedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates, uploadItemIndex: Int) {
|
||||
fileProcessor.prePopulateCategoriesAndDepictionsBy(imageCoordinates)
|
||||
items[uploadItemIndex].gpsCoords = imageCoordinates
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package fr.free.nrw.commons.upload
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.CommonsApplication.Companion.IS_LIMITED_CONNECTION_MODE_ENABLED
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.contributions.Contribution
|
||||
import fr.free.nrw.commons.kvstore.BasicKvStore
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.repository.UploadRepository
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract
|
||||
|
|
@ -34,6 +34,8 @@ class UploadPresenter @Inject internal constructor(
|
|||
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
lateinit var basicKvStoreFactory: (String) -> BasicKvStore
|
||||
|
||||
/**
|
||||
* Called by the submit button in [UploadActivity]
|
||||
*/
|
||||
|
|
@ -69,8 +71,7 @@ class UploadPresenter @Inject internal constructor(
|
|||
private fun processContributionsForSubmission() {
|
||||
if (view.isLoggedIn()) {
|
||||
view.showProgress(true)
|
||||
repository.buildContributions()
|
||||
?.observeOn(Schedulers.io())
|
||||
repository.buildContributions().observeOn(Schedulers.io())
|
||||
?.subscribe(object : Observer<Contribution> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
view.showProgress(false)
|
||||
|
|
@ -127,14 +128,20 @@ class UploadPresenter @Inject internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore) {
|
||||
basicKvStoreFactory = factory
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls checkImageQuality of UploadMediaPresenter to check image quality of next image
|
||||
*
|
||||
* @param uploadItemIndex Index of next image, whose quality is to be checked
|
||||
*/
|
||||
override fun checkImageQuality(uploadItemIndex: Int) {
|
||||
val uploadItem = repository.getUploadItem(uploadItemIndex)
|
||||
presenter.checkImageQuality(uploadItem, uploadItemIndex)
|
||||
repository.getUploadItem(uploadItemIndex)?.let {
|
||||
presenter.setupBasicKvStoreFactory(basicKvStoreFactory)
|
||||
presenter.checkImageQuality(it, uploadItemIndex)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deletePictureAtIndex(index: Int) {
|
||||
|
|
@ -156,8 +163,9 @@ class UploadPresenter @Inject internal constructor(
|
|||
view.onUploadMediaDeleted(index)
|
||||
if (index != uploadableFiles.size && index != 0) {
|
||||
// if the deleted image was not the last item to be uploaded, check quality of next
|
||||
val uploadItem = repository.getUploadItem(index)
|
||||
presenter.checkImageQuality(uploadItem, index)
|
||||
repository.getUploadItem(index)?.let {
|
||||
presenter.checkImageQuality(it, index)
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadableFiles.size < 2) {
|
||||
|
|
|
|||
|
|
@ -95,12 +95,10 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View {
|
|||
}
|
||||
if (media == null) {
|
||||
if (callback != null) {
|
||||
binding!!.tvTitle.text = getString(
|
||||
R.string.step_count, callback!!.getIndexInViewFlipper(
|
||||
this
|
||||
) + 1,
|
||||
callback!!.totalNumberOfSteps, getString(R.string.categories_activity_title)
|
||||
)
|
||||
binding!!.tvTitle.text = getString(R.string.step_count,
|
||||
callback!!.getIndexInViewFlipper(this) + 1,
|
||||
callback!!.totalNumberOfSteps,
|
||||
getString(R.string.categories_activity_title))
|
||||
}
|
||||
} else {
|
||||
binding!!.tvTitle.setText(R.string.edit_categories)
|
||||
|
|
@ -220,7 +218,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View {
|
|||
}
|
||||
|
||||
override fun goToNextScreen() {
|
||||
callback!!.onNextButtonClicked(callback!!.getIndexInViewFlipper(this))
|
||||
callback?.let { it.onNextButtonClicked(it.getIndexInViewFlipper(this)) }
|
||||
}
|
||||
|
||||
override fun showNoCategorySelected() {
|
||||
|
|
@ -322,7 +320,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View {
|
|||
mediaDetailFragment.onResume()
|
||||
goBackToPreviousScreen()
|
||||
} else {
|
||||
callback!!.onPreviousButtonClicked(callback!!.getIndexInViewFlipper(this))
|
||||
callback?.let { it.onPreviousButtonClicked(it.getIndexInViewFlipper(this)) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,11 +96,10 @@ class DepictsFragment : UploadBaseFragment(), DepictsContract.View {
|
|||
|
||||
if (media == null) {
|
||||
binding.depictsTitle.text =
|
||||
String.format(
|
||||
getString(R.string.step_count), callback!!.getIndexInViewFlipper(
|
||||
this
|
||||
) + 1,
|
||||
callback!!.totalNumberOfSteps, getString(R.string.depicts_step_title)
|
||||
String.format(getString(R.string.step_count),
|
||||
callback!!.getIndexInViewFlipper(this) + 1,
|
||||
callback!!.totalNumberOfSteps,
|
||||
getString(R.string.depicts_step_title)
|
||||
)
|
||||
} else {
|
||||
binding.depictsTitle.setText(R.string.edit_depictions)
|
||||
|
|
@ -285,6 +284,7 @@ class DepictsFragment : UploadBaseFragment(), DepictsContract.View {
|
|||
override fun showProgressDialog() {
|
||||
progressDialog = ProgressDialog(requireContext())
|
||||
progressDialog!!.setMessage(getString(R.string.please_wait))
|
||||
progressDialog!!.setCancelable(false)
|
||||
progressDialog!!.show()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,8 +45,7 @@ class MediaLicenseFragment : UploadBaseFragment(), MediaLicenseContract.View {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.tvTitle.text = getString(
|
||||
R.string.step_count,
|
||||
binding.tvTitle.text = getString(R.string.step_count,
|
||||
callback!!.getIndexInViewFlipper(this) + 1,
|
||||
callback!!.totalNumberOfSteps,
|
||||
getString(R.string.license_step_title)
|
||||
|
|
|
|||
|
|
@ -1,922 +0,0 @@
|
|||
package fr.free.nrw.commons.upload.mediaDetails;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.speech.RecognizerIntent;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import fr.free.nrw.commons.CameraPosition;
|
||||
import fr.free.nrw.commons.locationpicker.LocationPicker;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.databinding.FragmentUploadMediaDetailFragmentBinding;
|
||||
import fr.free.nrw.commons.edit.EditActivity;
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
||||
import fr.free.nrw.commons.kvstore.BasicKvStore;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import fr.free.nrw.commons.upload.ImageCoordinates;
|
||||
import fr.free.nrw.commons.upload.SimilarImageDialogFragment;
|
||||
import fr.free.nrw.commons.upload.UploadActivity;
|
||||
import fr.free.nrw.commons.upload.UploadBaseFragment;
|
||||
import fr.free.nrw.commons.upload.UploadItem;
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetail;
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter;
|
||||
import fr.free.nrw.commons.utils.ActivityUtils;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import fr.free.nrw.commons.utils.ImageUtils;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||
UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener {
|
||||
|
||||
private UploadMediaDetailAdapter uploadMediaDetailAdapter;
|
||||
|
||||
private final ActivityResultLauncher<Intent> startForResult = registerForActivityResult(
|
||||
new StartActivityForResult(), result -> {
|
||||
onCameraPosition(result);
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> startForEditActivityResult = registerForActivityResult(
|
||||
new StartActivityForResult(), result -> {
|
||||
onEditActivityResult(result);
|
||||
}
|
||||
);
|
||||
|
||||
private final ActivityResultLauncher<Intent> voiceInputResultLauncher = registerForActivityResult(
|
||||
new StartActivityForResult(), result -> {
|
||||
onVoiceInput(result);
|
||||
}
|
||||
);
|
||||
|
||||
public static Activity activity ;
|
||||
|
||||
private int indexOfFragment;
|
||||
|
||||
/**
|
||||
* A key for applicationKvStore. By this key we can retrieve the location of last UploadItem ex.
|
||||
* 12.3433,54.78897 from applicationKvStore.
|
||||
*/
|
||||
public static final String LAST_LOCATION = "last_location_while_uploading";
|
||||
public static final String LAST_ZOOM = "last_zoom_level_while_uploading";
|
||||
|
||||
|
||||
public static final String UPLOADABLE_FILE = "uploadable_file";
|
||||
|
||||
public static final String UPLOAD_MEDIA_DETAILS = "upload_media_detail_adapter";
|
||||
|
||||
/**
|
||||
* True when user removes location from the current image
|
||||
*/
|
||||
private boolean hasUserRemovedLocation;
|
||||
|
||||
|
||||
@Inject
|
||||
UploadMediaDetailsContract.UserActionListener presenter;
|
||||
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
JsonKvStore defaultKvStore;
|
||||
|
||||
@Inject
|
||||
RecentLanguagesDao recentLanguagesDao;
|
||||
|
||||
private UploadableFile uploadableFile;
|
||||
private Place place;
|
||||
|
||||
private boolean isExpanded = true;
|
||||
|
||||
/**
|
||||
* True if location is added via the "missing location" popup dialog (which appears after
|
||||
* tapping "Next" if the picture has no geographical coordinates).
|
||||
*/
|
||||
private boolean isMissingLocationDialog;
|
||||
|
||||
/**
|
||||
* showNearbyFound will be true, if any nearby location found that needs pictures and the nearby
|
||||
* popup is yet to be shown Used to show and check if the nearby found popup is already shown
|
||||
*/
|
||||
private boolean showNearbyFound;
|
||||
|
||||
/**
|
||||
* nearbyPlace holds the detail of nearby place that need pictures, if any found
|
||||
*/
|
||||
private Place nearbyPlace;
|
||||
private UploadItem uploadItem;
|
||||
/**
|
||||
* inAppPictureLocation: use location recorded while using the in-app camera if device camera
|
||||
* does not record it in the EXIF
|
||||
*/
|
||||
private LatLng inAppPictureLocation;
|
||||
/**
|
||||
* editableUploadItem : Storing the upload item before going to update the coordinates
|
||||
*/
|
||||
private UploadItem editableUploadItem;
|
||||
|
||||
private BasicKvStore basicKvStore;
|
||||
|
||||
private final String keyForShowingAlertDialog = "isNoNetworkAlertDialogShowing";
|
||||
|
||||
private UploadMediaDetailFragmentCallback callback;
|
||||
|
||||
private FragmentUploadMediaDetailFragmentBinding binding;
|
||||
|
||||
public void setCallback(UploadMediaDetailFragmentCallback callback) {
|
||||
this.callback = callback;
|
||||
UploadMediaPresenter.presenterCallback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
|
||||
if(savedInstanceState!=null && uploadableFile==null) {
|
||||
uploadableFile = savedInstanceState.getParcelable(UPLOADABLE_FILE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void setImageToBeUploaded(UploadableFile uploadableFile, Place place,
|
||||
LatLng inAppPictureLocation) {
|
||||
this.uploadableFile = uploadableFile;
|
||||
this.place = place;
|
||||
this.inAppPictureLocation = inAppPictureLocation;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
binding = FragmentUploadMediaDetailFragmentBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
activity = getActivity();
|
||||
basicKvStore = new BasicKvStore(activity, "CurrentUploadImageQualities");
|
||||
|
||||
if (callback != null) {
|
||||
indexOfFragment = callback.getIndexInViewFlipper(this);
|
||||
init();
|
||||
}
|
||||
|
||||
if(savedInstanceState!=null){
|
||||
if(uploadMediaDetailAdapter.getItems().size()==0 && callback != null){
|
||||
uploadMediaDetailAdapter.setItems(savedInstanceState.getParcelableArrayList(UPLOAD_MEDIA_DETAILS));
|
||||
presenter.setUploadMediaDetails(uploadMediaDetailAdapter.getItems(),
|
||||
indexOfFragment);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if(!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, getActivity())) {
|
||||
ActivityUtils.startActivityWithFlags(
|
||||
getActivity(), MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void init() {
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
binding.tvTitle.setText(getString(R.string.step_count, (indexOfFragment + 1),
|
||||
callback.getTotalNumberOfSteps(), getString(R.string.media_detail_step_title)));
|
||||
binding.tooltip.setOnClickListener(
|
||||
v -> showInfoAlert(R.string.media_detail_step_title, R.string.media_details_tooltip));
|
||||
initPresenter();
|
||||
presenter.receiveImage(uploadableFile, place, inAppPictureLocation);
|
||||
initRecyclerView();
|
||||
|
||||
if (indexOfFragment == 0) {
|
||||
binding.btnPrevious.setEnabled(false);
|
||||
binding.btnPrevious.setAlpha(0.5f);
|
||||
} else {
|
||||
binding.btnPrevious.setEnabled(true);
|
||||
binding.btnPrevious.setAlpha(1.0f);
|
||||
}
|
||||
|
||||
// If the image EXIF data contains the location, show the map icon with a green tick
|
||||
if (inAppPictureLocation != null ||
|
||||
(uploadableFile != null && uploadableFile.hasLocation())) {
|
||||
Drawable mapTick = getResources().getDrawable(R.drawable.ic_map_available_20dp);
|
||||
binding.locationImageView.setImageDrawable(mapTick);
|
||||
binding.locationTextView.setText(R.string.edit_location);
|
||||
} else {
|
||||
// Otherwise, show the map icon with a red question mark
|
||||
Drawable mapQuestionMark =
|
||||
getResources().getDrawable(R.drawable.ic_map_not_available_20dp);
|
||||
binding.locationImageView.setImageDrawable(mapQuestionMark);
|
||||
binding.locationTextView.setText(R.string.add_location);
|
||||
}
|
||||
|
||||
//If this is the last media, we have nothing to copy, lets not show the button
|
||||
if (indexOfFragment == callback.getTotalNumberOfSteps() - 4) {
|
||||
binding.btnCopySubsequentMedia.setVisibility(View.GONE);
|
||||
} else {
|
||||
binding.btnCopySubsequentMedia.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
binding.btnNext.setOnClickListener(v -> onNextButtonClicked());
|
||||
binding.btnPrevious.setOnClickListener(v -> onPreviousButtonClicked());
|
||||
binding.llEditImage.setOnClickListener(v -> onEditButtonClicked());
|
||||
binding.llContainerTitle.setOnClickListener(v -> onLlContainerTitleClicked());
|
||||
binding.llLocationStatus.setOnClickListener(v -> onIbMapClicked());
|
||||
binding.btnCopySubsequentMedia.setOnClickListener(v -> onButtonCopyTitleDescToSubsequentMedia());
|
||||
|
||||
|
||||
attachImageViewScaleChangeListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the scale change listener to the image view
|
||||
*/
|
||||
private void attachImageViewScaleChangeListener() {
|
||||
binding.backgroundImage.setOnScaleChangeListener(
|
||||
(scaleFactor, focusX, focusY) -> {
|
||||
//Whenever the uses plays with the image, lets collapse the media detail container
|
||||
//only if it is not already collapsed, which resolves flickering of arrow
|
||||
if (isExpanded) {
|
||||
expandCollapseLlMediaDetail(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* attach the presenter with the view
|
||||
*/
|
||||
private void initPresenter() {
|
||||
presenter.onAttachView(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* init the description recycler veiw and caption recyclerview
|
||||
*/
|
||||
private void initRecyclerView() {
|
||||
uploadMediaDetailAdapter = new UploadMediaDetailAdapter(this,
|
||||
defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao, voiceInputResultLauncher);
|
||||
uploadMediaDetailAdapter.setCallback(this::showInfoAlert);
|
||||
uploadMediaDetailAdapter.setEventListener(this);
|
||||
binding.rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.rvDescriptions.setAdapter(uploadMediaDetailAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* show dialog with info
|
||||
* @param titleStringID
|
||||
* @param messageStringId
|
||||
*/
|
||||
private void showInfoAlert(int titleStringID, int messageStringId) {
|
||||
DialogUtil.showAlertDialog(getActivity(), getString(titleStringID),
|
||||
getString(messageStringId), getString(android.R.string.ok), null);
|
||||
}
|
||||
|
||||
|
||||
public void onNextButtonClicked() {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
presenter.displayLocDialog(indexOfFragment, inAppPictureLocation, hasUserRemovedLocation);
|
||||
}
|
||||
|
||||
public void onPreviousButtonClicked() {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
callback.onPreviousButtonClicked(indexOfFragment);
|
||||
}
|
||||
|
||||
public void onEditButtonClicked() {
|
||||
presenter.onEditButtonClicked(indexOfFragment);
|
||||
}
|
||||
@Override
|
||||
public void showSimilarImageFragment(String originalFilePath, String possibleFilePath,
|
||||
ImageCoordinates similarImageCoordinates) {
|
||||
BasicKvStore basicKvStore = new BasicKvStore(getActivity(), "IsAnyImageCancelled");
|
||||
if (!basicKvStore.getBoolean("IsAnyImageCancelled", false)) {
|
||||
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
|
||||
newFragment.setCancelable(false);
|
||||
newFragment.setCallback(new SimilarImageDialogFragment.Callback() {
|
||||
@Override
|
||||
public void onPositiveResponse() {
|
||||
Timber.d("positive response from similar image fragment");
|
||||
presenter.useSimilarPictureCoordinates(similarImageCoordinates,
|
||||
indexOfFragment);
|
||||
|
||||
// set the description text when user selects to use coordinate from the other image
|
||||
// which was taken within 120s
|
||||
// fixing: https://github.com/commons-app/apps-android-commons/issues/4700
|
||||
uploadMediaDetailAdapter.getItems().get(0).setDescriptionText(
|
||||
getString(R.string.similar_coordinate_description_auto_set));
|
||||
updateMediaDetails(uploadMediaDetailAdapter.getItems());
|
||||
|
||||
// Replace the 'Add location' button with 'Edit location' button when user clicks
|
||||
// yes in similar image dialog
|
||||
// fixing: https://github.com/commons-app/apps-android-commons/issues/5669
|
||||
Drawable mapTick = getResources().getDrawable(R.drawable.ic_map_available_20dp);
|
||||
binding.locationImageView.setImageDrawable(mapTick);
|
||||
binding.locationTextView.setText(R.string.edit_location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNegativeResponse() {
|
||||
Timber.d("negative response from similar image fragment");
|
||||
}
|
||||
});
|
||||
Bundle args = new Bundle();
|
||||
args.putString("originalImagePath", originalFilePath);
|
||||
args.putString("possibleImagePath", possibleFilePath);
|
||||
newFragment.setArguments(args);
|
||||
newFragment.show(getChildFragmentManager(), "dialog");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImageProcessed(UploadItem uploadItem, Place place) {
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
binding.backgroundImage.setImageURI(uploadItem.getMediaUri());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets variables to Show popup if any nearby location needing pictures matches uploadable picture's GPS location
|
||||
* @param uploadItem
|
||||
* @param place
|
||||
*/
|
||||
@Override
|
||||
public void onNearbyPlaceFound(UploadItem uploadItem, Place place) {
|
||||
nearbyPlace = place;
|
||||
this.uploadItem = uploadItem;
|
||||
showNearbyFound = true;
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
if (indexOfFragment == 0) {
|
||||
if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) {
|
||||
final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace);
|
||||
if (response) {
|
||||
if (callback != null) {
|
||||
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showNearbyPlaceFound(nearbyPlace);
|
||||
}
|
||||
showNearbyFound = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows nearby place found popup
|
||||
* @param place
|
||||
*/
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
// To avoid the unwanted lint warning that string 'upload_nearby_place_found_description' is not of a valid format
|
||||
private void showNearbyPlaceFound(Place place) {
|
||||
final View customLayout = getLayoutInflater().inflate(R.layout.custom_nearby_found, null);
|
||||
ImageView nearbyFoundImage = customLayout.findViewById(R.id.nearbyItemImage);
|
||||
nearbyFoundImage.setImageURI(uploadItem.getMediaUri());
|
||||
|
||||
final Activity activity = getActivity();
|
||||
|
||||
if (activity instanceof UploadActivity) {
|
||||
final boolean isMultipleFilesSelected = ((UploadActivity) activity).getIsMultipleFilesSelected();
|
||||
|
||||
// Determine the message based on the selection status
|
||||
String message;
|
||||
if (isMultipleFilesSelected) {
|
||||
// Use plural message if multiple files are selected
|
||||
message = String.format(Locale.getDefault(),
|
||||
getString(R.string.upload_nearby_place_found_description_plural),
|
||||
place.getName());
|
||||
} else {
|
||||
// Use singular message if only one file is selected
|
||||
message = String.format(Locale.getDefault(),
|
||||
getString(R.string.upload_nearby_place_found_description_singular),
|
||||
place.getName());
|
||||
}
|
||||
|
||||
// Show the AlertDialog with the determined message
|
||||
DialogUtil.showAlertDialog(getActivity(),
|
||||
getString(R.string.upload_nearby_place_found_title),
|
||||
message,
|
||||
() -> {
|
||||
// Execute when user confirms the upload is of the specified place
|
||||
UploadActivity.nearbyPopupAnswers.put(place, true);
|
||||
presenter.onUserConfirmedUploadIsOfPlace(place, indexOfFragment);
|
||||
},
|
||||
() -> {
|
||||
// Execute when user cancels the upload of the specified place
|
||||
UploadActivity.nearbyPopupAnswers.put(place, false);
|
||||
},
|
||||
customLayout
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showProgress(boolean shouldShow) {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
callback.showProgress(shouldShow);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImageValidationSuccess() {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
callback.onNextButtonClicked(indexOfFragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method gets called whenever the next/previous button is pressed
|
||||
*/
|
||||
@Override
|
||||
protected void onBecameVisible() {
|
||||
super.onBecameVisible();
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
presenter.fetchTitleAndDescription(indexOfFragment);
|
||||
if (showNearbyFound) {
|
||||
if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) {
|
||||
final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace);
|
||||
if (response) {
|
||||
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment);
|
||||
}
|
||||
} else {
|
||||
showNearbyPlaceFound(nearbyPlace);
|
||||
}
|
||||
showNearbyFound = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMessage(int stringResourceId, int colorResourceId) {
|
||||
ViewUtil.showLongToast(getContext(), stringResourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMessage(String message, int colorResourceId) {
|
||||
ViewUtil.showLongToast(getContext(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showDuplicatePicturePopup(UploadItem uploadItem) {
|
||||
if (defaultKvStore.getBoolean("showDuplicatePicturePopup", true)) {
|
||||
String uploadTitleFormat = getString(R.string.upload_title_duplicate);
|
||||
View checkBoxView = View
|
||||
.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
|
||||
CheckBox checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
|
||||
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
defaultKvStore.putBoolean("showDuplicatePicturePopup", false);
|
||||
}
|
||||
});
|
||||
DialogUtil.showAlertDialog(getActivity(),
|
||||
getString(R.string.duplicate_file_name),
|
||||
String.format(Locale.getDefault(),
|
||||
uploadTitleFormat,
|
||||
uploadItem.getFilename()),
|
||||
getString(R.string.upload),
|
||||
getString(R.string.cancel),
|
||||
() -> {
|
||||
uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP);
|
||||
onImageValidationSuccess();
|
||||
}, null,
|
||||
checkBoxView);
|
||||
} else {
|
||||
uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP);
|
||||
onImageValidationSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog alerting the user that internet connection is required for upload process
|
||||
* Does nothing if there is network connectivity and then the user presses okay
|
||||
*/
|
||||
@Override
|
||||
public void showConnectionErrorPopupForCaptionCheck() {
|
||||
DialogUtil.showAlertDialog(getActivity(),
|
||||
getString(R.string.upload_connection_error_alert_title),
|
||||
getString(R.string.upload_connection_error_alert_detail),
|
||||
getString(R.string.ok),
|
||||
getString(R.string.cancel_upload),
|
||||
() -> {
|
||||
if (!NetworkUtils.isInternetConnectionEstablished(activity)) {
|
||||
showConnectionErrorPopupForCaptionCheck();
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
activity.finish();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog alerting the user that internet connection is required for upload process
|
||||
* Recalls UploadMediaPresenter.getImageQuality for all the next upload items,
|
||||
* if there is network connectivity and then the user presses okay
|
||||
*/
|
||||
@Override
|
||||
public void showConnectionErrorPopup() {
|
||||
try {
|
||||
boolean FLAG_ALERT_DIALOG_SHOWING = basicKvStore.getBoolean(
|
||||
keyForShowingAlertDialog, false);
|
||||
if (!FLAG_ALERT_DIALOG_SHOWING) {
|
||||
basicKvStore.putBoolean(keyForShowingAlertDialog, true);
|
||||
DialogUtil.showAlertDialog(getActivity(),
|
||||
getString(R.string.upload_connection_error_alert_title),
|
||||
getString(R.string.upload_connection_error_alert_detail),
|
||||
getString(R.string.ok),
|
||||
getString(R.string.cancel_upload),
|
||||
() -> {
|
||||
basicKvStore.putBoolean(keyForShowingAlertDialog, false);
|
||||
if (NetworkUtils.isInternetConnectionEstablished(activity)) {
|
||||
int sizeOfUploads = basicKvStore.getInt(
|
||||
UploadActivity.keyForCurrentUploadImagesSize);
|
||||
for (int i = indexOfFragment; i < sizeOfUploads; i++) {
|
||||
presenter.getImageQuality(i, inAppPictureLocation, activity);
|
||||
}
|
||||
} else {
|
||||
showConnectionErrorPopup();
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
basicKvStore.putBoolean(keyForShowingAlertDialog, false);
|
||||
activity.finish();
|
||||
},
|
||||
null
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showExternalMap(final UploadItem uploadItem) {
|
||||
goToLocationPickerActivity(uploadItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the image editing activity to edit the specified UploadItem.
|
||||
*
|
||||
* @param uploadItem The UploadItem to be edited.
|
||||
*
|
||||
* This method is called to start the image editing activity for a specific UploadItem.
|
||||
* It sets the UploadItem as the currently editable item, creates an intent to launch the
|
||||
* EditActivity, and passes the image file path as an extra in the intent. The activity
|
||||
* is started using resultLauncher that handles the result in respective callback.
|
||||
*/
|
||||
@Override
|
||||
public void showEditActivity(UploadItem uploadItem) {
|
||||
editableUploadItem = uploadItem;
|
||||
Intent intent = new Intent(getContext(), EditActivity.class);
|
||||
intent.putExtra("image", uploadableFile.getFilePath().toString());
|
||||
startForEditActivityResult.launch(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Location picker activity. Show the location first then user can modify it by clicking
|
||||
* modify location button.
|
||||
* @param uploadItem current upload item
|
||||
*/
|
||||
private void goToLocationPickerActivity(final UploadItem uploadItem) {
|
||||
|
||||
editableUploadItem = uploadItem;
|
||||
double defaultLatitude = 37.773972;
|
||||
double defaultLongitude = -122.431297;
|
||||
double defaultZoom = 16.0;
|
||||
|
||||
final Intent locationPickerIntent;
|
||||
|
||||
/* Retrieve image location from EXIF if present or
|
||||
check if user has provided location while using the in-app camera.
|
||||
Use location of last UploadItem if none of them is available */
|
||||
if (uploadItem.getGpsCoords() != null && uploadItem.getGpsCoords()
|
||||
.getDecLatitude() != 0.0 && uploadItem.getGpsCoords().getDecLongitude() != 0.0) {
|
||||
defaultLatitude = uploadItem.getGpsCoords()
|
||||
.getDecLatitude();
|
||||
defaultLongitude = uploadItem.getGpsCoords().getDecLongitude();
|
||||
defaultZoom = uploadItem.getGpsCoords().getZoomLevel();
|
||||
|
||||
locationPickerIntent = new LocationPicker.IntentBuilder()
|
||||
.defaultLocation(new CameraPosition(defaultLatitude,defaultLongitude,defaultZoom))
|
||||
.activityKey("UploadActivity")
|
||||
.build(getActivity());
|
||||
} else {
|
||||
if (defaultKvStore.getString(LAST_LOCATION) != null) {
|
||||
final String[] locationLatLng
|
||||
= defaultKvStore.getString(LAST_LOCATION).split(",");
|
||||
defaultLatitude = Double.parseDouble(locationLatLng[0]);
|
||||
defaultLongitude = Double.parseDouble(locationLatLng[1]);
|
||||
}
|
||||
if (defaultKvStore.getString(LAST_ZOOM) != null) {
|
||||
defaultZoom = Double.parseDouble(defaultKvStore.getString(LAST_ZOOM));
|
||||
}
|
||||
|
||||
locationPickerIntent = new LocationPicker.IntentBuilder()
|
||||
.defaultLocation(new CameraPosition(defaultLatitude,defaultLongitude,defaultZoom))
|
||||
.activityKey("NoLocationUploadActivity")
|
||||
.build(getActivity());
|
||||
}
|
||||
startForResult.launch(locationPickerIntent);
|
||||
}
|
||||
|
||||
private void onCameraPosition(ActivityResult result){
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
|
||||
assert result.getData() != null;
|
||||
final CameraPosition cameraPosition = LocationPicker.getCameraPosition(result.getData());
|
||||
|
||||
if (cameraPosition != null) {
|
||||
|
||||
final String latitude = String.valueOf(cameraPosition.getLatitude());
|
||||
final String longitude = String.valueOf(cameraPosition.getLongitude());
|
||||
final double zoom = cameraPosition.getZoom();
|
||||
|
||||
editLocation(latitude, longitude, zoom);
|
||||
// If isMissingLocationDialog is true, it means that the user has already tapped the
|
||||
// "Next" button, so go directly to the next step.
|
||||
if (isMissingLocationDialog) {
|
||||
isMissingLocationDialog = false;
|
||||
onNextButtonClicked();
|
||||
}
|
||||
} else {
|
||||
// If camera position is null means location is removed by the user
|
||||
removeLocation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onVoiceInput(ActivityResult result) {
|
||||
if (result.getResultCode() == RESULT_OK && result.getData() != null) {
|
||||
ArrayList<String> resultData = result.getData().getStringArrayListExtra(
|
||||
RecognizerIntent.EXTRA_RESULTS);
|
||||
uploadMediaDetailAdapter.handleSpeechResult(resultData.get(0));
|
||||
}else {
|
||||
Timber.e("Error %s", result.getResultCode());
|
||||
}
|
||||
}
|
||||
|
||||
private void onEditActivityResult(ActivityResult result){
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
String path = result.getData().getStringExtra("editedImageFilePath");
|
||||
|
||||
if (Objects.equals(result, "Error")) {
|
||||
Timber.e("Error in rotating image");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (binding != null){
|
||||
binding.backgroundImage.setImageURI(Uri.fromFile(new File(path)));
|
||||
}
|
||||
editableUploadItem.setContentAndMediaUri(Uri.fromFile(new File(path)));
|
||||
callback.changeThumbnail(indexOfFragment,
|
||||
path);
|
||||
} catch (Exception e) {
|
||||
Timber.e(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the location data from the image, by setting them to null
|
||||
*/
|
||||
public void removeLocation() {
|
||||
editableUploadItem.getGpsCoords().setDecimalCoords(null);
|
||||
try {
|
||||
ExifInterface sourceExif = new ExifInterface(uploadableFile.getFilePath());
|
||||
String[] exifTags = {
|
||||
ExifInterface.TAG_GPS_LATITUDE,
|
||||
ExifInterface.TAG_GPS_LATITUDE_REF,
|
||||
ExifInterface.TAG_GPS_LONGITUDE,
|
||||
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
||||
};
|
||||
|
||||
for (String tag : exifTags) {
|
||||
sourceExif.setAttribute(tag, null);
|
||||
}
|
||||
sourceExif.saveAttributes();
|
||||
|
||||
Drawable mapQuestion = getResources().getDrawable(R.drawable.ic_map_not_available_20dp);
|
||||
|
||||
if (binding != null) {
|
||||
binding.locationImageView.setImageDrawable(mapQuestion);
|
||||
binding.locationTextView.setText(R.string.add_location);
|
||||
}
|
||||
|
||||
editableUploadItem.getGpsCoords().setDecLatitude(0.0);
|
||||
editableUploadItem.getGpsCoords().setDecLongitude(0.0);
|
||||
editableUploadItem.getGpsCoords().setImageCoordsExists(false);
|
||||
hasUserRemovedLocation = true;
|
||||
|
||||
Toast.makeText(getContext(), getString(R.string.location_removed), Toast.LENGTH_LONG)
|
||||
.show();
|
||||
} catch (Exception e) {
|
||||
Timber.d(e);
|
||||
Toast.makeText(getContext(), "Location could not be removed due to internal error",
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the old coordinates with new one
|
||||
* @param latitude new latitude
|
||||
* @param longitude new longitude
|
||||
*/
|
||||
public void editLocation(final String latitude, final String longitude, final double zoom) {
|
||||
|
||||
editableUploadItem.getGpsCoords().setDecLatitude(Double.parseDouble(latitude));
|
||||
editableUploadItem.getGpsCoords().setDecLongitude(Double.parseDouble(longitude));
|
||||
editableUploadItem.getGpsCoords().setDecimalCoords(latitude + "|" + longitude);
|
||||
editableUploadItem.getGpsCoords().setImageCoordsExists(true);
|
||||
editableUploadItem.getGpsCoords().setZoomLevel(zoom);
|
||||
|
||||
// Replace the map icon using the one with a green tick
|
||||
Drawable mapTick = getResources().getDrawable(R.drawable.ic_map_available_20dp);
|
||||
|
||||
if (binding != null) {
|
||||
binding.locationImageView.setImageDrawable(mapTick);
|
||||
binding.locationTextView.setText(R.string.edit_location);
|
||||
}
|
||||
|
||||
Toast.makeText(getContext(), getString(R.string.location_updated), Toast.LENGTH_LONG).show();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMediaDetails(List<UploadMediaDetail> uploadMediaDetails) {
|
||||
uploadMediaDetailAdapter.setItems(uploadMediaDetails);
|
||||
showNearbyFound =
|
||||
showNearbyFound && (
|
||||
uploadMediaDetails == null || uploadMediaDetails.isEmpty()
|
||||
|| listContainsEmptyDetails(
|
||||
uploadMediaDetails));
|
||||
}
|
||||
|
||||
/**
|
||||
* if the media details that come in here are empty
|
||||
* (empty caption AND empty description, with caption being the decider here)
|
||||
* this method allows usage of nearby place caption and description if any
|
||||
* else it takes the media details saved in prior for this picture
|
||||
* @param uploadMediaDetails saved media details,
|
||||
* ex: in case when "copy to subsequent media" button is clicked
|
||||
* for a previous image
|
||||
* @return boolean whether the details are empty or not
|
||||
*/
|
||||
private boolean listContainsEmptyDetails(List<UploadMediaDetail> uploadMediaDetails) {
|
||||
for (UploadMediaDetail uploadDetail: uploadMediaDetails) {
|
||||
if (!TextUtils.isEmpty(uploadDetail.getCaptionText()) && !TextUtils.isEmpty(uploadDetail.getDescriptionText())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Showing dialog for adding location
|
||||
*
|
||||
* @param onSkipClicked proceed for verifying image quality
|
||||
*/
|
||||
@Override
|
||||
public void displayAddLocationDialog(final Runnable onSkipClicked) {
|
||||
isMissingLocationDialog = true;
|
||||
DialogUtil.showAlertDialog(requireActivity(),
|
||||
getString(R.string.no_location_found_title),
|
||||
getString(R.string.no_location_found_message),
|
||||
getString(R.string.add_location),
|
||||
getString(R.string.skip_login),
|
||||
this::onIbMapClicked,
|
||||
onSkipClicked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
presenter.onDetachView();
|
||||
}
|
||||
|
||||
public void onLlContainerTitleClicked() {
|
||||
expandCollapseLlMediaDetail(!isExpanded);
|
||||
}
|
||||
|
||||
/**
|
||||
* show hide media detail based on
|
||||
* @param shouldExpand
|
||||
*/
|
||||
private void expandCollapseLlMediaDetail(boolean shouldExpand){
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
binding.llContainerMediaDetail.setVisibility(shouldExpand ? View.VISIBLE : View.GONE);
|
||||
isExpanded = !isExpanded;
|
||||
binding.ibExpandCollapse.setRotation(binding.ibExpandCollapse.getRotation() + 180);
|
||||
}
|
||||
|
||||
public void onIbMapClicked() {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
presenter.onMapIconClicked(indexOfFragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrimaryCaptionTextChange(boolean isNotEmpty) {
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
binding.btnCopySubsequentMedia.setEnabled(isNotEmpty);
|
||||
binding.btnCopySubsequentMedia.setClickable(isNotEmpty);
|
||||
binding.btnCopySubsequentMedia.setAlpha(isNotEmpty ? 1.0f : 0.5f);
|
||||
binding.btnNext.setEnabled(isNotEmpty);
|
||||
binding.btnNext.setClickable(isNotEmpty);
|
||||
binding.btnNext.setAlpha(isNotEmpty ? 1.0f : 0.5f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new language item to RecyclerView
|
||||
*/
|
||||
@Override
|
||||
public void addLanguage() {
|
||||
UploadMediaDetail uploadMediaDetail = new UploadMediaDetail();
|
||||
uploadMediaDetail.setManuallyAdded(true);//This was manually added by the user
|
||||
uploadMediaDetailAdapter.addDescription(uploadMediaDetail);
|
||||
binding.rvDescriptions.smoothScrollToPosition(uploadMediaDetailAdapter.getItemCount()-1);
|
||||
}
|
||||
|
||||
public interface UploadMediaDetailFragmentCallback extends Callback {
|
||||
|
||||
void deletePictureAtIndex(int index);
|
||||
|
||||
void changeThumbnail(int index, String uri);
|
||||
}
|
||||
|
||||
|
||||
public void onButtonCopyTitleDescToSubsequentMedia(){
|
||||
presenter.copyTitleAndDescriptionToSubsequentMedia(indexOfFragment);
|
||||
Toast.makeText(getContext(), getResources().getString(R.string.copied_successfully), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
if(uploadableFile!=null){
|
||||
outState.putParcelable(UPLOADABLE_FILE,uploadableFile);
|
||||
}
|
||||
if(uploadMediaDetailAdapter!=null){
|
||||
outState.putParcelableArrayList(UPLOAD_MEDIA_DETAILS,
|
||||
(ArrayList<? extends Parcelable>) uploadMediaDetailAdapter.getItems());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,903 @@
|
|||
package fr.free.nrw.commons.upload.mediaDetails
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.speech.RecognizerIntent
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import fr.free.nrw.commons.CameraPosition
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.contributions.MainActivity
|
||||
import fr.free.nrw.commons.databinding.FragmentUploadMediaDetailFragmentBinding
|
||||
import fr.free.nrw.commons.edit.EditActivity
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile
|
||||
import fr.free.nrw.commons.kvstore.BasicKvStore
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
import fr.free.nrw.commons.locationpicker.LocationPicker
|
||||
import fr.free.nrw.commons.locationpicker.LocationPicker.getCameraPosition
|
||||
import fr.free.nrw.commons.nearby.Place
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
|
||||
import fr.free.nrw.commons.settings.Prefs
|
||||
import fr.free.nrw.commons.upload.ImageCoordinates
|
||||
import fr.free.nrw.commons.upload.SimilarImageDialogFragment
|
||||
import fr.free.nrw.commons.upload.UploadActivity
|
||||
import fr.free.nrw.commons.upload.UploadBaseFragment
|
||||
import fr.free.nrw.commons.upload.UploadItem
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetail
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter.Companion.presenterCallback
|
||||
import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags
|
||||
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||
import fr.free.nrw.commons.utils.ImageUtils
|
||||
import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK
|
||||
import fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult
|
||||
import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
|
||||
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.ArrayList
|
||||
import java.util.Locale
|
||||
import java.util.Objects
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContract.View,
|
||||
UploadMediaDetailAdapter.EventListener {
|
||||
|
||||
private val startForResult = registerForActivityResult<Intent, ActivityResult>(
|
||||
ActivityResultContracts.StartActivityForResult(), ::onCameraPosition)
|
||||
|
||||
private val startForEditActivityResult = registerForActivityResult<Intent, ActivityResult>(
|
||||
ActivityResultContracts.StartActivityForResult(), ::onEditActivityResult)
|
||||
|
||||
private val voiceInputResultLauncher = registerForActivityResult<Intent, ActivityResult>(
|
||||
ActivityResultContracts.StartActivityForResult(), ::onVoiceInput)
|
||||
|
||||
@Inject
|
||||
lateinit var presenter: UploadMediaDetailsContract.UserActionListener
|
||||
|
||||
@Inject
|
||||
@field:Named("default_preferences")
|
||||
lateinit var defaultKvStore: JsonKvStore
|
||||
|
||||
@Inject
|
||||
lateinit var recentLanguagesDao: RecentLanguagesDao
|
||||
|
||||
/**
|
||||
* True when user removes location from the current image
|
||||
*/
|
||||
var hasUserRemovedLocation = false
|
||||
|
||||
/**
|
||||
* True if location is added via the "missing location" popup dialog (which appears after
|
||||
* tapping "Next" if the picture has no geographical coordinates).
|
||||
*/
|
||||
private var isMissingLocationDialog = false
|
||||
|
||||
/**
|
||||
* showNearbyFound will be true, if any nearby location found that needs pictures and the nearby
|
||||
* popup is yet to be shown Used to show and check if the nearby found popup is already shown
|
||||
*/
|
||||
private var showNearbyFound = false
|
||||
|
||||
/**
|
||||
* nearbyPlace holds the detail of nearby place that need pictures, if any found
|
||||
*/
|
||||
private var nearbyPlace: Place? = null
|
||||
private var uploadItem: UploadItem? = null
|
||||
|
||||
/**
|
||||
* inAppPictureLocation: use location recorded while using the in-app camera if device camera
|
||||
* does not record it in the EXIF
|
||||
*/
|
||||
var inAppPictureLocation: LatLng? = null
|
||||
|
||||
/**
|
||||
* editableUploadItem : Storing the upload item before going to update the coordinates
|
||||
*/
|
||||
private var editableUploadItem: UploadItem? = null
|
||||
|
||||
private var _binding: FragmentUploadMediaDetailFragmentBinding? = null
|
||||
private val binding: FragmentUploadMediaDetailFragmentBinding get() = _binding!!
|
||||
|
||||
private var basicKvStore: BasicKvStore? = null
|
||||
private val keyForShowingAlertDialog = "isNoNetworkAlertDialogShowing"
|
||||
private var uploadableFile: UploadableFile? = null
|
||||
private var place: Place? = null
|
||||
private lateinit var uploadMediaDetailAdapter: UploadMediaDetailAdapter
|
||||
var indexOfFragment = 0
|
||||
var isExpanded = true
|
||||
var fragmentCallback: UploadMediaDetailFragmentCallback? = null
|
||||
set(value) {
|
||||
field = value
|
||||
UploadMediaPresenter.presenterCallback = value
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState != null && uploadableFile == null) {
|
||||
uploadableFile = savedInstanceState.getParcelable(UPLOADABLE_FILE)
|
||||
}
|
||||
}
|
||||
|
||||
fun setImageToBeUploaded(
|
||||
uploadableFile: UploadableFile?, place: Place?, inAppPictureLocation: LatLng?
|
||||
) {
|
||||
this.uploadableFile = uploadableFile
|
||||
this.place = place
|
||||
this.inAppPictureLocation = inAppPictureLocation
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentUploadMediaDetailFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
basicKvStore = BasicKvStore(requireActivity(), "CurrentUploadImageQualities")
|
||||
|
||||
if (fragmentCallback != null) {
|
||||
indexOfFragment = fragmentCallback!!.getIndexInViewFlipper(this)
|
||||
initializeFragment()
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
if (uploadMediaDetailAdapter.items.isEmpty() && fragmentCallback != null) {
|
||||
uploadMediaDetailAdapter.items = savedInstanceState.getParcelableArrayList(UPLOAD_MEDIA_DETAILS)!!
|
||||
presenter.setUploadMediaDetails(uploadMediaDetailAdapter.items, indexOfFragment)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, requireActivity())) {
|
||||
startActivityWithFlags(
|
||||
requireActivity(),
|
||||
MainActivity::class.java,
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP,
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeFragment() {
|
||||
if (_binding == null) {
|
||||
return
|
||||
}
|
||||
binding.tvTitle.text = getString(
|
||||
R.string.step_count, (indexOfFragment + 1),
|
||||
fragmentCallback!!.totalNumberOfSteps, getString(R.string.media_detail_step_title)
|
||||
)
|
||||
binding.tooltip.setOnClickListener {
|
||||
showInfoAlert(
|
||||
R.string.media_detail_step_title,
|
||||
R.string.media_details_tooltip
|
||||
)
|
||||
}
|
||||
presenter.onAttachView(this)
|
||||
presenter.setupBasicKvStoreFactory { BasicKvStore(requireActivity(), it) }
|
||||
|
||||
presenter.receiveImage(uploadableFile, place, inAppPictureLocation)
|
||||
initRecyclerView()
|
||||
|
||||
with (binding){
|
||||
if (indexOfFragment == 0) {
|
||||
btnPrevious.isEnabled = false
|
||||
btnPrevious.alpha = 0.5f
|
||||
} else {
|
||||
btnPrevious.isEnabled = true
|
||||
btnPrevious.alpha = 1.0f
|
||||
}
|
||||
|
||||
// If the image EXIF data contains the location, show the map icon with a green tick
|
||||
if (inAppPictureLocation != null || (uploadableFile != null && uploadableFile!!.hasLocation())) {
|
||||
val mapTick =
|
||||
ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_available_20dp)
|
||||
locationImageView.setImageDrawable(mapTick)
|
||||
locationTextView.setText(R.string.edit_location)
|
||||
} else {
|
||||
// Otherwise, show the map icon with a red question mark
|
||||
val mapQuestionMark = ContextCompat.getDrawable(
|
||||
requireContext(),
|
||||
R.drawable.ic_map_not_available_20dp
|
||||
)
|
||||
locationImageView.setImageDrawable(mapQuestionMark)
|
||||
locationTextView.setText(R.string.add_location)
|
||||
}
|
||||
|
||||
//If this is the last media, we have nothing to copy, lets not show the button
|
||||
btnCopySubsequentMedia.visibility =
|
||||
if (indexOfFragment == fragmentCallback!!.totalNumberOfSteps - 4) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
|
||||
btnNext.setOnClickListener { presenter.displayLocDialog(indexOfFragment, inAppPictureLocation, hasUserRemovedLocation) }
|
||||
btnPrevious.setOnClickListener { fragmentCallback?.onPreviousButtonClicked(indexOfFragment) }
|
||||
llEditImage.setOnClickListener { presenter.onEditButtonClicked(indexOfFragment) }
|
||||
llContainerTitle.setOnClickListener { expandCollapseLlMediaDetail(!isExpanded) }
|
||||
llLocationStatus.setOnClickListener { presenter.onMapIconClicked(indexOfFragment) }
|
||||
btnCopySubsequentMedia.setOnClickListener { onButtonCopyTitleDescToSubsequentMedia() }
|
||||
}
|
||||
|
||||
attachImageViewScaleChangeListener()
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the scale change listener to the image view
|
||||
*/
|
||||
private fun attachImageViewScaleChangeListener() {
|
||||
binding.backgroundImage.setOnScaleChangeListener { _: Float, _: Float, _: Float ->
|
||||
//Whenever the uses plays with the image, lets collapse the media detail container
|
||||
//only if it is not already collapsed, which resolves flickering of arrow
|
||||
if (isExpanded) {
|
||||
expandCollapseLlMediaDetail(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* init the description recycler veiw and caption recyclerview
|
||||
*/
|
||||
private fun initRecyclerView() {
|
||||
uploadMediaDetailAdapter = UploadMediaDetailAdapter(
|
||||
this,
|
||||
defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, "")!!,
|
||||
recentLanguagesDao, voiceInputResultLauncher
|
||||
)
|
||||
uploadMediaDetailAdapter.callback =
|
||||
UploadMediaDetailAdapter.Callback { titleStringID: Int, messageStringId: Int ->
|
||||
showInfoAlert(titleStringID, messageStringId)
|
||||
}
|
||||
uploadMediaDetailAdapter.eventListener = this
|
||||
binding.rvDescriptions.layoutManager = LinearLayoutManager(context)
|
||||
binding.rvDescriptions.adapter = uploadMediaDetailAdapter
|
||||
}
|
||||
|
||||
private fun showInfoAlert(titleStringID: Int, messageStringId: Int) {
|
||||
showAlertDialog(
|
||||
requireActivity(),
|
||||
getString(titleStringID),
|
||||
getString(messageStringId),
|
||||
getString(android.R.string.ok),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
override fun showSimilarImageFragment(
|
||||
originalFilePath: String?, possibleFilePath: String?,
|
||||
similarImageCoordinates: ImageCoordinates?
|
||||
) {
|
||||
val basicKvStore = BasicKvStore(requireActivity(), "IsAnyImageCancelled")
|
||||
if (!basicKvStore.getBoolean("IsAnyImageCancelled", false)) {
|
||||
val newFragment = SimilarImageDialogFragment()
|
||||
newFragment.isCancelable = false
|
||||
newFragment.callback = object : SimilarImageDialogFragment.Callback {
|
||||
override fun onPositiveResponse() {
|
||||
Timber.d("positive response from similar image fragment")
|
||||
presenter.useSimilarPictureCoordinates(
|
||||
similarImageCoordinates!!,
|
||||
indexOfFragment
|
||||
)
|
||||
|
||||
// set the description text when user selects to use coordinate from the other image
|
||||
// which was taken within 120s
|
||||
// fixing: https://github.com/commons-app/apps-android-commons/issues/4700
|
||||
uploadMediaDetailAdapter.items[0].descriptionText =
|
||||
getString(R.string.similar_coordinate_description_auto_set)
|
||||
updateMediaDetails(uploadMediaDetailAdapter.items)
|
||||
|
||||
// Replace the 'Add location' button with 'Edit location' button when user clicks
|
||||
// yes in similar image dialog
|
||||
// fixing: https://github.com/commons-app/apps-android-commons/issues/5669
|
||||
val mapTick = ContextCompat.getDrawable(
|
||||
requireContext(),
|
||||
R.drawable.ic_map_available_20dp
|
||||
)
|
||||
binding.locationImageView.setImageDrawable(mapTick)
|
||||
binding.locationTextView.setText(R.string.edit_location)
|
||||
}
|
||||
|
||||
override fun onNegativeResponse() {
|
||||
Timber.d("negative response from similar image fragment")
|
||||
}
|
||||
}
|
||||
newFragment.arguments = bundleOf(
|
||||
"originalImagePath" to originalFilePath,
|
||||
"possibleImagePath" to possibleFilePath
|
||||
)
|
||||
newFragment.show(childFragmentManager, "dialog")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onImageProcessed(uploadItem: UploadItem) {
|
||||
if (_binding == null) {
|
||||
return
|
||||
}
|
||||
binding.backgroundImage.setImageURI(uploadItem.mediaUri)
|
||||
}
|
||||
|
||||
override fun onNearbyPlaceFound(
|
||||
uploadItem: UploadItem, place: Place?
|
||||
) {
|
||||
nearbyPlace = place
|
||||
this.uploadItem = uploadItem
|
||||
showNearbyFound = true
|
||||
if (fragmentCallback == null) {
|
||||
return
|
||||
}
|
||||
if (indexOfFragment == 0) {
|
||||
if (UploadActivity.nearbyPopupAnswers!!.containsKey(nearbyPlace!!)) {
|
||||
val response = UploadActivity.nearbyPopupAnswers!![nearbyPlace!!]!!
|
||||
if (response) {
|
||||
if (fragmentCallback != null) {
|
||||
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showNearbyPlaceFound(nearbyPlace!!)
|
||||
}
|
||||
showNearbyFound = false
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid") // To avoid the unwanted lint warning that string 'upload_nearby_place_found_description' is not of a valid format
|
||||
private fun showNearbyPlaceFound(place: Place) {
|
||||
val customLayout = layoutInflater.inflate(R.layout.custom_nearby_found, null)
|
||||
val nearbyFoundImage = customLayout.findViewById<ImageView>(R.id.nearbyItemImage)
|
||||
nearbyFoundImage.setImageURI(uploadItem!!.mediaUri)
|
||||
|
||||
val activity: Activity? = activity
|
||||
|
||||
if (activity is UploadActivity) {
|
||||
val isMultipleFilesSelected = activity.isMultipleFilesSelected
|
||||
|
||||
// Determine the message based on the selection status
|
||||
val message = if (isMultipleFilesSelected) {
|
||||
// Use plural message if multiple files are selected
|
||||
String.format(
|
||||
Locale.getDefault(),
|
||||
getString(R.string.upload_nearby_place_found_description_plural),
|
||||
place.getName()
|
||||
)
|
||||
} else {
|
||||
// Use singular message if only one file is selected
|
||||
String.format(
|
||||
Locale.getDefault(),
|
||||
getString(R.string.upload_nearby_place_found_description_singular),
|
||||
place.getName()
|
||||
)
|
||||
}
|
||||
|
||||
// Show the AlertDialog with the determined message
|
||||
showAlertDialog(
|
||||
requireActivity(),
|
||||
getString(R.string.upload_nearby_place_found_title),
|
||||
message,
|
||||
{
|
||||
// Execute when user confirms the upload is of the specified place
|
||||
UploadActivity.nearbyPopupAnswers!![place] = true
|
||||
presenter.onUserConfirmedUploadIsOfPlace(place, indexOfFragment)
|
||||
},
|
||||
{
|
||||
// Execute when user cancels the upload of the specified place
|
||||
UploadActivity.nearbyPopupAnswers!![place] = false
|
||||
},
|
||||
customLayout
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showProgress(shouldShow: Boolean) {
|
||||
if (fragmentCallback == null) {
|
||||
return
|
||||
}
|
||||
fragmentCallback!!.showProgress(shouldShow)
|
||||
}
|
||||
|
||||
override fun onImageValidationSuccess() {
|
||||
if (fragmentCallback == null) {
|
||||
return
|
||||
}
|
||||
fragmentCallback!!.onNextButtonClicked(indexOfFragment)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method gets called whenever the next/previous button is pressed
|
||||
*/
|
||||
override fun onBecameVisible() {
|
||||
super.onBecameVisible()
|
||||
if (fragmentCallback == null) {
|
||||
return
|
||||
}
|
||||
presenter.fetchTitleAndDescription(indexOfFragment)
|
||||
if (showNearbyFound) {
|
||||
if (UploadActivity.nearbyPopupAnswers!!.containsKey(nearbyPlace!!)) {
|
||||
val response = UploadActivity.nearbyPopupAnswers!![nearbyPlace!!]!!
|
||||
if (response) {
|
||||
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment)
|
||||
}
|
||||
} else {
|
||||
showNearbyPlaceFound(nearbyPlace!!)
|
||||
}
|
||||
showNearbyFound = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun showMessage(stringResourceId: Int, colorResourceId: Int) =
|
||||
showLongToast(requireContext(), stringResourceId)
|
||||
|
||||
override fun showMessage(message: String, colorResourceId: Int) =
|
||||
showLongToast(requireContext(), message)
|
||||
|
||||
override fun showDuplicatePicturePopup(uploadItem: UploadItem) {
|
||||
if (defaultKvStore.getBoolean("showDuplicatePicturePopup", true)) {
|
||||
val uploadTitleFormat = getString(R.string.upload_title_duplicate)
|
||||
val checkBoxView = View
|
||||
.inflate(activity, R.layout.nearby_permission_dialog, null)
|
||||
val checkBox = checkBoxView.findViewById<View>(R.id.never_ask_again) as CheckBox
|
||||
checkBox.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean ->
|
||||
if (isChecked) {
|
||||
defaultKvStore.putBoolean("showDuplicatePicturePopup", false)
|
||||
}
|
||||
}
|
||||
showAlertDialog(
|
||||
requireActivity(),
|
||||
getString(R.string.duplicate_file_name),
|
||||
String.format(
|
||||
Locale.getDefault(),
|
||||
uploadTitleFormat,
|
||||
uploadItem.filename
|
||||
),
|
||||
getString(R.string.upload),
|
||||
getString(R.string.cancel),
|
||||
{
|
||||
uploadItem.imageQuality = ImageUtils.IMAGE_KEEP
|
||||
onImageValidationSuccess()
|
||||
}, null,
|
||||
checkBoxView
|
||||
)
|
||||
} else {
|
||||
uploadItem.imageQuality = ImageUtils.IMAGE_KEEP
|
||||
onImageValidationSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog alerting the user that internet connection is required for upload process
|
||||
* Does nothing if there is network connectivity and then the user presses okay
|
||||
*/
|
||||
override fun showConnectionErrorPopupForCaptionCheck() {
|
||||
showAlertDialog(requireActivity(),
|
||||
getString(R.string.upload_connection_error_alert_title),
|
||||
getString(R.string.upload_connection_error_alert_detail),
|
||||
getString(R.string.ok),
|
||||
getString(R.string.cancel_upload),
|
||||
{
|
||||
if (!isInternetConnectionEstablished(requireActivity())) {
|
||||
showConnectionErrorPopupForCaptionCheck()
|
||||
}
|
||||
},
|
||||
{
|
||||
requireActivity().finish()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog alerting the user that internet connection is required for upload process
|
||||
* Recalls UploadMediaPresenter.getImageQuality for all the next upload items,
|
||||
* if there is network connectivity and then the user presses okay
|
||||
*/
|
||||
override fun showConnectionErrorPopup() {
|
||||
try {
|
||||
val FLAG_ALERT_DIALOG_SHOWING = basicKvStore!!.getBoolean(
|
||||
keyForShowingAlertDialog, false
|
||||
)
|
||||
if (!FLAG_ALERT_DIALOG_SHOWING) {
|
||||
basicKvStore!!.putBoolean(keyForShowingAlertDialog, true)
|
||||
showAlertDialog(
|
||||
requireActivity(),
|
||||
getString(R.string.upload_connection_error_alert_title),
|
||||
getString(R.string.upload_connection_error_alert_detail),
|
||||
getString(R.string.ok),
|
||||
getString(R.string.cancel_upload),
|
||||
{
|
||||
basicKvStore!!.putBoolean(keyForShowingAlertDialog, false)
|
||||
if (isInternetConnectionEstablished(requireActivity())) {
|
||||
val sizeOfUploads = basicKvStore!!.getInt(
|
||||
UploadActivity.keyForCurrentUploadImagesSize
|
||||
)
|
||||
for (i in indexOfFragment until sizeOfUploads) {
|
||||
presenter.getImageQuality(
|
||||
i,
|
||||
inAppPictureLocation,
|
||||
requireActivity()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
showConnectionErrorPopup()
|
||||
}
|
||||
},
|
||||
{
|
||||
basicKvStore!!.putBoolean(keyForShowingAlertDialog, false)
|
||||
requireActivity().finish()
|
||||
},
|
||||
null
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showExternalMap(uploadItem: UploadItem) =
|
||||
goToLocationPickerActivity(uploadItem)
|
||||
|
||||
/**
|
||||
* Launches the image editing activity to edit the specified UploadItem.
|
||||
*
|
||||
* @param uploadItem The UploadItem to be edited.
|
||||
*
|
||||
* This method is called to start the image editing activity for a specific UploadItem.
|
||||
* It sets the UploadItem as the currently editable item, creates an intent to launch the
|
||||
* EditActivity, and passes the image file path as an extra in the intent. The activity
|
||||
* is started using resultLauncher that handles the result in respective callback.
|
||||
*/
|
||||
override fun showEditActivity(uploadItem: UploadItem) {
|
||||
editableUploadItem = uploadItem
|
||||
val intent = Intent(context, EditActivity::class.java)
|
||||
intent.putExtra("image", uploadableFile!!.getFilePath().toString())
|
||||
startForEditActivityResult.launch(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Location picker activity. Show the location first then user can modify it by clicking
|
||||
* modify location button.
|
||||
* @param uploadItem current upload item
|
||||
*/
|
||||
private fun goToLocationPickerActivity(uploadItem: UploadItem) {
|
||||
editableUploadItem = uploadItem
|
||||
var defaultLatitude = 37.773972
|
||||
var defaultLongitude = -122.431297
|
||||
var defaultZoom = 16.0
|
||||
|
||||
val locationPickerIntent: Intent
|
||||
|
||||
/* Retrieve image location from EXIF if present or
|
||||
check if user has provided location while using the in-app camera.
|
||||
Use location of last UploadItem if none of them is available */
|
||||
if (uploadItem.gpsCoords != null && uploadItem.gpsCoords!!
|
||||
.decLatitude != 0.0 && uploadItem.gpsCoords!!.decLongitude != 0.0
|
||||
) {
|
||||
defaultLatitude = uploadItem.gpsCoords!!
|
||||
.decLatitude
|
||||
defaultLongitude = uploadItem.gpsCoords!!.decLongitude
|
||||
defaultZoom = uploadItem.gpsCoords!!.zoomLevel
|
||||
|
||||
locationPickerIntent = LocationPicker.IntentBuilder()
|
||||
.defaultLocation(CameraPosition(defaultLatitude, defaultLongitude, defaultZoom))
|
||||
.activityKey("UploadActivity")
|
||||
.build(requireActivity())
|
||||
} else {
|
||||
if (defaultKvStore.getString(LAST_LOCATION) != null) {
|
||||
val locationLatLng = defaultKvStore.getString(LAST_LOCATION)!!
|
||||
.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
defaultLatitude = locationLatLng[0].toDouble()
|
||||
defaultLongitude = locationLatLng[1].toDouble()
|
||||
}
|
||||
if (defaultKvStore.getString(LAST_ZOOM) != null) {
|
||||
defaultZoom = defaultKvStore.getString(LAST_ZOOM)!!
|
||||
.toDouble()
|
||||
}
|
||||
|
||||
locationPickerIntent = LocationPicker.IntentBuilder()
|
||||
.defaultLocation(CameraPosition(defaultLatitude, defaultLongitude, defaultZoom))
|
||||
.activityKey("NoLocationUploadActivity")
|
||||
.build(requireActivity())
|
||||
}
|
||||
startForResult.launch(locationPickerIntent)
|
||||
}
|
||||
|
||||
private fun onCameraPosition(result: ActivityResult) {
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
checkNotNull(result.data)
|
||||
val cameraPosition = getCameraPosition(
|
||||
result.data!!
|
||||
)
|
||||
|
||||
if (cameraPosition != null) {
|
||||
val latitude = cameraPosition.latitude.toString()
|
||||
val longitude = cameraPosition.longitude.toString()
|
||||
val zoom = cameraPosition.zoom
|
||||
|
||||
editLocation(latitude, longitude, zoom)
|
||||
// If isMissingLocationDialog is true, it means that the user has already tapped the
|
||||
// "Next" button, so go directly to the next step.
|
||||
if (isMissingLocationDialog) {
|
||||
isMissingLocationDialog = false
|
||||
presenter.displayLocDialog(
|
||||
indexOfFragment,
|
||||
inAppPictureLocation,
|
||||
hasUserRemovedLocation
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// If camera position is null means location is removed by the user
|
||||
removeLocation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onVoiceInput(result: ActivityResult) {
|
||||
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
|
||||
val resultData = result.data!!.getStringArrayListExtra(
|
||||
RecognizerIntent.EXTRA_RESULTS
|
||||
)
|
||||
uploadMediaDetailAdapter.handleSpeechResult(resultData!![0])
|
||||
} else {
|
||||
Timber.e("Error %s", result.resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEditActivityResult(result: ActivityResult) {
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val path = result.data!!.getStringExtra("editedImageFilePath")
|
||||
|
||||
if (Objects.equals(result, "Error")) {
|
||||
Timber.e("Error in rotating image")
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (_binding != null) {
|
||||
binding.backgroundImage.setImageURI(Uri.fromFile(File(path!!)))
|
||||
}
|
||||
editableUploadItem!!.setContentAndMediaUri(Uri.fromFile(File(path!!)))
|
||||
fragmentCallback!!.changeThumbnail(
|
||||
indexOfFragment,
|
||||
path
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the location data from the image, by setting them to null
|
||||
*/
|
||||
private fun removeLocation() {
|
||||
editableUploadItem!!.gpsCoords!!.decimalCoords = null
|
||||
try {
|
||||
val sourceExif = ExifInterface(
|
||||
uploadableFile!!.getFilePath()
|
||||
)
|
||||
val exifTags = arrayOf(
|
||||
ExifInterface.TAG_GPS_LATITUDE,
|
||||
ExifInterface.TAG_GPS_LATITUDE_REF,
|
||||
ExifInterface.TAG_GPS_LONGITUDE,
|
||||
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
||||
)
|
||||
|
||||
for (tag in exifTags) {
|
||||
sourceExif.setAttribute(tag, null)
|
||||
}
|
||||
sourceExif.saveAttributes()
|
||||
|
||||
val mapQuestion =
|
||||
ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_not_available_20dp)
|
||||
|
||||
if (_binding != null) {
|
||||
binding.locationImageView.setImageDrawable(mapQuestion)
|
||||
binding.locationTextView.setText(R.string.add_location)
|
||||
}
|
||||
|
||||
editableUploadItem!!.gpsCoords!!.decLatitude = 0.0
|
||||
editableUploadItem!!.gpsCoords!!.decLongitude = 0.0
|
||||
editableUploadItem!!.gpsCoords!!.imageCoordsExists = false
|
||||
hasUserRemovedLocation = true
|
||||
|
||||
Toast.makeText(context, getString(R.string.location_removed), Toast.LENGTH_LONG)
|
||||
.show()
|
||||
} catch (e: Exception) {
|
||||
Timber.d(e)
|
||||
Toast.makeText(
|
||||
context, "Location could not be removed due to internal error",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the old coordinates with new one
|
||||
* @param latitude new latitude
|
||||
* @param longitude new longitude
|
||||
*/
|
||||
fun editLocation(latitude: String, longitude: String, zoom: Double) {
|
||||
editableUploadItem!!.gpsCoords!!.decLatitude = latitude.toDouble()
|
||||
editableUploadItem!!.gpsCoords!!.decLongitude = longitude.toDouble()
|
||||
editableUploadItem!!.gpsCoords!!.decimalCoords = "$latitude|$longitude"
|
||||
editableUploadItem!!.gpsCoords!!.imageCoordsExists = true
|
||||
editableUploadItem!!.gpsCoords!!.zoomLevel = zoom
|
||||
|
||||
// Replace the map icon using the one with a green tick
|
||||
val mapTick = ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_available_20dp)
|
||||
|
||||
if (_binding != null) {
|
||||
binding.locationImageView.setImageDrawable(mapTick)
|
||||
binding.locationTextView.setText(R.string.edit_location)
|
||||
}
|
||||
|
||||
Toast.makeText(context, getString(R.string.location_updated), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun updateMediaDetails(uploadMediaDetails: List<UploadMediaDetail>) {
|
||||
uploadMediaDetailAdapter.items = uploadMediaDetails
|
||||
showNearbyFound =
|
||||
showNearbyFound && (uploadMediaDetails.isEmpty() || listContainsEmptyDetails(
|
||||
uploadMediaDetails
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* if the media details that come in here are empty
|
||||
* (empty caption AND empty description, with caption being the decider here)
|
||||
* this method allows usage of nearby place caption and description if any
|
||||
* else it takes the media details saved in prior for this picture
|
||||
* @param uploadMediaDetails saved media details,
|
||||
* ex: in case when "copy to subsequent media" button is clicked
|
||||
* for a previous image
|
||||
* @return boolean whether the details are empty or not
|
||||
*/
|
||||
private fun listContainsEmptyDetails(uploadMediaDetails: List<UploadMediaDetail>): Boolean {
|
||||
for ((_, descriptionText, captionText) in uploadMediaDetails) {
|
||||
if (!TextUtils.isEmpty(captionText) && !TextUtils.isEmpty(
|
||||
descriptionText
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Showing dialog for adding location
|
||||
*
|
||||
* @param runnable proceed for verifying image quality
|
||||
*/
|
||||
override fun displayAddLocationDialog(runnable: Runnable) {
|
||||
isMissingLocationDialog = true
|
||||
showAlertDialog(
|
||||
requireActivity(),
|
||||
getString(R.string.no_location_found_title),
|
||||
getString(R.string.no_location_found_message),
|
||||
getString(R.string.add_location),
|
||||
getString(R.string.skip_login),
|
||||
{
|
||||
presenter.onMapIconClicked(indexOfFragment)
|
||||
},
|
||||
runnable
|
||||
)
|
||||
}
|
||||
|
||||
override fun showBadImagePopup(errorCode: Int, index: Int, uploadItem: UploadItem) {
|
||||
//If the error message is null, we will probably not show anything
|
||||
val activity = requireActivity()
|
||||
val errorMessageForResult = getErrorMessageForResult(activity, errorCode)
|
||||
if (errorMessageForResult.isNotEmpty()) {
|
||||
showAlertDialog(
|
||||
activity,
|
||||
activity.getString(R.string.upload_problem_image),
|
||||
errorMessageForResult,
|
||||
activity.getString(R.string.upload),
|
||||
activity.getString(R.string.cancel),
|
||||
{
|
||||
showProgress(false)
|
||||
uploadItem.imageQuality = IMAGE_OK
|
||||
},
|
||||
{
|
||||
presenterCallback!!.deletePictureAtIndex(index)
|
||||
}
|
||||
)?.setCancelable(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
presenter.onDetachView()
|
||||
}
|
||||
|
||||
fun expandCollapseLlMediaDetail(shouldExpand: Boolean) {
|
||||
if (_binding == null) {
|
||||
return
|
||||
}
|
||||
binding.llContainerMediaDetail.visibility =
|
||||
if (shouldExpand) View.VISIBLE else View.GONE
|
||||
isExpanded = !isExpanded
|
||||
binding.ibExpandCollapse.rotation = binding.ibExpandCollapse.rotation + 180
|
||||
}
|
||||
|
||||
override fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) {
|
||||
if (_binding == null) {
|
||||
return
|
||||
}
|
||||
binding.btnCopySubsequentMedia.isEnabled = isNotEmpty
|
||||
binding.btnCopySubsequentMedia.isClickable = isNotEmpty
|
||||
binding.btnCopySubsequentMedia.alpha = if (isNotEmpty) 1.0f else 0.5f
|
||||
binding.btnNext.isEnabled = isNotEmpty
|
||||
binding.btnNext.isClickable = isNotEmpty
|
||||
binding.btnNext.alpha = if (isNotEmpty) 1.0f else 0.5f
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new language item to RecyclerView
|
||||
*/
|
||||
override fun addLanguage() {
|
||||
val uploadMediaDetail = UploadMediaDetail()
|
||||
uploadMediaDetail.isManuallyAdded = true //This was manually added by the user
|
||||
uploadMediaDetailAdapter.addDescription(uploadMediaDetail)
|
||||
binding.rvDescriptions.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1)
|
||||
}
|
||||
|
||||
fun onButtonCopyTitleDescToSubsequentMedia() {
|
||||
presenter.copyTitleAndDescriptionToSubsequentMedia(indexOfFragment)
|
||||
Toast.makeText(context, R.string.copied_successfully, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
if (uploadableFile != null) {
|
||||
outState.putParcelable(UPLOADABLE_FILE, uploadableFile)
|
||||
}
|
||||
outState.putParcelableArrayList(
|
||||
UPLOAD_MEDIA_DETAILS,
|
||||
ArrayList(uploadMediaDetailAdapter.items)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
interface UploadMediaDetailFragmentCallback : Callback {
|
||||
fun deletePictureAtIndex(index: Int)
|
||||
|
||||
fun changeThumbnail(index: Int, uri: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* A key for applicationKvStore. By this key we can retrieve the location of last UploadItem ex.
|
||||
* 12.3433,54.78897 from applicationKvStore.
|
||||
*/
|
||||
const val LAST_LOCATION: String = "last_location_while_uploading"
|
||||
const val LAST_ZOOM: String = "last_zoom_level_while_uploading"
|
||||
const val UPLOADABLE_FILE: String = "uploadable_file"
|
||||
const val UPLOAD_MEDIA_DETAILS: String = "upload_media_detail_adapter"
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.upload.mediaDetails
|
|||
import android.app.Activity
|
||||
import fr.free.nrw.commons.BasePresenter
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile
|
||||
import fr.free.nrw.commons.kvstore.BasicKvStore
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
import fr.free.nrw.commons.nearby.Place
|
||||
import fr.free.nrw.commons.upload.ImageCoordinates
|
||||
|
|
@ -15,9 +16,9 @@ import fr.free.nrw.commons.upload.UploadMediaDetail
|
|||
*/
|
||||
interface UploadMediaDetailsContract {
|
||||
interface View : SimilarImageInterface {
|
||||
fun onImageProcessed(uploadItem: UploadItem?, place: Place?)
|
||||
fun onImageProcessed(uploadItem: UploadItem)
|
||||
|
||||
fun onNearbyPlaceFound(uploadItem: UploadItem?, place: Place?)
|
||||
fun onNearbyPlaceFound(uploadItem: UploadItem, place: Place?)
|
||||
|
||||
fun showProgress(shouldShow: Boolean)
|
||||
|
||||
|
|
@ -25,9 +26,9 @@ interface UploadMediaDetailsContract {
|
|||
|
||||
fun showMessage(stringResourceId: Int, colorResourceId: Int)
|
||||
|
||||
fun showMessage(message: String?, colorResourceId: Int)
|
||||
fun showMessage(message: String, colorResourceId: Int)
|
||||
|
||||
fun showDuplicatePicturePopup(uploadItem: UploadItem?)
|
||||
fun showDuplicatePicturePopup(uploadItem: UploadItem)
|
||||
|
||||
/**
|
||||
* Shows a dialog alerting the user that internet connection is required for upload process
|
||||
|
|
@ -42,16 +43,20 @@ interface UploadMediaDetailsContract {
|
|||
*/
|
||||
fun showConnectionErrorPopupForCaptionCheck()
|
||||
|
||||
fun showExternalMap(uploadItem: UploadItem?)
|
||||
fun showExternalMap(uploadItem: UploadItem)
|
||||
|
||||
fun showEditActivity(uploadItem: UploadItem?)
|
||||
fun showEditActivity(uploadItem: UploadItem)
|
||||
|
||||
fun updateMediaDetails(uploadMediaDetails: List<UploadMediaDetail?>?)
|
||||
fun updateMediaDetails(uploadMediaDetails: List<UploadMediaDetail>)
|
||||
|
||||
fun displayAddLocationDialog(runnable: Runnable?)
|
||||
fun displayAddLocationDialog(runnable: Runnable)
|
||||
|
||||
fun showBadImagePopup(errorCode: Int, index: Int, uploadItem: UploadItem)
|
||||
}
|
||||
|
||||
interface UserActionListener : BasePresenter<View?> {
|
||||
fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore)
|
||||
|
||||
fun receiveImage(
|
||||
uploadableFile: UploadableFile?,
|
||||
place: Place?,
|
||||
|
|
@ -59,7 +64,7 @@ interface UploadMediaDetailsContract {
|
|||
)
|
||||
|
||||
fun setUploadMediaDetails(
|
||||
uploadMediaDetails: List<UploadMediaDetail?>?,
|
||||
uploadMediaDetails: List<UploadMediaDetail>,
|
||||
uploadItemIndex: Int
|
||||
)
|
||||
|
||||
|
|
@ -74,7 +79,7 @@ interface UploadMediaDetailsContract {
|
|||
fun getImageQuality(
|
||||
uploadItemIndex: Int,
|
||||
inAppPictureLocation: LatLng?,
|
||||
activity: Activity?
|
||||
activity: Activity
|
||||
): Boolean
|
||||
|
||||
/**
|
||||
|
|
@ -87,7 +92,8 @@ interface UploadMediaDetailsContract {
|
|||
* @param hasUserRemovedLocation True if user has removed location from the image
|
||||
*/
|
||||
fun displayLocDialog(
|
||||
uploadItemIndex: Int, inAppPictureLocation: LatLng?,
|
||||
uploadItemIndex: Int,
|
||||
inAppPictureLocation: LatLng?,
|
||||
hasUserRemovedLocation: Boolean
|
||||
)
|
||||
|
||||
|
|
@ -97,7 +103,7 @@ interface UploadMediaDetailsContract {
|
|||
* @param uploadItem UploadItem whose quality is to be checked
|
||||
* @param index Index of the UploadItem whose quality is to be checked
|
||||
*/
|
||||
fun checkImageQuality(uploadItem: UploadItem?, index: Int)
|
||||
fun checkImageQuality(uploadItem: UploadItem, index: Int)
|
||||
|
||||
/**
|
||||
* Updates the image qualities stored in JSON, whenever an image is deleted
|
||||
|
|
@ -111,7 +117,7 @@ interface UploadMediaDetailsContract {
|
|||
|
||||
fun fetchTitleAndDescription(indexInViewFlipper: Int)
|
||||
|
||||
fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates?, uploadItemIndex: Int)
|
||||
fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates, uploadItemIndex: Int)
|
||||
|
||||
fun onMapIconClicked(indexInViewFlipper: Int)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,547 +0,0 @@
|
|||
package fr.free.nrw.commons.upload.mediaDetails;
|
||||
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
|
||||
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.activity;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
|
||||
|
||||
import android.app.Activity;
|
||||
import androidx.annotation.Nullable;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
||||
import fr.free.nrw.commons.kvstore.BasicKvStore;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.repository.UploadRepository;
|
||||
import fr.free.nrw.commons.upload.ImageCoordinates;
|
||||
import fr.free.nrw.commons.upload.SimilarImageInterface;
|
||||
import fr.free.nrw.commons.upload.UploadActivity;
|
||||
import fr.free.nrw.commons.upload.UploadItem;
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetail;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.UserActionListener;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.View;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import io.github.coordinates2country.Coordinates2Country;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.json.JSONObject;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class UploadMediaPresenter implements UserActionListener, SimilarImageInterface {
|
||||
|
||||
private static final UploadMediaDetailsContract.View DUMMY = (UploadMediaDetailsContract.View) Proxy
|
||||
.newProxyInstance(
|
||||
UploadMediaDetailsContract.View.class.getClassLoader(),
|
||||
new Class[]{UploadMediaDetailsContract.View.class},
|
||||
(proxy, method, methodArgs) -> null);
|
||||
|
||||
private final UploadRepository repository;
|
||||
private UploadMediaDetailsContract.View view = DUMMY;
|
||||
|
||||
private CompositeDisposable compositeDisposable;
|
||||
|
||||
private final JsonKvStore defaultKVStore;
|
||||
private Scheduler ioScheduler;
|
||||
private Scheduler mainThreadScheduler;
|
||||
|
||||
public static UploadMediaDetailFragmentCallback presenterCallback ;
|
||||
|
||||
private final List<String> WLM_SUPPORTED_COUNTRIES= Arrays.asList("am","at","az","br","hr","sv","fi","fr","de","gh","in","ie","il","mk","my","mt","pk","pe","pl","ru","rw","si","es","se","tw","ug","ua","us");
|
||||
private Map<String, String> countryNamesAndCodes = null;
|
||||
|
||||
private final String keyForCurrentUploadImageQualities = "UploadedImagesQualities";
|
||||
|
||||
/**
|
||||
* Variable used to determine if the battery-optimisation dialog is being shown or not
|
||||
*/
|
||||
public static boolean isBatteryDialogShowing;
|
||||
|
||||
public static boolean isCategoriesDialogShowing;
|
||||
|
||||
@Inject
|
||||
public UploadMediaPresenter(final UploadRepository uploadRepository,
|
||||
@Named("default_preferences") final JsonKvStore defaultKVStore,
|
||||
@Named(IO_THREAD) final Scheduler ioScheduler,
|
||||
@Named(MAIN_THREAD) final Scheduler mainThreadScheduler) {
|
||||
this.repository = uploadRepository;
|
||||
this.defaultKVStore = defaultKVStore;
|
||||
this.ioScheduler = ioScheduler;
|
||||
this.mainThreadScheduler = mainThreadScheduler;
|
||||
compositeDisposable = new CompositeDisposable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachView(final View view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachView() {
|
||||
this.view = DUMMY;
|
||||
compositeDisposable.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Upload Media Details for the corresponding upload item
|
||||
*/
|
||||
@Override
|
||||
public void setUploadMediaDetails(final List<UploadMediaDetail> uploadMediaDetails, final int uploadItemIndex) {
|
||||
repository.getUploads().get(uploadItemIndex).setUploadMediaDetails(uploadMediaDetails);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives the corresponding uploadable file, processes it and return the view with and uplaod item
|
||||
*/
|
||||
@Override
|
||||
public void receiveImage(final UploadableFile uploadableFile, final Place place,
|
||||
final LatLng inAppPictureLocation) {
|
||||
view.showProgress(true);
|
||||
compositeDisposable.add(
|
||||
repository
|
||||
.preProcessImage(uploadableFile, place, this, inAppPictureLocation)
|
||||
.map(uploadItem -> {
|
||||
if(place!=null && place.isMonument()){
|
||||
if (place.location != null) {
|
||||
final String countryCode = reverseGeoCode(place.location);
|
||||
if (countryCode != null && WLM_SUPPORTED_COUNTRIES
|
||||
.contains(countryCode.toLowerCase(Locale.ROOT))) {
|
||||
uploadItem.setWLMUpload(true);
|
||||
uploadItem.setCountryCode(countryCode.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
}
|
||||
return uploadItem;
|
||||
})
|
||||
.subscribeOn(ioScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe(uploadItem ->
|
||||
{
|
||||
view.onImageProcessed(uploadItem, place);
|
||||
view.updateMediaDetails(uploadItem.getUploadMediaDetails());
|
||||
view.showProgress(false);
|
||||
final ImageCoordinates gpsCoords = uploadItem.getGpsCoords();
|
||||
final boolean hasImageCoordinates =
|
||||
gpsCoords != null && gpsCoords.getImageCoordsExists();
|
||||
if (hasImageCoordinates && place == null) {
|
||||
checkNearbyPlaces(uploadItem);
|
||||
}
|
||||
},
|
||||
throwable -> Timber.e(throwable, "Error occurred in processing images")));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String reverseGeoCode(final LatLng latLng){
|
||||
if(countryNamesAndCodes == null){
|
||||
countryNamesAndCodes = getCountryNamesAndCodes();
|
||||
}
|
||||
return countryNamesAndCodes.get(Coordinates2Country.country(latLng.getLatitude(), latLng.getLongitude()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates HashMap containing all ISO countries 2-letter codes provided by <code>Locale.getISOCountries()</code>
|
||||
* and their english names
|
||||
*
|
||||
* @return HashMap where Key is country english name and Value is 2-letter country code
|
||||
* e.g. ["Germany":"DE", ...]
|
||||
*/
|
||||
private Map<String, String> getCountryNamesAndCodes(){
|
||||
final Map<String, String> result = new HashMap<>();
|
||||
|
||||
final String[] isoCountries = Locale.getISOCountries();
|
||||
|
||||
for (final String isoCountry : isoCountries) {
|
||||
result.put(
|
||||
new Locale("en", isoCountry).getDisplayCountry(Locale.ENGLISH),
|
||||
isoCountry
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks for the nearest location that needs images and suggests it to the user.
|
||||
*/
|
||||
private void checkNearbyPlaces(final UploadItem uploadItem) {
|
||||
final Disposable checkNearbyPlaces = Maybe.fromCallable(() -> repository
|
||||
.checkNearbyPlaces(uploadItem.getGpsCoords().getDecLatitude(),
|
||||
uploadItem.getGpsCoords().getDecLongitude()))
|
||||
.subscribeOn(ioScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe(place -> {
|
||||
if (place != null) {
|
||||
view.onNearbyPlaceFound(uploadItem, place);
|
||||
}
|
||||
},
|
||||
throwable -> Timber.e(throwable, "Error occurred in processing images"));
|
||||
compositeDisposable.add(checkNearbyPlaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the image has a location. Displays a dialog alerting user that no
|
||||
* location has been to added to the image and asking them to add one, if location was not
|
||||
* removed by the user
|
||||
*
|
||||
* @param uploadItemIndex Index of the uploadItem which has no location
|
||||
* @param inAppPictureLocation In app picture location (if any)
|
||||
* @param hasUserRemovedLocation True if user has removed location from the image
|
||||
*/
|
||||
@Override
|
||||
public void displayLocDialog(final int uploadItemIndex, final LatLng inAppPictureLocation,
|
||||
final boolean hasUserRemovedLocation) {
|
||||
final List<UploadItem> uploadItems = repository.getUploads();
|
||||
final UploadItem uploadItem = uploadItems.get(uploadItemIndex);
|
||||
if (uploadItem.getGpsCoords().getDecimalCoords() == null && inAppPictureLocation == null
|
||||
&& !hasUserRemovedLocation) {
|
||||
final Runnable onSkipClicked = () -> {
|
||||
verifyCaptionQuality(uploadItem);
|
||||
};
|
||||
view.displayAddLocationDialog(onSkipClicked);
|
||||
} else {
|
||||
verifyCaptionQuality(uploadItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the image's caption and calls function to handle the result
|
||||
*
|
||||
* @param uploadItem UploadItem whose caption is checked
|
||||
*/
|
||||
private void verifyCaptionQuality(final UploadItem uploadItem) {
|
||||
view.showProgress(true);
|
||||
compositeDisposable.add(
|
||||
repository
|
||||
.getCaptionQuality(uploadItem)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe(capResult -> {
|
||||
view.showProgress(false);
|
||||
handleCaptionResult(capResult, uploadItem);
|
||||
},
|
||||
throwable -> {
|
||||
view.showProgress(false);
|
||||
if (throwable instanceof UnknownHostException) {
|
||||
view.showConnectionErrorPopupForCaptionCheck();
|
||||
} else {
|
||||
view.showMessage("" + throwable.getLocalizedMessage(),
|
||||
R.color.color_error);
|
||||
}
|
||||
Timber.e(throwable, "Error occurred while handling image");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles image's caption results and shows dialog if necessary
|
||||
*
|
||||
* @param errorCode Error code of the UploadItem
|
||||
* @param uploadItem UploadItem whose caption is checked
|
||||
*/
|
||||
public void handleCaptionResult(final Integer errorCode, final UploadItem uploadItem) {
|
||||
// If errorCode is empty caption show message
|
||||
if (errorCode == EMPTY_CAPTION) {
|
||||
Timber.d("Captions are empty. Showing toast");
|
||||
view.showMessage(R.string.add_caption_toast, R.color.color_error);
|
||||
}
|
||||
|
||||
// If image with same file name exists check the bit in errorCode is set or not
|
||||
if ((errorCode & FILE_NAME_EXISTS) != 0) {
|
||||
Timber.d("Trying to show duplicate picture popup");
|
||||
view.showDuplicatePicturePopup(uploadItem);
|
||||
}
|
||||
|
||||
// If caption is not duplicate or user still wants to upload it
|
||||
if (errorCode == IMAGE_OK) {
|
||||
Timber.d("Image captions are okay or user still wants to upload it");
|
||||
view.onImageValidationSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Copies the caption and description of the current item to the subsequent media
|
||||
*/
|
||||
@Override
|
||||
public void copyTitleAndDescriptionToSubsequentMedia(final int indexInViewFlipper) {
|
||||
for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){
|
||||
final UploadItem subsequentUploadItem = repository.getUploads().get(i);
|
||||
subsequentUploadItem.setUploadMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and set the caption and description of the item
|
||||
*/
|
||||
@Override
|
||||
public void fetchTitleAndDescription(final int indexInViewFlipper) {
|
||||
final UploadItem currentUploadItem = repository.getUploads().get(indexInViewFlipper);
|
||||
view.updateMediaDetails(currentUploadItem.getUploadMediaDetails());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<UploadMediaDetail> deepCopy(final List<UploadMediaDetail> uploadMediaDetails) {
|
||||
final ArrayList<UploadMediaDetail> newList = new ArrayList<>();
|
||||
for (final UploadMediaDetail uploadMediaDetail : uploadMediaDetails) {
|
||||
newList.add(uploadMediaDetail.javaCopy());
|
||||
}
|
||||
return newList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void useSimilarPictureCoordinates(final ImageCoordinates imageCoordinates, final int uploadItemIndex) {
|
||||
repository.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMapIconClicked(final int indexInViewFlipper) {
|
||||
view.showExternalMap(repository.getUploads().get(indexInViewFlipper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEditButtonClicked(final int indexInViewFlipper){
|
||||
view.showEditActivity(repository.getUploads().get(indexInViewFlipper));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the information regarding the specified place for the specified upload item
|
||||
* when the user confirms the suggested nearby place.
|
||||
*
|
||||
* @param place The place to be associated with the uploads.
|
||||
* @param uploadItemIndex Index of the uploadItem whose detected place has been confirmed
|
||||
*/
|
||||
@Override
|
||||
public void onUserConfirmedUploadIsOfPlace(final Place place, final int uploadItemIndex) {
|
||||
final UploadItem uploadItem = repository.getUploads().get(uploadItemIndex);
|
||||
|
||||
uploadItem.setPlace(place);
|
||||
final List<UploadMediaDetail> uploadMediaDetails = uploadItem.getUploadMediaDetails();
|
||||
// Update UploadMediaDetail object for this UploadItem
|
||||
uploadMediaDetails.set(0, new UploadMediaDetail(place));
|
||||
|
||||
// Now that the UploadItem and its associated UploadMediaDetail objects have been updated,
|
||||
// update the view with the modified media details of the first upload item
|
||||
view.updateMediaDetails(uploadMediaDetails);
|
||||
UploadActivity.setUploadIsOfAPlace(true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the image quality
|
||||
*
|
||||
* @param uploadItemIndex Index of the UploadItem whose quality is to be checked
|
||||
* @param inAppPictureLocation In app picture location (if any)
|
||||
* @param activity Context reference
|
||||
* @return true if no internal error occurs, else returns false
|
||||
*/
|
||||
@Override
|
||||
public boolean getImageQuality(final int uploadItemIndex, final LatLng inAppPictureLocation,
|
||||
final Activity activity) {
|
||||
final List<UploadItem> uploadItems = repository.getUploads();
|
||||
view.showProgress(true);
|
||||
if (uploadItems.isEmpty()) {
|
||||
view.showProgress(false);
|
||||
// No internationalization required for this error message because it's an internal error.
|
||||
view.showMessage(
|
||||
"Internal error: Zero upload items received by the Upload Media Detail Fragment. Sorry, please upload again.",
|
||||
R.color.color_error);
|
||||
return false;
|
||||
}
|
||||
final UploadItem uploadItem = uploadItems.get(uploadItemIndex);
|
||||
compositeDisposable.add(
|
||||
repository
|
||||
.getImageQuality(uploadItem, inAppPictureLocation)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe(imageResult -> {
|
||||
storeImageQuality(imageResult, uploadItemIndex, activity, uploadItem);
|
||||
},
|
||||
throwable -> {
|
||||
if (throwable instanceof UnknownHostException) {
|
||||
view.showProgress(false);
|
||||
view.showConnectionErrorPopup();
|
||||
} else {
|
||||
view.showMessage("" + throwable.getLocalizedMessage(),
|
||||
R.color.color_error);
|
||||
}
|
||||
Timber.e(throwable, "Error occurred while handling image");
|
||||
})
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the image quality in JSON format in SharedPrefs
|
||||
*
|
||||
* @param imageResult Image quality
|
||||
* @param uploadItemIndex Index of the UploadItem whose quality is calculated
|
||||
* @param activity Context reference
|
||||
* @param uploadItem UploadItem whose quality is to be checked
|
||||
*/
|
||||
private void storeImageQuality(final Integer imageResult, final int uploadItemIndex, final Activity activity,
|
||||
final UploadItem uploadItem) {
|
||||
final BasicKvStore store = new BasicKvStore(activity,
|
||||
UploadActivity.storeNameForCurrentUploadImagesSize);
|
||||
final String value = store.getString(keyForCurrentUploadImageQualities, null);
|
||||
final JSONObject jsonObject;
|
||||
try {
|
||||
if (value != null) {
|
||||
jsonObject = new JSONObject(value);
|
||||
} else {
|
||||
jsonObject = new JSONObject();
|
||||
}
|
||||
jsonObject.put("UploadItem" + uploadItemIndex, imageResult);
|
||||
store.putString(keyForCurrentUploadImageQualities, jsonObject.toString());
|
||||
} catch (final Exception e) {
|
||||
Timber.e(e);
|
||||
}
|
||||
|
||||
if (uploadItemIndex == 0) {
|
||||
if (!isBatteryDialogShowing && !isCategoriesDialogShowing) {
|
||||
// if battery-optimisation dialog is not being shown, call checkImageQuality
|
||||
checkImageQuality(uploadItem, uploadItemIndex);
|
||||
} else {
|
||||
view.showProgress(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check image quality from stored qualities and display dialogs
|
||||
*
|
||||
* @param uploadItem UploadItem whose quality is to be checked
|
||||
* @param index Index of the UploadItem whose quality is to be checked
|
||||
*/
|
||||
@Override
|
||||
public void checkImageQuality(final UploadItem uploadItem, final int index) {
|
||||
if ((uploadItem.getImageQuality() != IMAGE_OK) && (uploadItem.getImageQuality()
|
||||
!= IMAGE_KEEP)) {
|
||||
final BasicKvStore store = new BasicKvStore(activity,
|
||||
UploadActivity.storeNameForCurrentUploadImagesSize);
|
||||
final String value = store.getString(keyForCurrentUploadImageQualities, null);
|
||||
final JSONObject jsonObject;
|
||||
try {
|
||||
if (value != null) {
|
||||
jsonObject = new JSONObject(value);
|
||||
} else {
|
||||
jsonObject = new JSONObject();
|
||||
}
|
||||
final Integer imageQuality = (int) jsonObject.get("UploadItem" + index);
|
||||
view.showProgress(false);
|
||||
if (imageQuality == IMAGE_OK) {
|
||||
uploadItem.setHasInvalidLocation(false);
|
||||
uploadItem.setImageQuality(imageQuality);
|
||||
} else {
|
||||
handleBadImage(imageQuality, uploadItem, index);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the image qualities stored in JSON, whenever an image is deleted
|
||||
*
|
||||
* @param size Size of uploadableFiles
|
||||
* @param index Index of the UploadItem which was deleted
|
||||
*/
|
||||
@Override
|
||||
public void updateImageQualitiesJSON(final int size, final int index) {
|
||||
final BasicKvStore store = new BasicKvStore(activity,
|
||||
UploadActivity.storeNameForCurrentUploadImagesSize);
|
||||
final String value = store.getString(keyForCurrentUploadImageQualities, null);
|
||||
final JSONObject jsonObject;
|
||||
try {
|
||||
if (value != null) {
|
||||
jsonObject = new JSONObject(value);
|
||||
} else {
|
||||
jsonObject = new JSONObject();
|
||||
}
|
||||
for (int i = index; i < (size - 1); i++) {
|
||||
jsonObject.put("UploadItem" + i, jsonObject.get("UploadItem" + (i + 1)));
|
||||
}
|
||||
jsonObject.remove("UploadItem" + (size - 1));
|
||||
store.putString(keyForCurrentUploadImageQualities, jsonObject.toString());
|
||||
} catch (final Exception e) {
|
||||
Timber.e(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles bad pictures, like too dark, already on wikimedia, downloaded from internet
|
||||
*
|
||||
* @param errorCode Error code of the bad image quality
|
||||
* @param uploadItem UploadItem whose quality is bad
|
||||
* @param index Index of item whose quality is bad
|
||||
*/
|
||||
public void handleBadImage(final Integer errorCode,
|
||||
final UploadItem uploadItem, final int index) {
|
||||
Timber.d("Handle bad picture with error code %d", errorCode);
|
||||
if (errorCode >= 8) { // If location of image and nearby does not match
|
||||
uploadItem.setHasInvalidLocation(true);
|
||||
}
|
||||
|
||||
// If image has some other problems, show popup accordingly
|
||||
if (errorCode != EMPTY_CAPTION && errorCode != FILE_NAME_EXISTS) {
|
||||
showBadImagePopup(errorCode, index, activity, uploadItem);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog describing the potential problems in the current image
|
||||
*
|
||||
* @param errorCode Has the potential problems in the current image
|
||||
* @param index Index of the UploadItem which has problems
|
||||
* @param activity Context reference
|
||||
* @param uploadItem UploadItem which has problems
|
||||
*/
|
||||
public void showBadImagePopup(final Integer errorCode,
|
||||
final int index, final Activity activity, final UploadItem uploadItem) {
|
||||
final String errorMessageForResult = getErrorMessageForResult(activity, errorCode);
|
||||
if (!StringUtils.isBlank(errorMessageForResult)) {
|
||||
DialogUtil.showAlertDialog(activity,
|
||||
activity.getString(R.string.upload_problem_image),
|
||||
errorMessageForResult,
|
||||
activity.getString(R.string.upload),
|
||||
activity.getString(R.string.cancel),
|
||||
() -> {
|
||||
view.showProgress(false);
|
||||
uploadItem.setImageQuality(IMAGE_OK);
|
||||
},
|
||||
() -> {
|
||||
presenterCallback.deletePictureAtIndex(index);
|
||||
}
|
||||
).setCancelable(false);
|
||||
}
|
||||
//If the error message is null, we will probably not show anything
|
||||
}
|
||||
|
||||
/**
|
||||
* notifies the user that a similar image exists
|
||||
*/
|
||||
@Override
|
||||
public void showSimilarImageFragment(final String originalFilePath, final String possibleFilePath,
|
||||
final ImageCoordinates similarImageCoordinates) {
|
||||
view.showSimilarImageFragment(originalFilePath, possibleFilePath,
|
||||
similarImageCoordinates
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,441 @@
|
|||
package fr.free.nrw.commons.upload.mediaDetails
|
||||
|
||||
import android.app.Activity
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile
|
||||
import fr.free.nrw.commons.kvstore.BasicKvStore
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
import fr.free.nrw.commons.nearby.Place
|
||||
import fr.free.nrw.commons.repository.UploadRepository
|
||||
import fr.free.nrw.commons.upload.ImageCoordinates
|
||||
import fr.free.nrw.commons.upload.SimilarImageInterface
|
||||
import fr.free.nrw.commons.upload.UploadActivity
|
||||
import fr.free.nrw.commons.upload.UploadActivity.Companion.setUploadIsOfAPlace
|
||||
import fr.free.nrw.commons.upload.UploadItem
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetail
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback
|
||||
import fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION
|
||||
import fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS
|
||||
import fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP
|
||||
import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK
|
||||
import io.github.coordinates2country.Coordinates2Country
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Scheduler
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Proxy
|
||||
import java.net.UnknownHostException
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class UploadMediaPresenter @Inject constructor(
|
||||
private val repository: UploadRepository,
|
||||
@param:Named(IO_THREAD) private val ioScheduler: Scheduler,
|
||||
@param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler
|
||||
) : UploadMediaDetailsContract.UserActionListener, SimilarImageInterface {
|
||||
private var view = DUMMY
|
||||
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
private val countryNamesAndCodes: Map<String, String> by lazy {
|
||||
// Create a map containing all ISO countries 2-letter codes provided by
|
||||
// `Locale.getISOCountries()` and their english names
|
||||
buildMap {
|
||||
Locale.getISOCountries().forEach {
|
||||
put(Locale("en", it).getDisplayCountry(Locale.ENGLISH), it)
|
||||
}
|
||||
}
|
||||
}
|
||||
lateinit var basicKvStoreFactory: (String) -> BasicKvStore
|
||||
|
||||
override fun onAttachView(view: UploadMediaDetailsContract.View) {
|
||||
this.view = view
|
||||
}
|
||||
|
||||
override fun onDetachView() {
|
||||
view = DUMMY
|
||||
compositeDisposable.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Upload Media Details for the corresponding upload item
|
||||
*/
|
||||
override fun setUploadMediaDetails(
|
||||
uploadMediaDetails: List<UploadMediaDetail>,
|
||||
uploadItemIndex: Int
|
||||
) {
|
||||
repository.getUploads()[uploadItemIndex].uploadMediaDetails = uploadMediaDetails.toMutableList()
|
||||
}
|
||||
|
||||
override fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore) {
|
||||
basicKvStoreFactory = factory
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives the corresponding uploadable file, processes it and return the view with and uplaod item
|
||||
*/
|
||||
override fun receiveImage(
|
||||
uploadableFile: UploadableFile?,
|
||||
place: Place?,
|
||||
inAppPictureLocation: LatLng?
|
||||
) {
|
||||
view.showProgress(true)
|
||||
compositeDisposable.add(
|
||||
repository.preProcessImage(
|
||||
uploadableFile, place, this, inAppPictureLocation
|
||||
).map { uploadItem: UploadItem ->
|
||||
if (place != null && place.isMonument && place.location != null) {
|
||||
val countryCode = countryNamesAndCodes[Coordinates2Country.country(
|
||||
place.location.latitude,
|
||||
place.location.longitude
|
||||
)]
|
||||
if (countryCode != null && WLM_SUPPORTED_COUNTRIES.contains(countryCode.lowercase())) {
|
||||
uploadItem.isWLMUpload = true
|
||||
uploadItem.countryCode = countryCode.lowercase()
|
||||
}
|
||||
}
|
||||
uploadItem
|
||||
}.subscribeOn(ioScheduler).observeOn(mainThreadScheduler)
|
||||
.subscribe({ uploadItem: UploadItem ->
|
||||
view.onImageProcessed(uploadItem)
|
||||
view.updateMediaDetails(uploadItem.uploadMediaDetails)
|
||||
view.showProgress(false)
|
||||
val gpsCoords = uploadItem.gpsCoords
|
||||
val hasImageCoordinates = gpsCoords != null && gpsCoords.imageCoordsExists
|
||||
if (hasImageCoordinates && place == null) {
|
||||
checkNearbyPlaces(uploadItem)
|
||||
}
|
||||
}, { throwable: Throwable? ->
|
||||
Timber.e(throwable, "Error occurred in processing images")
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks for the nearest location that needs images and suggests it to the user.
|
||||
*/
|
||||
private fun checkNearbyPlaces(uploadItem: UploadItem) {
|
||||
compositeDisposable.add(Maybe.fromCallable {
|
||||
repository.checkNearbyPlaces(
|
||||
uploadItem.gpsCoords!!.decLatitude, uploadItem.gpsCoords!!.decLongitude
|
||||
)
|
||||
}.subscribeOn(ioScheduler).observeOn(mainThreadScheduler).subscribe({
|
||||
view.onNearbyPlaceFound(uploadItem, it)
|
||||
}, { throwable: Throwable? ->
|
||||
Timber.e(throwable, "Error occurred in processing images")
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the image has a location. Displays a dialog alerting user that no
|
||||
* location has been to added to the image and asking them to add one, if location was not
|
||||
* removed by the user
|
||||
*
|
||||
* @param uploadItemIndex Index of the uploadItem which has no location
|
||||
* @param inAppPictureLocation In app picture location (if any)
|
||||
* @param hasUserRemovedLocation True if user has removed location from the image
|
||||
*/
|
||||
override fun displayLocDialog(
|
||||
uploadItemIndex: Int, inAppPictureLocation: LatLng?,
|
||||
hasUserRemovedLocation: Boolean
|
||||
) {
|
||||
val uploadItem = repository.getUploads()[uploadItemIndex]
|
||||
if (uploadItem.gpsCoords!!.decimalCoords == null && inAppPictureLocation == null && !hasUserRemovedLocation) {
|
||||
view.displayAddLocationDialog { verifyCaptionQuality(uploadItem) }
|
||||
} else {
|
||||
verifyCaptionQuality(uploadItem)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the image's caption and calls function to handle the result
|
||||
*
|
||||
* @param uploadItem UploadItem whose caption is checked
|
||||
*/
|
||||
private fun verifyCaptionQuality(uploadItem: UploadItem) {
|
||||
view.showProgress(true)
|
||||
compositeDisposable.add(repository.getCaptionQuality(uploadItem)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe({ capResult: Int ->
|
||||
view.showProgress(false)
|
||||
handleCaptionResult(capResult, uploadItem)
|
||||
}, { throwable: Throwable ->
|
||||
view.showProgress(false)
|
||||
if (throwable is UnknownHostException) {
|
||||
view.showConnectionErrorPopupForCaptionCheck()
|
||||
} else {
|
||||
view.showMessage(throwable.localizedMessage, R.color.color_error)
|
||||
}
|
||||
Timber.e(throwable, "Error occurred while handling image")
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles image's caption results and shows dialog if necessary
|
||||
*
|
||||
* @param errorCode Error code of the UploadItem
|
||||
* @param uploadItem UploadItem whose caption is checked
|
||||
*/
|
||||
fun handleCaptionResult(errorCode: Int, uploadItem: UploadItem) {
|
||||
// If errorCode is empty caption show message
|
||||
if (errorCode == EMPTY_CAPTION) {
|
||||
Timber.d("Captions are empty. Showing toast")
|
||||
view.showMessage(R.string.add_caption_toast, R.color.color_error)
|
||||
}
|
||||
|
||||
// If image with same file name exists check the bit in errorCode is set or not
|
||||
if ((errorCode and FILE_NAME_EXISTS) != 0) {
|
||||
Timber.d("Trying to show duplicate picture popup")
|
||||
view.showDuplicatePicturePopup(uploadItem)
|
||||
}
|
||||
|
||||
// If caption is not duplicate or user still wants to upload it
|
||||
if (errorCode == IMAGE_OK) {
|
||||
Timber.d("Image captions are okay or user still wants to upload it")
|
||||
view.onImageValidationSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Copies the caption and description of the current item to the subsequent media
|
||||
*/
|
||||
override fun copyTitleAndDescriptionToSubsequentMedia(indexInViewFlipper: Int) {
|
||||
for (i in indexInViewFlipper + 1 until repository.getCount()) {
|
||||
val subsequentUploadItem = repository.getUploads()[i]
|
||||
subsequentUploadItem.uploadMediaDetails = deepCopy(
|
||||
repository.getUploads()[indexInViewFlipper].uploadMediaDetails
|
||||
).toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and set the caption and description of the item
|
||||
*/
|
||||
override fun fetchTitleAndDescription(indexInViewFlipper: Int) =
|
||||
view.updateMediaDetails(repository.getUploads()[indexInViewFlipper].uploadMediaDetails)
|
||||
|
||||
private fun deepCopy(uploadMediaDetails: List<UploadMediaDetail>) =
|
||||
uploadMediaDetails.map(UploadMediaDetail::javaCopy)
|
||||
|
||||
override fun useSimilarPictureCoordinates(
|
||||
imageCoordinates: ImageCoordinates, uploadItemIndex: Int
|
||||
) = repository.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex)
|
||||
|
||||
override fun onMapIconClicked(indexInViewFlipper: Int) =
|
||||
view.showExternalMap(repository.getUploads()[indexInViewFlipper])
|
||||
|
||||
override fun onEditButtonClicked(indexInViewFlipper: Int) =
|
||||
view.showEditActivity(repository.getUploads()[indexInViewFlipper])
|
||||
|
||||
/**
|
||||
* Updates the information regarding the specified place for the specified upload item
|
||||
* when the user confirms the suggested nearby place.
|
||||
*
|
||||
* @param place The place to be associated with the uploads.
|
||||
* @param uploadItemIndex Index of the uploadItem whose detected place has been confirmed
|
||||
*/
|
||||
override fun onUserConfirmedUploadIsOfPlace(place: Place?, uploadItemIndex: Int) {
|
||||
val uploadItem = repository.getUploads()[uploadItemIndex]
|
||||
|
||||
uploadItem.place = place
|
||||
val uploadMediaDetails = uploadItem.uploadMediaDetails
|
||||
// Update UploadMediaDetail object for this UploadItem
|
||||
uploadMediaDetails[0] = UploadMediaDetail(place)
|
||||
|
||||
// Now that the UploadItem and its associated UploadMediaDetail objects have been updated,
|
||||
// update the view with the modified media details of the first upload item
|
||||
view.updateMediaDetails(uploadMediaDetails)
|
||||
setUploadIsOfAPlace(true)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the image quality
|
||||
*
|
||||
* @param uploadItemIndex Index of the UploadItem whose quality is to be checked
|
||||
* @param inAppPictureLocation In app picture location (if any)
|
||||
* @param activity Context reference
|
||||
* @return true if no internal error occurs, else returns false
|
||||
*/
|
||||
override fun getImageQuality(
|
||||
uploadItemIndex: Int,
|
||||
inAppPictureLocation: LatLng?,
|
||||
activity: Activity
|
||||
): Boolean {
|
||||
val uploadItems = repository.getUploads()
|
||||
view.showProgress(true)
|
||||
if (uploadItems.isEmpty()) {
|
||||
view.showProgress(false)
|
||||
// No internationalization required for this error message because it's an internal error.
|
||||
view.showMessage(
|
||||
"Internal error: Zero upload items received by the Upload Media Detail Fragment. Sorry, please upload again.",
|
||||
R.color.color_error
|
||||
)
|
||||
return false
|
||||
}
|
||||
val uploadItem = uploadItems[uploadItemIndex]
|
||||
compositeDisposable.add(repository.getImageQuality(uploadItem, inAppPictureLocation)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe({ imageResult: Int ->
|
||||
storeImageQuality(imageResult, uploadItemIndex, activity, uploadItem)
|
||||
}, { throwable: Throwable ->
|
||||
if (throwable is UnknownHostException) {
|
||||
view.showProgress(false)
|
||||
view.showConnectionErrorPopup()
|
||||
} else {
|
||||
view.showMessage(throwable.localizedMessage, R.color.color_error)
|
||||
}
|
||||
Timber.e(throwable, "Error occurred while handling image")
|
||||
})
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the image quality in JSON format in SharedPrefs
|
||||
*
|
||||
* @param imageResult Image quality
|
||||
* @param uploadItemIndex Index of the UploadItem whose quality is calculated
|
||||
* @param activity Context reference
|
||||
* @param uploadItem UploadItem whose quality is to be checked
|
||||
*/
|
||||
private fun storeImageQuality(
|
||||
imageResult: Int, uploadItemIndex: Int, activity: Activity, uploadItem: UploadItem
|
||||
) {
|
||||
val store = BasicKvStore(activity, UploadActivity.storeNameForCurrentUploadImagesSize)
|
||||
val value = store.getString(UPLOAD_QUALITIES_KEY, null)
|
||||
try {
|
||||
val jsonObject = value.asJsonObject().apply {
|
||||
put("UploadItem$uploadItemIndex", imageResult)
|
||||
}
|
||||
store.putString(UPLOAD_QUALITIES_KEY, jsonObject.toString())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
|
||||
if (uploadItemIndex == 0) {
|
||||
if (!isBatteryDialogShowing && !isCategoriesDialogShowing) {
|
||||
// if battery-optimisation dialog is not being shown, call checkImageQuality
|
||||
checkImageQuality(uploadItem, uploadItemIndex)
|
||||
} else {
|
||||
view.showProgress(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check image quality from stored qualities and display dialogs
|
||||
*
|
||||
* @param uploadItem UploadItem whose quality is to be checked
|
||||
* @param index Index of the UploadItem whose quality is to be checked
|
||||
*/
|
||||
override fun checkImageQuality(uploadItem: UploadItem, index: Int) {
|
||||
if ((uploadItem.imageQuality != IMAGE_OK) && (uploadItem.imageQuality != IMAGE_KEEP)) {
|
||||
val value = basicKvStoreFactory(UploadActivity.storeNameForCurrentUploadImagesSize)
|
||||
.getString(UPLOAD_QUALITIES_KEY, null)
|
||||
try {
|
||||
val imageQuality = value.asJsonObject()["UploadItem$index"] as Int
|
||||
view.showProgress(false)
|
||||
if (imageQuality == IMAGE_OK) {
|
||||
uploadItem.hasInvalidLocation = false
|
||||
uploadItem.imageQuality = imageQuality
|
||||
} else {
|
||||
handleBadImage(imageQuality, uploadItem, index)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the image qualities stored in JSON, whenever an image is deleted
|
||||
*
|
||||
* @param size Size of uploadableFiles
|
||||
* @param index Index of the UploadItem which was deleted
|
||||
*/
|
||||
override fun updateImageQualitiesJSON(size: Int, index: Int) {
|
||||
val value = basicKvStoreFactory(UploadActivity.storeNameForCurrentUploadImagesSize)
|
||||
.getString(UPLOAD_QUALITIES_KEY, null)
|
||||
try {
|
||||
val jsonObject = value.asJsonObject().apply {
|
||||
for (i in index until (size - 1)) {
|
||||
put("UploadItem$i", this["UploadItem" + (i + 1)])
|
||||
}
|
||||
remove("UploadItem" + (size - 1))
|
||||
}
|
||||
basicKvStoreFactory(UploadActivity.storeNameForCurrentUploadImagesSize)
|
||||
.putString(UPLOAD_QUALITIES_KEY, jsonObject.toString())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles bad pictures, like too dark, already on wikimedia, downloaded from internet
|
||||
*
|
||||
* @param errorCode Error code of the bad image quality
|
||||
* @param uploadItem UploadItem whose quality is bad
|
||||
* @param index Index of item whose quality is bad
|
||||
*/
|
||||
private fun handleBadImage(
|
||||
errorCode: Int,
|
||||
uploadItem: UploadItem, index: Int
|
||||
) {
|
||||
Timber.d("Handle bad picture with error code %d", errorCode)
|
||||
if (errorCode >= 8) { // If location of image and nearby does not match
|
||||
uploadItem.hasInvalidLocation = true
|
||||
}
|
||||
|
||||
// If image has some other problems, show popup accordingly
|
||||
if (errorCode != EMPTY_CAPTION && errorCode != FILE_NAME_EXISTS) {
|
||||
view.showBadImagePopup(errorCode, index, uploadItem)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* notifies the user that a similar image exists
|
||||
*/
|
||||
override fun showSimilarImageFragment(
|
||||
originalFilePath: String?,
|
||||
possibleFilePath: String?,
|
||||
similarImageCoordinates: ImageCoordinates?
|
||||
) = view.showSimilarImageFragment(originalFilePath, possibleFilePath, similarImageCoordinates)
|
||||
|
||||
private fun String?.asJsonObject() = if (this != null) {
|
||||
JSONObject(this)
|
||||
} else {
|
||||
JSONObject()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val UPLOAD_QUALITIES_KEY = "UploadedImagesQualities"
|
||||
private val WLM_SUPPORTED_COUNTRIES = listOf(
|
||||
"am", "at", "az", "br", "hr", "sv", "fi", "fr", "de", "gh",
|
||||
"in", "ie", "il", "mk", "my", "mt", "pk", "pe", "pl", "ru",
|
||||
"rw", "si", "es", "se", "tw", "ug", "ua", "us"
|
||||
)
|
||||
|
||||
private val DUMMY = Proxy.newProxyInstance(
|
||||
UploadMediaDetailsContract.View::class.java.classLoader,
|
||||
arrayOf<Class<*>>(UploadMediaDetailsContract.View::class.java)
|
||||
) { _: Any?, _: Method?, _: Array<Any?>? -> null } as UploadMediaDetailsContract.View
|
||||
|
||||
var presenterCallback: UploadMediaDetailFragmentCallback? = null
|
||||
|
||||
/**
|
||||
* Variable used to determine if the battery-optimisation dialog is being shown or not
|
||||
*/
|
||||
var isBatteryDialogShowing: Boolean = false
|
||||
|
||||
var isCategoriesDialogShowing: Boolean = false
|
||||
}
|
||||
}
|
||||
|
|
@ -68,6 +68,9 @@ data class DepictedItem constructor(
|
|||
entity.id(),
|
||||
)
|
||||
|
||||
val primaryImage: String?
|
||||
get() = imageUrl?.split('-')?.lastOrNull()
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
when {
|
||||
this === other -> true
|
||||
|
|
|
|||
|
|
@ -481,8 +481,8 @@ class UploadWorker(
|
|||
)
|
||||
if (null != revisionID) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val place = placesRepository.fetchPlace(wikiDataPlace.id);
|
||||
place.name = wikiDataPlace.name;
|
||||
val place = placesRepository.fetchPlace(wikiDataPlace.id)
|
||||
place.name = wikiDataPlace.name
|
||||
place.pic = HOME_URL + uploadResult.createCanonicalFileName()
|
||||
placesRepository
|
||||
.save(place)
|
||||
|
|
@ -568,12 +568,18 @@ class UploadWorker(
|
|||
|
||||
sequenceFileName =
|
||||
if (fileName.indexOf('.') == -1) {
|
||||
"$fileName #$randomHash"
|
||||
// Append the random hash in parentheses if no file extension is present
|
||||
"$fileName ($randomHash)"
|
||||
} else {
|
||||
val regex =
|
||||
Pattern.compile("^(.*)(\\..+?)$")
|
||||
val regexMatcher = regex.matcher(fileName)
|
||||
regexMatcher.replaceAll("$1 #$randomHash")
|
||||
// Append the random hash in parentheses before the file extension
|
||||
if (regexMatcher.find()) {
|
||||
"${regexMatcher.group(1)} ($randomHash)${regexMatcher.group(2)}"
|
||||
} else {
|
||||
"$fileName ($randomHash)"
|
||||
}
|
||||
}
|
||||
}
|
||||
return sequenceFileName!!
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ import com.karumi.dexter.listener.PermissionRequest
|
|||
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.upload.UploadActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
object PermissionUtils {
|
||||
|
|
@ -130,7 +133,7 @@ object PermissionUtils {
|
|||
vararg permissions: String
|
||||
) {
|
||||
if (hasPartialAccess(activity)) {
|
||||
Thread(onPermissionGranted).start()
|
||||
CoroutineScope(Dispatchers.Main).launch { onPermissionGranted.run() }
|
||||
return
|
||||
}
|
||||
checkPermissionsAndPerformAction(
|
||||
|
|
@ -166,13 +169,15 @@ object PermissionUtils {
|
|||
rationaleMessage: Int,
|
||||
vararg permissions: String
|
||||
) {
|
||||
val scope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
Dexter.withActivity(activity)
|
||||
.withPermissions(*permissions)
|
||||
.withListener(object : MultiplePermissionsListener {
|
||||
override fun onPermissionsChecked(report: MultiplePermissionsReport) {
|
||||
when {
|
||||
report.areAllPermissionsGranted() || hasPartialAccess(activity) ->
|
||||
Thread(onPermissionGranted).start()
|
||||
scope.launch { onPermissionGranted.run() }
|
||||
report.isAnyPermissionPermanentlyDenied -> {
|
||||
DialogUtil.showAlertDialog(
|
||||
activity,
|
||||
|
|
@ -189,7 +194,7 @@ object PermissionUtils {
|
|||
null, null
|
||||
)
|
||||
}
|
||||
else -> Thread(onPermissionDenied).start()
|
||||
else -> scope.launch { onPermissionDenied?.run() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -208,7 +213,7 @@ object PermissionUtils {
|
|||
activity.getString(android.R.string.cancel),
|
||||
{
|
||||
if (activity is UploadActivity) {
|
||||
activity.setShowPermissionsDialog(true)
|
||||
activity.isShowPermissionsDialog = true
|
||||
}
|
||||
token.continuePermissionRequest()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,9 +21,14 @@ abstract class SwipableCardView @JvmOverloads constructor(
|
|||
defStyleAttr: Int = 0
|
||||
) : CardView(context, attrs, defStyleAttr) {
|
||||
|
||||
|
||||
companion object{
|
||||
const val MINIMUM_THRESHOLD_FOR_SWIPE = 100f
|
||||
}
|
||||
|
||||
private var x1 = 0f
|
||||
private var x2 = 0f
|
||||
private val MINIMUM_THRESHOLD_FOR_SWIPE = 100f
|
||||
|
||||
|
||||
init {
|
||||
interceptOnTouchListener()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package fr.free.nrw.commons.wikidata.mwapi
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
open class MwPostResponse : MwResponse() {
|
||||
@SerializedName("success")
|
||||
val successVal: Int = 0
|
||||
|
||||
fun success(result: String?): Boolean =
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class MwQueryPage : BaseModel() {
|
|||
|
||||
fun title(): String = title!!
|
||||
|
||||
fun categoryInfo(): CategoryInfo = categoryinfo!!
|
||||
fun categoryInfo(): CategoryInfo? = categoryinfo
|
||||
|
||||
fun index(): Int = index
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@
|
|||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:fillColor="?attr/more_bottom_sheet_drawable_color"
|
||||
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
android:layout_width="@dimen/landscape_width"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="@dimen/small_gap">
|
||||
android:layout_marginTop="@dimen/login_gap">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="@dimen/landscape_width"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
<LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fillViewport="true"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
|
|
@ -132,15 +132,32 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_submit_feedback"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginBottom="@dimen/dimen_10"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/dimen_10"
|
||||
android:gravity="center"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/submit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_cancel"
|
||||
android:textColor="@color/white"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/cancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_submit_feedback"
|
||||
android:textColor="@color/white"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/submit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
|
|
|
|||
|
|
@ -66,9 +66,18 @@
|
|||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:id="@+id/language_list"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/cancel_button"
|
||||
app:layout_constraintTop_toBottomOf="@+id/all_languages" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/cancel_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/cancel"
|
||||
android:textColor="@color/primaryColor"
|
||||
android:background="@android:color/transparent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
android:fillViewport="true"
|
||||
tools:ignore="ContentDescription" >
|
||||
|
||||
<!-- TODO Add ContentDescription For ALL Images Added ignore to suppress Lints -->
|
||||
<!-- TODO Add ContentDescription For ALL Images Added ignore to suppress Lints -->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/layout"
|
||||
|
|
|
|||
|
|
@ -49,6 +49,20 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/all_images_uploaded_or_marked"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:textSize="16sp"
|
||||
android:padding="@dimen/standard_gap"
|
||||
android:textColor="@color/text_color_selector"
|
||||
android:text="@string/congratulations_all_pictures_in_this_album_have_been_either_uploaded_or_marked_as_not_for_upload"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loader"
|
||||
|
|
|
|||
|
|
@ -117,12 +117,12 @@
|
|||
android:layout_alignParentRight="true"
|
||||
android:clickable="true"
|
||||
android:visibility="visible"
|
||||
app:backgroundTint="@color/main_background_light"
|
||||
android:layout_margin="16dp"
|
||||
app:backgroundTint="?attr/mainBackground"
|
||||
app:elevation="@dimen/dimen_6"
|
||||
app:fabSize="normal"
|
||||
app:layout_anchorGravity="top|right|end"
|
||||
app:srcCompat="@drawable/ic_my_location_black_24dp"
|
||||
app:useCompatPadding="true" />
|
||||
app:srcCompat="@drawable/ic_my_location_black_24dp" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab_legend"
|
||||
|
|
@ -133,12 +133,12 @@
|
|||
android:layout_alignParentRight="true"
|
||||
android:clickable="true"
|
||||
android:visibility="visible"
|
||||
app:backgroundTint="@color/main_background_light"
|
||||
android:layout_margin="16dp"
|
||||
app:backgroundTint="?attr/mainBackground"
|
||||
app:elevation="@dimen/dimen_6"
|
||||
app:fabSize="normal"
|
||||
app:layout_anchorGravity="top|right|end"
|
||||
app:srcCompat="@drawable/ic_info_outline_24dp"
|
||||
app:useCompatPadding="true" />
|
||||
app:srcCompat="@drawable/ic_info_outline_24dp" />
|
||||
|
||||
<include
|
||||
android:id="@+id/nearby_legend_layout"
|
||||
|
|
|
|||
|
|
@ -331,6 +331,7 @@
|
|||
<string name="copy_wikicode">انسخ نص الويكي إلى الكليب بورد</string>
|
||||
<string name="wikicode_copied">نص الويكي تم نسخه إلى الكليب بورد</string>
|
||||
<string name="nearby_location_not_available">قد لا تعمل الأجهزة المجاورة بشكل صحيح، الموقع غير متوفر.</string>
|
||||
<string name="nearby_showing_pins_offline">الإنترنت غير متاح. يتم عرض الأماكن المخزنة مؤقتًا فقط.</string>
|
||||
<string name="upload_location_access_denied">رُفض الوصول إلى الموقع. يرجى ضبط موقعك يدويًا لاستخدام هذه الميزة.</string>
|
||||
<string name="location_permission_rationale_nearby">صلاحية مطلوبة لعرض قائمة بالأماكن القريبة</string>
|
||||
<string name="location_permission_rationale_explore">الإذن مطلوب لعرض قائمة الصور القريبة</string>
|
||||
|
|
@ -874,4 +875,12 @@
|
|||
<string name="usages_on_commons_heading">كومنز</string>
|
||||
<string name="usages_on_other_wikis_heading">مواقع ويكي أخرى</string>
|
||||
<string name="file_usages_container_heading">حالات استخدام الملف</string>
|
||||
<string name="title_activity_single_web_view">نشاط عرض ويب واحد</string>
|
||||
<string name="account">حساب</string>
|
||||
<string name="vanish_account">حذف الحساب</string>
|
||||
<string name="account_vanish_request_confirm_title">تحذير من اختفاء الحساب</string>
|
||||
<string name="account_vanish_request_confirm">الاختفاء هو <b>الملاذ الأخير</b> ويجب <b>استخدامه فقط عندما ترغب في التوقف عن التحرير إلى الأبد</b> وأيضًا لإخفاء أكبر عدد ممكن من ارتباطاتك السابقة.<br/><br/> يتم حذف الحساب على ويكيميديا كومنز عن طريق تغيير اسم حسابك بحيث لا يتمكن الآخرون من التعرف على مساهماتك في عملية تسمى اختفاء الحساب. <b>لا يضمن الاختفاء عدم الكشف عن الهوية تمامًا أو إزالة المساهمات في المشاريع</b> .</string>
|
||||
<string name="caption">الشرح</string>
|
||||
<string name="caption_copied_to_clipboard">تم نسخ التسمية التوضيحية إلى الحافظة</string>
|
||||
<string name="congratulations_all_pictures_in_this_album_have_been_either_uploaded_or_marked_as_not_for_upload">مبروك، جميع الصور الموجودة في هذا الألبوم تم تحميلها أو تم وضع علامة عليها بأنها غير قابلة للتحميل.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -53,9 +53,11 @@
|
|||
<string name="signup">Qeydiyyatdan keç</string>
|
||||
<string name="logging_in_title">Giriş edilir</string>
|
||||
<string name="logging_in_message">Zəhmət olmasa, gözləyin…</string>
|
||||
<string name="login_success" fuzzy="true">Daxil oldunuz!</string>
|
||||
<string name="login_success">Uğurlu giriş!</string>
|
||||
<string name="login_failed">Giriş baş tutmadı!</string>
|
||||
<string name="upload_failed">Fayl tapılmadı. Xahiş edirik başqa bir fayl üzərində cəhd edin.</string>
|
||||
<string name="retry_limit_reached">Maksimum təkrar cəhd limitinə çatdınız! Zəhmət olmasa, yükləməni ləğv edin və yenidən cəhd edin</string>
|
||||
<string name="unrestricted_battery_mode">Batareya optimallaşdırılması söndürülsün?</string>
|
||||
<string name="authentication_failed" fuzzy="true">Doğrulama alınmadı, xahiş edirəm yenidən daxil olun</string>
|
||||
<string name="uploading_started">Yükləmə başladı!</string>
|
||||
<string name="upload_completed_notification_title">%1$s yükləndi!</string>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue