mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 12:53:55 +01:00
Merge branch 'main' into 1-issue#5829
This commit is contained in:
commit
02a2bdc41b
160 changed files with 4292 additions and 3488 deletions
|
|
@ -146,7 +146,7 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
||||||
loginTwoFactor.removeTextChangedListener(textWatcher)
|
loginTwoFactor.removeTextChangedListener(textWatcher)
|
||||||
}
|
}
|
||||||
delegate.onDestroy()
|
delegate.onDestroy()
|
||||||
loginClient?.cancel()
|
loginClient.cancel()
|
||||||
binding = null
|
binding = null
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
@ -182,34 +182,34 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
// if progressDialog is visible during the configuration change then store state as true else false so that
|
// if progressDialog is visible during the configuration change then store state as true else false so that
|
||||||
// we maintain visibility of progressDailog after configuration change
|
// we maintain visibility of progressDialog after configuration change
|
||||||
if (progressDialog != null && progressDialog!!.isShowing) {
|
if (progressDialog != null && progressDialog!!.isShowing) {
|
||||||
outState.putBoolean(saveProgressDailog, true)
|
outState.putBoolean(SAVE_PROGRESS_DIALOG, true)
|
||||||
} else {
|
} else {
|
||||||
outState.putBoolean(saveProgressDailog, false)
|
outState.putBoolean(SAVE_PROGRESS_DIALOG, false)
|
||||||
}
|
}
|
||||||
outState.putString(
|
outState.putString(
|
||||||
saveErrorMessage,
|
SAVE_ERROR_MESSAGE,
|
||||||
binding!!.errorMessage.text.toString()
|
binding!!.errorMessage.text.toString()
|
||||||
) //Save the errorMessage
|
) //Save the errorMessage
|
||||||
outState.putString(
|
outState.putString(
|
||||||
saveUsername,
|
SAVE_USERNAME,
|
||||||
binding!!.loginUsername.text.toString()
|
binding!!.loginUsername.text.toString()
|
||||||
) // Save the username
|
) // Save the username
|
||||||
outState.putString(
|
outState.putString(
|
||||||
savePassword,
|
SAVE_PASSWORD,
|
||||||
binding!!.loginPassword.text.toString()
|
binding!!.loginPassword.text.toString()
|
||||||
) // Save the password
|
) // Save the password
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
super.onRestoreInstanceState(savedInstanceState)
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
binding!!.loginUsername.setText(savedInstanceState.getString(saveUsername))
|
binding!!.loginUsername.setText(savedInstanceState.getString(SAVE_USERNAME))
|
||||||
binding!!.loginPassword.setText(savedInstanceState.getString(savePassword))
|
binding!!.loginPassword.setText(savedInstanceState.getString(SAVE_PASSWORD))
|
||||||
if (savedInstanceState.getBoolean(saveProgressDailog)) {
|
if (savedInstanceState.getBoolean(SAVE_PROGRESS_DIALOG)) {
|
||||||
performLogin()
|
performLogin()
|
||||||
}
|
}
|
||||||
val errorMessage = savedInstanceState.getString(saveErrorMessage)
|
val errorMessage = savedInstanceState.getString(SAVE_ERROR_MESSAGE)
|
||||||
if (sessionManager.isUserLoggedIn) {
|
if (sessionManager.isUserLoggedIn) {
|
||||||
showMessage(R.string.login_success, R.color.primaryDarkColor)
|
showMessage(R.string.login_success, R.color.primaryDarkColor)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -396,9 +396,9 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
||||||
fun startYourself(context: Context) =
|
fun startYourself(context: Context) =
|
||||||
context.startActivity(Intent(context, LoginActivity::class.java))
|
context.startActivity(Intent(context, LoginActivity::class.java))
|
||||||
|
|
||||||
const val saveProgressDailog: String = "ProgressDailog_state"
|
const val SAVE_PROGRESS_DIALOG: String = "ProgressDialog_state"
|
||||||
const val saveErrorMessage: String = "errorMessage"
|
const val SAVE_ERROR_MESSAGE: String = "errorMessage"
|
||||||
const val saveUsername: String = "username"
|
const val SAVE_USERNAME: String = "username"
|
||||||
const val savePassword: String = "password"
|
const val SAVE_PASSWORD: String = "password"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,17 @@ package fr.free.nrw.commons.bookmarks;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment;
|
||||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment;
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment;
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
|
||||||
|
|
@ -26,6 +25,7 @@ import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||||
import fr.free.nrw.commons.navtab.NavTab;
|
import fr.free.nrw.commons.navtab.NavTab;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements
|
public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements
|
||||||
FragmentManager.OnBackStackChangedListener,
|
FragmentManager.OnBackStackChangedListener,
|
||||||
|
|
@ -48,14 +48,21 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
||||||
String title = bundle.getString("categoryName");
|
String title = bundle.getString("categoryName");
|
||||||
int order = bundle.getInt("order");
|
int order = bundle.getInt("order");
|
||||||
final int orderItem = bundle.getInt("orderItem");
|
final int orderItem = bundle.getInt("orderItem");
|
||||||
if (order == 0) {
|
|
||||||
listFragment = new BookmarkPicturesFragment();
|
switch (order){
|
||||||
} else {
|
case 0: listFragment = new BookmarkPicturesFragment();
|
||||||
listFragment = new BookmarkLocationsFragment();
|
break;
|
||||||
|
|
||||||
|
case 1: listFragment = new BookmarkLocationsFragment();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3: listFragment = new BookmarkCategoriesFragment();
|
||||||
|
break;
|
||||||
|
}
|
||||||
if(orderItem == 2) {
|
if(orderItem == 2) {
|
||||||
listFragment = new BookmarkItemsFragment();
|
listFragment = new BookmarkItemsFragment();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Bundle featuredArguments = new Bundle();
|
Bundle featuredArguments = new Bundle();
|
||||||
featuredArguments.putString("categoryName", title);
|
featuredArguments.putString("categoryName", title);
|
||||||
listFragment.setArguments(featuredArguments);
|
listFragment.setArguments(featuredArguments);
|
||||||
|
|
@ -129,7 +136,7 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMediaClicked(int position) {
|
public void onMediaClicked(int position) {
|
||||||
Log.d("deneme8", "on media clicked");
|
Timber.d("on media clicked");
|
||||||
/*container.setVisibility(View.VISIBLE);
|
/*container.setVisibility(View.VISIBLE);
|
||||||
((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE);
|
((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE);
|
||||||
mediaDetails = new MediaDetailPagerFragment(false, true, position);
|
mediaDetails = new MediaDetailPagerFragment(false, true, position);
|
||||||
|
|
@ -237,7 +244,7 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
Log.d("deneme8", "on media clicked");
|
Timber.d("on media clicked");
|
||||||
binding.exploreContainer.setVisibility(View.VISIBLE);
|
binding.exploreContainer.setVisibility(View.VISIBLE);
|
||||||
((BookmarkFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
|
((BookmarkFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
|
||||||
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,13 @@ public class BookmarksPagerAdapter extends FragmentPagerAdapter {
|
||||||
new BookmarkListRootFragment(locationBundle, this),
|
new BookmarkListRootFragment(locationBundle, this),
|
||||||
context.getString(R.string.title_page_bookmarks_items)));
|
context.getString(R.string.title_page_bookmarks_items)));
|
||||||
}
|
}
|
||||||
|
final Bundle categoriesBundle = new Bundle();
|
||||||
|
categoriesBundle.putString("categoryName",
|
||||||
|
context.getString(R.string.title_page_bookmarks_categories));
|
||||||
|
categoriesBundle.putInt("order", 3);
|
||||||
|
pages.add(new BookmarkPages(
|
||||||
|
new BookmarkListRootFragment(categoriesBundle, this),
|
||||||
|
context.getString(R.string.title_page_bookmarks_categories)));
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.category
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bookmark categories dao
|
||||||
|
*
|
||||||
|
* @constructor Create empty Bookmark categories dao
|
||||||
|
*/
|
||||||
|
@Dao
|
||||||
|
interface BookmarkCategoriesDao {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert or Delete category bookmark into DB
|
||||||
|
*
|
||||||
|
* @param bookmarksCategoryModal
|
||||||
|
*/
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insert(bookmarksCategoryModal: BookmarksCategoryModal)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete category bookmark from DB
|
||||||
|
*
|
||||||
|
* @param bookmarksCategoryModal
|
||||||
|
*/
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(bookmarksCategoryModal: BookmarksCategoryModal)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if given category exist in DB
|
||||||
|
*
|
||||||
|
* @param categoryName
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Query("SELECT EXISTS (SELECT 1 FROM bookmarks_categories WHERE categoryName = :categoryName)")
|
||||||
|
suspend fun doesExist(categoryName: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all categories
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM bookmarks_categories")
|
||||||
|
fun getAllCategories(): Flow<List<BookmarksCategoryModal>>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.category
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import dagger.android.support.DaggerFragment
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.category.CategoryDetailsActivity
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab fragment to show list of bookmarked Categories
|
||||||
|
*/
|
||||||
|
class BookmarkCategoriesFragment : DaggerFragment() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var bookmarkCategoriesDao: BookmarkCategoriesDao
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return ComposeView(requireContext()).apply {
|
||||||
|
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||||
|
setContent {
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = if (isSystemInDarkTheme()) darkColorScheme(
|
||||||
|
primary = colorResource(R.color.primaryDarkColor),
|
||||||
|
surface = colorResource(R.color.main_background_dark),
|
||||||
|
background = colorResource(R.color.main_background_dark)
|
||||||
|
) else lightColorScheme(
|
||||||
|
primary = colorResource(R.color.primaryColor),
|
||||||
|
surface = colorResource(R.color.main_background_light),
|
||||||
|
background = colorResource(R.color.main_background_light)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val listOfBookmarks by bookmarkCategoriesDao.getAllCategories()
|
||||||
|
.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
Surface(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
if (listOfBookmarks.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.bookmark_empty),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (isSystemInDarkTheme()) Color(0xB3FFFFFF)
|
||||||
|
else Color(
|
||||||
|
0x8A000000
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
items(items = listOfBookmarks) { bookmarkItem ->
|
||||||
|
CategoryItem(
|
||||||
|
categoryName = bookmarkItem.categoryName,
|
||||||
|
onClick = {
|
||||||
|
val categoryDetailsIntent = Intent(
|
||||||
|
requireContext(),
|
||||||
|
CategoryDetailsActivity::class.java
|
||||||
|
).putExtra("categoryName", it)
|
||||||
|
startActivity(categoryDetailsIntent)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CategoryItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: (String) -> Unit,
|
||||||
|
categoryName: String
|
||||||
|
) {
|
||||||
|
Row(modifier = modifier.clickable {
|
||||||
|
onClick(categoryName)
|
||||||
|
}) {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
painter = painterResource(R.drawable.commons),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = categoryName,
|
||||||
|
maxLines = 2,
|
||||||
|
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun CategoryItemPreview() {
|
||||||
|
CategoryItem(
|
||||||
|
onClick = {},
|
||||||
|
categoryName = "Test Category"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.category
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class representing bookmarked category in DB
|
||||||
|
*
|
||||||
|
* @property categoryName
|
||||||
|
* @constructor Create empty Bookmarks category modal
|
||||||
|
*/
|
||||||
|
@Entity(tableName = "bookmarks_categories")
|
||||||
|
data class BookmarksCategoryModal(
|
||||||
|
@PrimaryKey val categoryName: String
|
||||||
|
)
|
||||||
|
|
@ -7,8 +7,12 @@ import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import fr.free.nrw.commons.Media
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.Utils
|
import fr.free.nrw.commons.Utils
|
||||||
|
|
@ -19,6 +23,8 @@ import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment
|
||||||
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment
|
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
||||||
import fr.free.nrw.commons.theme.BaseActivity
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -38,6 +44,11 @@ class CategoryDetailsActivity : BaseActivity(),
|
||||||
|
|
||||||
private lateinit var binding: ActivityCategoryDetailsBinding
|
private lateinit var binding: ActivityCategoryDetailsBinding
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var categoryViewModelFactory: CategoryDetailsViewModel.ViewModelFactory
|
||||||
|
|
||||||
|
private val viewModel: CategoryDetailsViewModel by viewModels<CategoryDetailsViewModel> { categoryViewModelFactory }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
|
@ -53,6 +64,15 @@ class CategoryDetailsActivity : BaseActivity(),
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
setTabs()
|
setTabs()
|
||||||
setPageTitle()
|
setPageTitle()
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED){
|
||||||
|
viewModel.bookmarkState.collect {
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -73,6 +93,8 @@ class CategoryDetailsActivity : BaseActivity(),
|
||||||
categoriesMediaFragment.arguments = arguments
|
categoriesMediaFragment.arguments = arguments
|
||||||
subCategoryListFragment.arguments = arguments
|
subCategoryListFragment.arguments = arguments
|
||||||
parentCategoriesFragment.arguments = arguments
|
parentCategoriesFragment.arguments = arguments
|
||||||
|
|
||||||
|
viewModel.onCheckIfBookmarked(categoryName!!)
|
||||||
}
|
}
|
||||||
fragmentList.add(categoriesMediaFragment)
|
fragmentList.add(categoriesMediaFragment)
|
||||||
titleList.add("MEDIA")
|
titleList.add("MEDIA")
|
||||||
|
|
@ -181,6 +203,14 @@ class CategoryDetailsActivity : BaseActivity(),
|
||||||
Utils.handleWebUrl(this, Uri.parse(title.canonicalUri))
|
Utils.handleWebUrl(this, Uri.parse(title.canonicalUri))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.menu_bookmark_current_category -> {
|
||||||
|
categoryName?.let {
|
||||||
|
viewModel.onBookmarkClick(categoryName = it)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
onBackPressed()
|
onBackPressed()
|
||||||
true
|
true
|
||||||
|
|
@ -189,6 +219,22 @@ class CategoryDetailsActivity : BaseActivity(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
menu?.run {
|
||||||
|
val bookmarkMenuItem = findItem(R.id.menu_bookmark_current_category)
|
||||||
|
if (bookmarkMenuItem != null) {
|
||||||
|
val icon = if(viewModel.bookmarkState.value){
|
||||||
|
R.drawable.menu_ic_round_star_filled_24px
|
||||||
|
} else {
|
||||||
|
R.drawable.menu_ic_round_star_border_24px
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarkMenuItem.setIcon(icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onPrepareOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called on backPressed of anyFragment in the activity.
|
* This method is called on backPressed of anyFragment in the activity.
|
||||||
* If condition is called when mediaDetailFragment is opened.
|
* If condition is called when mediaDetailFragment is opened.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
package fr.free.nrw.commons.category
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao
|
||||||
|
import fr.free.nrw.commons.bookmarks.category.BookmarksCategoryModal
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModal for [CategoryDetailsActivity]
|
||||||
|
*/
|
||||||
|
class CategoryDetailsViewModel(
|
||||||
|
private val bookmarkCategoriesDao: BookmarkCategoriesDao
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _bookmarkState = MutableStateFlow(false)
|
||||||
|
val bookmarkState = _bookmarkState.asStateFlow()
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to check if bookmark exists for the given category in DB
|
||||||
|
* based on that bookmark state is updated
|
||||||
|
* @param categoryName
|
||||||
|
*/
|
||||||
|
fun onCheckIfBookmarked(categoryName: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val isBookmarked = bookmarkCategoriesDao.doesExist(categoryName)
|
||||||
|
_bookmarkState.update {
|
||||||
|
isBookmarked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles event when bookmark button is clicked from view
|
||||||
|
* based on that category is bookmarked or removed in/from in the DB
|
||||||
|
* and bookmark state is update as well
|
||||||
|
* @param categoryName
|
||||||
|
*/
|
||||||
|
fun onBookmarkClick(categoryName: String) {
|
||||||
|
if (_bookmarkState.value) {
|
||||||
|
deleteBookmark(categoryName)
|
||||||
|
_bookmarkState.update {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addBookmark(categoryName)
|
||||||
|
_bookmarkState.update {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add bookmark into DB
|
||||||
|
*
|
||||||
|
* @param categoryName
|
||||||
|
*/
|
||||||
|
private fun addBookmark(categoryName: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val categoryItem = BookmarksCategoryModal(
|
||||||
|
categoryName = categoryName
|
||||||
|
)
|
||||||
|
|
||||||
|
bookmarkCategoriesDao.insert(categoryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete bookmark from DB
|
||||||
|
*
|
||||||
|
* @param categoryName
|
||||||
|
*/
|
||||||
|
private fun deleteBookmark(categoryName: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
bookmarkCategoriesDao.delete(
|
||||||
|
BookmarksCategoryModal(
|
||||||
|
categoryName = categoryName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View model factory to create [CategoryDetailsViewModel]
|
||||||
|
*
|
||||||
|
* @property bookmarkCategoriesDao
|
||||||
|
* @constructor Create empty View model factory
|
||||||
|
*/
|
||||||
|
class ViewModelFactory @Inject constructor(
|
||||||
|
private val bookmarkCategoriesDao: BookmarkCategoriesDao
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||||
|
if (modelClass.isAssignableFrom(CategoryDetailsViewModel::class.java)) {
|
||||||
|
CategoryDetailsViewModel(bookmarkCategoriesDao) as T
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Unknown class name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -63,13 +63,13 @@ data class Contribution constructor(
|
||||||
Media(
|
Media(
|
||||||
formatCaptions(item.uploadMediaDetails),
|
formatCaptions(item.uploadMediaDetails),
|
||||||
categories,
|
categories,
|
||||||
item.fileName,
|
item.filename,
|
||||||
formatDescriptions(item.uploadMediaDetails),
|
formatDescriptions(item.uploadMediaDetails),
|
||||||
sessionManager.userName,
|
sessionManager.userName,
|
||||||
sessionManager.userName,
|
sessionManager.userName,
|
||||||
),
|
),
|
||||||
localUri = item.mediaUri,
|
localUri = item.mediaUri,
|
||||||
decimalCoords = item.gpsCoords.decimalCoords,
|
decimalCoords = item.gpsCoords?.decimalCoords,
|
||||||
dateCreatedSource = "",
|
dateCreatedSource = "",
|
||||||
depictedItems = depictedItems,
|
depictedItems = depictedItems,
|
||||||
wikidataPlace = from(item.place),
|
wikidataPlace = from(item.place),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
import androidx.paging.PagedList.BoundaryCallback
|
import androidx.paging.PagedList.BoundaryCallback
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationModule
|
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
|
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
|
||||||
import fr.free.nrw.commons.media.MediaClient
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
import io.reactivex.Scheduler
|
import io.reactivex.Scheduler
|
||||||
|
|
@ -31,10 +30,7 @@ class ContributionBoundaryCallback
|
||||||
* network
|
* network
|
||||||
*/
|
*/
|
||||||
override fun onZeroItemsLoaded() {
|
override fun onZeroItemsLoaded() {
|
||||||
if (sessionManager.userName != null) {
|
refreshList()
|
||||||
mediaClient.resetUserNameContinuation(sessionManager.userName!!)
|
|
||||||
}
|
|
||||||
fetchContributions()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -52,9 +48,25 @@ class ContributionBoundaryCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches contributions using the MediaWiki API
|
* Fetch list from network and save it to local DB.
|
||||||
|
*
|
||||||
|
* @param onRefreshFinish callback to invoke when operations finishes
|
||||||
|
* with either error or success.
|
||||||
*/
|
*/
|
||||||
private fun fetchContributions() {
|
fun refreshList(onRefreshFinish: () -> Unit = {}){
|
||||||
|
if (sessionManager.userName != null) {
|
||||||
|
mediaClient.resetUserNameContinuation(sessionManager.userName!!)
|
||||||
|
}
|
||||||
|
fetchContributions(onRefreshFinish)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches contributions using the MediaWiki API
|
||||||
|
*
|
||||||
|
* @param onRefreshFinish callback to invoke when operations finishes
|
||||||
|
* with either error or success.
|
||||||
|
*/
|
||||||
|
private fun fetchContributions(onRefreshFinish: () -> Unit = {}) {
|
||||||
if (sessionManager.userName != null) {
|
if (sessionManager.userName != null) {
|
||||||
userName
|
userName
|
||||||
?.let { userName ->
|
?.let { userName ->
|
||||||
|
|
@ -65,12 +77,15 @@ class ContributionBoundaryCallback
|
||||||
Contribution(media = media, state = Contribution.STATE_COMPLETED)
|
Contribution(media = media, state = Contribution.STATE_COMPLETED)
|
||||||
}
|
}
|
||||||
}.subscribeOn(ioThreadScheduler)
|
}.subscribeOn(ioThreadScheduler)
|
||||||
.subscribe(::saveContributionsToDB) { error: Throwable ->
|
.subscribe({ list ->
|
||||||
|
saveContributionsToDB(list, onRefreshFinish)
|
||||||
|
},{ error ->
|
||||||
|
onRefreshFinish()
|
||||||
Timber.e(
|
Timber.e(
|
||||||
"Failed to fetch contributions: %s",
|
"Failed to fetch contributions: %s",
|
||||||
error.message,
|
error.message,
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
}?.let {
|
}?.let {
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
it,
|
it,
|
||||||
|
|
@ -83,13 +98,16 @@ class ContributionBoundaryCallback
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the contributions the the local DB
|
* Saves the contributions the the local DB
|
||||||
|
*
|
||||||
|
* @param onRefreshFinish callback to invoke when successfully saved to DB.
|
||||||
*/
|
*/
|
||||||
private fun saveContributionsToDB(contributions: List<Contribution>) {
|
private fun saveContributionsToDB(contributions: List<Contribution>, onRefreshFinish: () -> Unit) {
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
repository
|
repository
|
||||||
.save(contributions)
|
.save(contributions)
|
||||||
.subscribeOn(ioThreadScheduler)
|
.subscribeOn(ioThreadScheduler)
|
||||||
.subscribe { longs: List<Long?>? ->
|
.subscribe { longs: List<Long?>? ->
|
||||||
|
onRefreshFinish()
|
||||||
repository["last_fetch_timestamp"] = System.currentTimeMillis()
|
repository["last_fetch_timestamp"] = System.currentTimeMillis()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
import fr.free.nrw.commons.BasePresenter;
|
import fr.free.nrw.commons.BasePresenter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -17,5 +18,8 @@ public class ContributionsListContract {
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface UserActionListener extends BasePresenter<View> {
|
public interface UserActionListener extends BasePresenter<View> {
|
||||||
|
|
||||||
|
void refreshList(SwipeRefreshLayout swipeRefreshLayout);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,15 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
||||||
|
|
||||||
initAdapter();
|
initAdapter();
|
||||||
|
|
||||||
|
// pull down to refresh only enabled for self user.
|
||||||
|
if(Objects.equals(sessionManager.getUserName(), userName)){
|
||||||
|
binding.swipeRefreshLayout.setOnRefreshListener(() -> {
|
||||||
|
contributionsListPresenter.refreshList(binding.swipeRefreshLayout);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
binding.swipeRefreshLayout.setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
return binding.getRoot();
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,15 @@ import androidx.paging.DataSource;
|
||||||
import androidx.paging.DataSource.Factory;
|
import androidx.paging.DataSource.Factory;
|
||||||
import androidx.paging.LivePagedListBuilder;
|
import androidx.paging.LivePagedListBuilder;
|
||||||
import androidx.paging.PagedList;
|
import androidx.paging.PagedList;
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsListContract.UserActionListener;
|
import fr.free.nrw.commons.contributions.ContributionsListContract.UserActionListener;
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
|
||||||
import io.reactivex.Scheduler;
|
import io.reactivex.Scheduler;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
import kotlin.Unit;
|
||||||
|
import kotlin.jvm.functions.Function0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The presenter class for Contributions
|
* The presenter class for Contributions
|
||||||
|
|
@ -95,4 +96,17 @@ public class ContributionsListPresenter implements UserActionListener {
|
||||||
contributionBoundaryCallback.dispose();
|
contributionBoundaryCallback.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is used to refresh list.
|
||||||
|
*
|
||||||
|
* @param swipeRefreshLayout used to stop refresh animation when
|
||||||
|
* refresh finishes.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void refreshList(final SwipeRefreshLayout swipeRefreshLayout) {
|
||||||
|
contributionBoundaryCallback.refreshList(() -> {
|
||||||
|
swipeRefreshLayout.setRefreshing(false);
|
||||||
|
return Unit.INSTANCE;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,11 @@ import android.app.NotificationManager;
|
||||||
import android.app.WallpaperManager;
|
import android.app.WallpaperManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.work.Data;
|
|
||||||
import androidx.work.Worker;
|
import androidx.work.Worker;
|
||||||
import androidx.work.WorkerParameters;
|
import androidx.work.WorkerParameters;
|
||||||
import com.facebook.common.executors.CallerThreadExecutor;
|
import com.facebook.common.executors.CallerThreadExecutor;
|
||||||
|
|
@ -25,7 +22,6 @@ import com.facebook.imagepipeline.image.CloseableImage;
|
||||||
import com.facebook.imagepipeline.request.ImageRequest;
|
import com.facebook.imagepipeline.request.ImageRequest;
|
||||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import java.io.IOException;
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class SetWallpaperWorker extends Worker {
|
public class SetWallpaperWorker extends Worker {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ object FolderDeletionHelper {
|
||||||
|
|
||||||
AlertDialog.Builder(context)
|
AlertDialog.Builder(context)
|
||||||
.setTitle(context.getString(R.string.custom_selector_confirm_deletion_title))
|
.setTitle(context.getString(R.string.custom_selector_confirm_deletion_title))
|
||||||
|
.setCancelable(false)
|
||||||
.setMessage(
|
.setMessage(
|
||||||
context.getString(
|
context.getString(
|
||||||
R.string.custom_selector_confirm_deletion_message,
|
R.string.custom_selector_confirm_deletion_message,
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,7 @@ class CustomSelectorActivity :
|
||||||
val selectedImages: ArrayList<Image> =
|
val selectedImages: ArrayList<Image> =
|
||||||
result.data!!
|
result.data!!
|
||||||
.getParcelableArrayListExtra(CustomSelectorConstants.NEW_SELECTED_IMAGES)!!
|
.getParcelableArrayListExtra(CustomSelectorConstants.NEW_SELECTED_IMAGES)!!
|
||||||
viewModel?.selectedImages?.value = selectedImages
|
viewModel.selectedImages?.value = selectedImages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,6 +268,7 @@ class CustomSelectorActivity :
|
||||||
*/
|
*/
|
||||||
private fun showWelcomeDialog() {
|
private fun showWelcomeDialog() {
|
||||||
val dialog = Dialog(this)
|
val dialog = Dialog(this)
|
||||||
|
dialog.setCancelable(false)
|
||||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
dialog.setContentView(R.layout.custom_selector_info_dialog)
|
dialog.setContentView(R.layout.custom_selector_info_dialog)
|
||||||
(dialog.findViewById(R.id.btn_ok) as Button).setOnClickListener { dialog.dismiss() }
|
(dialog.findViewById(R.id.btn_ok) as Button).setOnClickListener { dialog.dismiss() }
|
||||||
|
|
@ -683,6 +684,7 @@ class CustomSelectorActivity :
|
||||||
*/
|
*/
|
||||||
private fun displayUploadLimitWarning() {
|
private fun displayUploadLimitWarning() {
|
||||||
val dialog = Dialog(this)
|
val dialog = Dialog(this)
|
||||||
|
dialog.setCancelable(false)
|
||||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
dialog.setContentView(R.layout.custom_selector_limit_dialog)
|
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.btn_dismiss_limit_warning) as Button).setOnClickListener { dialog.dismiss() }
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class DBOpenHelper(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DATABASE_NAME = "commons.db"
|
private const val DATABASE_NAME = "commons.db"
|
||||||
private const val DATABASE_VERSION = 20
|
private const val DATABASE_VERSION = 21
|
||||||
const val CONTRIBUTIONS_TABLE = "contributions"
|
const val CONTRIBUTIONS_TABLE = "contributions"
|
||||||
private const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS %s"
|
private const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS %s"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package fr.free.nrw.commons.db
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
|
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao
|
||||||
|
import fr.free.nrw.commons.bookmarks.category.BookmarksCategoryModal
|
||||||
import fr.free.nrw.commons.contributions.Contribution
|
import fr.free.nrw.commons.contributions.Contribution
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao
|
import fr.free.nrw.commons.contributions.ContributionDao
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
||||||
|
|
@ -21,8 +23,8 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class],
|
entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class, BookmarksCategoryModal::class],
|
||||||
version = 18,
|
version = 19,
|
||||||
exportSchema = false,
|
exportSchema = false,
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
@ -38,4 +40,6 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun NotForUploadStatusDao(): NotForUploadStatusDao
|
abstract fun NotForUploadStatusDao(): NotForUploadStatusDao
|
||||||
|
|
||||||
abstract fun ReviewDao(): ReviewDao
|
abstract fun ReviewDao(): ReviewDao
|
||||||
|
|
||||||
|
abstract fun bookmarkCategoriesDao(): BookmarkCategoriesDao
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,7 @@ class DescriptionEditActivity :
|
||||||
applicationContext,
|
applicationContext,
|
||||||
media,
|
media,
|
||||||
updatedWikiText,
|
updatedWikiText,
|
||||||
)?.subscribeOn(Schedulers.io())
|
).subscribeOn(Schedulers.io())
|
||||||
?.observeOn(AndroidSchedulers.mainThread())
|
?.observeOn(AndroidSchedulers.mainThread())
|
||||||
?.subscribe(Consumer<Boolean> { s: Boolean? -> Timber.d("Descriptions are added.") })
|
?.subscribe(Consumer<Boolean> { s: Boolean? -> Timber.d("Descriptions are added.") })
|
||||||
?.let {
|
?.let {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import dagger.Provides
|
||||||
import fr.free.nrw.commons.BuildConfig
|
import fr.free.nrw.commons.BuildConfig
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
|
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao
|
import fr.free.nrw.commons.contributions.ContributionDao
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
||||||
|
|
@ -221,6 +222,10 @@ open class CommonsApplicationModule(private val applicationContext: Context) {
|
||||||
fun providesReviewDao(appDatabase: AppDatabase): ReviewDao =
|
fun providesReviewDao(appDatabase: AppDatabase): ReviewDao =
|
||||||
appDatabase.ReviewDao()
|
appDatabase.ReviewDao()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun providesBookmarkCategoriesDao (appDatabase: AppDatabase): BookmarkCategoriesDao =
|
||||||
|
appDatabase.bookmarkCategoriesDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun providesContentResolver(context: Context): ContentResolver =
|
fun providesContentResolver(context: Context): ContentResolver =
|
||||||
context.contentResolver
|
context.contentResolver
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
import fr.free.nrw.commons.bookmarks.BookmarkFragment
|
import fr.free.nrw.commons.bookmarks.BookmarkFragment
|
||||||
import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment
|
import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment
|
||||||
|
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment
|
||||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment
|
||||||
|
|
@ -97,6 +98,9 @@ abstract class FragmentBuilderModule {
|
||||||
@ContributesAndroidInjector(modules = [BookmarkItemsFragmentModule::class])
|
@ContributesAndroidInjector(modules = [BookmarkItemsFragmentModule::class])
|
||||||
abstract fun bindBookmarkItemListFragment(): BookmarkItemsFragment
|
abstract fun bindBookmarkItemListFragment(): BookmarkItemsFragment
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun bindBookmarkCategoriesListFragment(): BookmarkCategoriesFragment
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun bindReviewOutOfContextFragment(): ReviewImageFragment
|
abstract fun bindReviewOutOfContextFragment(): ReviewImageFragment
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import android.graphics.drawable.BitmapDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.location.LocationManager;
|
import android.location.LocationManager;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
|
|
@ -167,7 +168,11 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
setSearchThisAreaButtonVisibility(false);
|
setSearchThisAreaButtonVisibility(false);
|
||||||
binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution), Html.FROM_HTML_MODE_LEGACY));
|
||||||
|
} else {
|
||||||
|
binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
|
||||||
|
}
|
||||||
initNetworkBroadCastReceiver();
|
initNetworkBroadCastReceiver();
|
||||||
locationPermissionsHelper = new LocationPermissionsHelper(getActivity(),locationManager,this);
|
locationPermissionsHelper = new LocationPermissionsHelper(getActivity(),locationManager,this);
|
||||||
if (presenter == null) {
|
if (presenter == null) {
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
|
||||||
.setPositiveButton(android.R.string.yes,
|
.setPositiveButton(android.R.string.yes,
|
||||||
(dialog, which) -> setDeleteRecentPositiveButton(context, dialog))
|
(dialog, which) -> setDeleteRecentPositiveButton(context, dialog))
|
||||||
.setNegativeButton(android.R.string.no, null)
|
.setNegativeButton(android.R.string.no, null)
|
||||||
|
.setCancelable(false)
|
||||||
.create()
|
.create()
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
@ -94,6 +95,7 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
|
||||||
.setPositiveButton(getString(R.string.delete).toUpperCase(Locale.ROOT),
|
.setPositiveButton(getString(R.string.delete).toUpperCase(Locale.ROOT),
|
||||||
((dialog, which) -> setDeletePositiveButton(context, dialog, position)))
|
((dialog, which) -> setDeletePositiveButton(context, dialog, position)))
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setCancelable(false)
|
||||||
.create()
|
.create()
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.feedback
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
|
|
@ -26,11 +27,13 @@ class FeedbackDialog(
|
||||||
private val onFeedbackSubmitCallback: OnFeedbackSubmitCallback) : Dialog(context) {
|
private val onFeedbackSubmitCallback: OnFeedbackSubmitCallback) : Dialog(context) {
|
||||||
private var _binding: DialogFeedbackBinding? = null
|
private var _binding: DialogFeedbackBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
// TODO("Remove Deprecation") Issue : #6002
|
// Refactored to handle deprecation for Html.fromHtml()
|
||||||
// 'fromHtml(String!): Spanned!' is deprecated. Deprecated in Java
|
private var feedbackDestinationHtml: Spanned = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
@Suppress("DEPRECATION")
|
Html.fromHtml(context.getString(R.string.feedback_destination_note), Html.FROM_HTML_MODE_LEGACY)
|
||||||
private var feedbackDestinationHtml: Spanned = Html.fromHtml(
|
} else {
|
||||||
context.getString(R.string.feedback_destination_note))
|
@Suppress("DEPRECATION")
|
||||||
|
Html.fromHtml(context.getString(R.string.feedback_destination_note))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
@ -43,6 +46,7 @@ class FeedbackDialog(
|
||||||
// 'SOFT_INPUT_ADJUST_RESIZE: Int' is deprecated. Deprecated in Java
|
// 'SOFT_INPUT_ADJUST_RESIZE: Int' is deprecated. Deprecated in Java
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||||
|
binding.btnCancel.setOnClickListener { dismiss() }
|
||||||
binding.btnSubmitFeedback.setOnClickListener {
|
binding.btnSubmitFeedback.setOnClickListener {
|
||||||
try {
|
try {
|
||||||
submitFeedback()
|
submitFeedback()
|
||||||
|
|
|
||||||
|
|
@ -263,17 +263,8 @@ object FilePicker : Constants {
|
||||||
) {
|
) {
|
||||||
if (result.resultCode == Activity.RESULT_OK && !isPhoto(result.data)) {
|
if (result.resultCode == Activity.RESULT_OK && !isPhoto(result.data)) {
|
||||||
try {
|
try {
|
||||||
val photoPath = result.data?.data
|
val files = getFilesFromGalleryPictures(result.data, activity)
|
||||||
val photoFile = PickedFiles.pickedExistingPicture(activity, photoPath!!)
|
callbacks.onImagesPicked(files, ImageSource.DOCUMENTS, restoreType(activity))
|
||||||
callbacks.onImagesPicked(
|
|
||||||
singleFileList(photoFile),
|
|
||||||
ImageSource.DOCUMENTS,
|
|
||||||
restoreType(activity)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) {
|
|
||||||
PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
callbacks.onImagePickerError(e, ImageSource.DOCUMENTS, restoreType(activity))
|
callbacks.onImagePickerError(e, ImageSource.DOCUMENTS, restoreType(activity))
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class MimeTypeMapWrapper {
|
||||||
)
|
)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getExtensionFromMimeType(mimeType: String): String? {
|
fun getExtensionFromMimeType(mimeType: String?): String? {
|
||||||
val result = sMimeTypeToExtensionMap[mimeType]
|
val result = sMimeTypeToExtensionMap[mimeType]
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import android.os.Parcelable
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
|
||||||
import fr.free.nrw.commons.upload.FileUtils
|
import fr.free.nrw.commons.upload.FileUtils
|
||||||
|
import fr.free.nrw.commons.upload.ImageCoordinates
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
@ -87,9 +88,7 @@ class UploadableFile : Parcelable {
|
||||||
fun hasLocation(): Boolean {
|
fun hasLocation(): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val exif = ExifInterface(file.absolutePath)
|
val exif = ExifInterface(file.absolutePath)
|
||||||
val latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)
|
ImageCoordinates(exif, null).imageCoordsExists
|
||||||
val longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE)
|
|
||||||
latitude != null && longitude != null
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.tag("UploadableFile").d(e)
|
Timber.tag("UploadableFile").d(e)
|
||||||
false
|
false
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import android.util.Log
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
import ch.qos.logback.classic.LoggerContext
|
import ch.qos.logback.classic.LoggerContext
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import com.google.android.material.snackbar.Snackbar;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.bookmarks.models.Bookmark;
|
import fr.free.nrw.commons.bookmarks.models.Bookmark;
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
|
||||||
|
|
@ -211,6 +212,13 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
||||||
snackbar.show();
|
snackbar.show();
|
||||||
updateBookmarkState(item);
|
updateBookmarkState(item);
|
||||||
return true;
|
return true;
|
||||||
|
case R.id.menu_copy_link:
|
||||||
|
String uri = m.getPageTitle().getCanonicalUri();
|
||||||
|
Utils.copy("shareLink", uri, requireContext());
|
||||||
|
Timber.d("Copied share link to clipboard: %s", uri);
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.menu_link_copied),
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
return true;
|
||||||
case R.id.menu_share_current_image:
|
case R.id.menu_share_current_image:
|
||||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||||
shareIntent.setType("text/plain");
|
shareIntent.setType("text/plain");
|
||||||
|
|
@ -283,6 +291,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
||||||
builder.setItems(R.array.report_violation_options, (dialog, which) -> {
|
builder.setItems(R.array.report_violation_options, (dialog, which) -> {
|
||||||
sendReportEmail(media, values[which]);
|
sendReportEmail(media, values[which]);
|
||||||
});
|
});
|
||||||
|
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {});
|
||||||
builder.setCancelable(false);
|
builder.setCancelable(false);
|
||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
|
@ -390,6 +399,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
||||||
if (m != null) {
|
if (m != null) {
|
||||||
// Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
|
// Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
|
||||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true);
|
menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true);
|
||||||
|
menu.findItem(R.id.menu_copy_link).setEnabled(true).setVisible(true);
|
||||||
menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true);
|
menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true);
|
||||||
menu.findItem(R.id.menu_download_current_image).setEnabled(true).setVisible(true);
|
menu.findItem(R.id.menu_download_current_image).setEnabled(true).setVisible(true);
|
||||||
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true).setVisible(true);
|
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true).setVisible(true);
|
||||||
|
|
@ -423,6 +433,8 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
||||||
case Contribution.STATE_QUEUED:
|
case Contribution.STATE_QUEUED:
|
||||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
|
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
|
||||||
.setVisible(false);
|
.setVisible(false);
|
||||||
|
menu.findItem(R.id.menu_copy_link).setEnabled(false)
|
||||||
|
.setVisible(false);
|
||||||
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
|
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
|
||||||
.setVisible(false);
|
.setVisible(false);
|
||||||
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
|
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
|
||||||
|
|
@ -440,6 +452,8 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
||||||
} else {
|
} else {
|
||||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
|
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
|
||||||
.setVisible(false);
|
.setVisible(false);
|
||||||
|
menu.findItem(R.id.menu_copy_link).setEnabled(false)
|
||||||
|
.setVisible(false);
|
||||||
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
|
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
|
||||||
.setVisible(false);
|
.setVisible(false);
|
||||||
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
|
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.mwapi
|
||||||
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonParser
|
||||||
import fr.free.nrw.commons.BuildConfig
|
import fr.free.nrw.commons.BuildConfig
|
||||||
import fr.free.nrw.commons.campaigns.CampaignResponseDTO
|
import fr.free.nrw.commons.campaigns.CampaignResponseDTO
|
||||||
import fr.free.nrw.commons.explore.depictions.DepictsClient
|
import fr.free.nrw.commons.explore.depictions.DepictsClient
|
||||||
|
|
@ -10,6 +11,7 @@ import fr.free.nrw.commons.fileusages.GlobalFileUsagesResponse
|
||||||
import fr.free.nrw.commons.location.LatLng
|
import fr.free.nrw.commons.location.LatLng
|
||||||
import fr.free.nrw.commons.nearby.Place
|
import fr.free.nrw.commons.nearby.Place
|
||||||
import fr.free.nrw.commons.nearby.model.ItemsClass
|
import fr.free.nrw.commons.nearby.model.ItemsClass
|
||||||
|
import fr.free.nrw.commons.nearby.model.NearbyQueryParams
|
||||||
import fr.free.nrw.commons.nearby.model.NearbyResponse
|
import fr.free.nrw.commons.nearby.model.NearbyResponse
|
||||||
import fr.free.nrw.commons.nearby.model.PlaceBindings
|
import fr.free.nrw.commons.nearby.model.PlaceBindings
|
||||||
import fr.free.nrw.commons.profile.achievements.FeaturedImages
|
import fr.free.nrw.commons.profile.achievements.FeaturedImages
|
||||||
|
|
@ -175,7 +177,7 @@ class OkHttpJsonApiClient @Inject constructor(
|
||||||
.build()
|
.build()
|
||||||
val response: Response = okHttpClient.newCall(request).execute()
|
val response: Response = okHttpClient.newCall(request).execute()
|
||||||
if (response.body != null && response.isSuccessful) {
|
if (response.body != null && response.isSuccessful) {
|
||||||
val json: String = response.body!!.string() ?: return@fromCallable null
|
val json: String = response.body!!.string()
|
||||||
try {
|
try {
|
||||||
return@fromCallable gson.fromJson<UpdateAvatarResponse>(
|
return@fromCallable gson.fromJson<UpdateAvatarResponse>(
|
||||||
json,
|
json,
|
||||||
|
|
@ -330,36 +332,130 @@ class OkHttpJsonApiClient @Inject constructor(
|
||||||
throw Exception(response.message)
|
throw Exception(response.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the count of items in the specified area by querying Wikidata.
|
||||||
|
*
|
||||||
|
* @param queryParams: a `NearbyQueryParam` specifying the geographical area.
|
||||||
|
* @return The count of items in the specified area.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getNearbyItemCount(
|
||||||
|
queryParams: NearbyQueryParams
|
||||||
|
): Int {
|
||||||
|
val wikidataQuery: String = when (queryParams) {
|
||||||
|
is NearbyQueryParams.Rectangular -> {
|
||||||
|
val westCornerLat = queryParams.screenTopRight.latitude
|
||||||
|
val westCornerLong = queryParams.screenTopRight.longitude
|
||||||
|
val eastCornerLat = queryParams.screenBottomLeft.latitude
|
||||||
|
val eastCornerLong = queryParams.screenBottomLeft.longitude
|
||||||
|
FileUtils.readFromResource("/queries/rectangle_query_for_item_count.rq")
|
||||||
|
.replace("\${LAT_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLat))
|
||||||
|
.replace("\${LONG_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLong))
|
||||||
|
.replace("\${LAT_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLat))
|
||||||
|
.replace("\${LONG_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLong))
|
||||||
|
}
|
||||||
|
|
||||||
|
is NearbyQueryParams.Radial -> {
|
||||||
|
FileUtils.readFromResource("/queries/radius_query_for_item_count.rq")
|
||||||
|
.replace(
|
||||||
|
"\${LAT}",
|
||||||
|
String.format(Locale.ROOT, "%.4f", queryParams.center.latitude)
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"\${LONG}",
|
||||||
|
String.format(Locale.ROOT, "%.4f", queryParams.center.longitude)
|
||||||
|
)
|
||||||
|
.replace("\${RAD}", String.format(Locale.ROOT, "%.2f", queryParams.radiusInKm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!!
|
||||||
|
.newBuilder()
|
||||||
|
.addQueryParameter("query", wikidataQuery)
|
||||||
|
.addQueryParameter("format", "json")
|
||||||
|
|
||||||
|
val request: Request = Request.Builder()
|
||||||
|
.url(urlBuilder.build())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = okHttpClient.newCall(request).execute()
|
||||||
|
if (response.body != null && response.isSuccessful) {
|
||||||
|
val json = response.body!!.string()
|
||||||
|
return JsonParser.parseString(json).getAsJsonObject().getAsJsonObject("results")
|
||||||
|
.getAsJsonArray("bindings").get(0).getAsJsonObject().getAsJsonObject("itemCount")
|
||||||
|
.get("value").asInt
|
||||||
|
}
|
||||||
|
throw Exception(response.message)
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getNearbyPlaces(
|
fun getNearbyPlaces(
|
||||||
screenTopRight: LatLng,
|
queryParams: NearbyQueryParams, language: String,
|
||||||
screenBottomLeft: LatLng, language: String,
|
|
||||||
shouldQueryForMonuments: Boolean, customQuery: String?
|
shouldQueryForMonuments: Boolean, customQuery: String?
|
||||||
): List<Place>? {
|
): List<Place>? {
|
||||||
Timber.d("CUSTOM_SPARQL: %s", (customQuery != null).toString())
|
Timber.d("CUSTOM_SPARQL: %s", (customQuery != null).toString())
|
||||||
|
|
||||||
|
val locale = Locale.ROOT;
|
||||||
val wikidataQuery: String = if (customQuery != null) {
|
val wikidataQuery: String = if (customQuery != null) {
|
||||||
customQuery
|
when (queryParams) {
|
||||||
} else if (!shouldQueryForMonuments) {
|
is NearbyQueryParams.Rectangular -> {
|
||||||
FileUtils.readFromResource("/queries/rectangle_query_for_nearby.rq")
|
val westCornerLat = queryParams.screenTopRight.latitude
|
||||||
} else {
|
val westCornerLong = queryParams.screenTopRight.longitude
|
||||||
FileUtils.readFromResource("/queries/rectangle_query_for_nearby_monuments.rq")
|
val eastCornerLat = queryParams.screenBottomLeft.latitude
|
||||||
|
val eastCornerLong = queryParams.screenBottomLeft.longitude
|
||||||
|
customQuery
|
||||||
|
.replace("\${LAT_WEST}", String.format(locale, "%.4f", westCornerLat))
|
||||||
|
.replace("\${LONG_WEST}", String.format(locale, "%.4f", westCornerLong))
|
||||||
|
.replace("\${LAT_EAST}", String.format(locale, "%.4f", eastCornerLat))
|
||||||
|
.replace("\${LONG_EAST}", String.format(locale, "%.4f", eastCornerLong))
|
||||||
|
.replace("\${LANG}", language)
|
||||||
|
}
|
||||||
|
is NearbyQueryParams.Radial -> {
|
||||||
|
Timber.e(
|
||||||
|
"%s%s",
|
||||||
|
"okHttpJsonApiClient.getNearbyPlaces invoked with custom query",
|
||||||
|
"and radial coordinates. This is currently not supported."
|
||||||
|
)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else when (queryParams) {
|
||||||
|
is NearbyQueryParams.Radial -> {
|
||||||
|
val placeHolderQuery: String = if (!shouldQueryForMonuments) {
|
||||||
|
FileUtils.readFromResource("/queries/radius_query_for_nearby.rq")
|
||||||
|
} else {
|
||||||
|
FileUtils.readFromResource("/queries/radius_query_for_nearby_monuments.rq")
|
||||||
|
}
|
||||||
|
placeHolderQuery.replace(
|
||||||
|
"\${LAT}", String.format(locale, "%.4f", queryParams.center.latitude)
|
||||||
|
).replace(
|
||||||
|
"\${LONG}", String.format(locale, "%.4f", queryParams.center.longitude)
|
||||||
|
)
|
||||||
|
.replace("\${RAD}", String.format(locale, "%.2f", queryParams.radiusInKm))
|
||||||
|
}
|
||||||
|
|
||||||
|
is NearbyQueryParams.Rectangular -> {
|
||||||
|
val placeHolderQuery: String = if (!shouldQueryForMonuments) {
|
||||||
|
FileUtils.readFromResource("/queries/rectangle_query_for_nearby.rq")
|
||||||
|
} else {
|
||||||
|
FileUtils.readFromResource("/queries/rectangle_query_for_nearby_monuments.rq")
|
||||||
|
}
|
||||||
|
val westCornerLat = queryParams.screenTopRight.latitude
|
||||||
|
val westCornerLong = queryParams.screenTopRight.longitude
|
||||||
|
val eastCornerLat = queryParams.screenBottomLeft.latitude
|
||||||
|
val eastCornerLong = queryParams.screenBottomLeft.longitude
|
||||||
|
placeHolderQuery
|
||||||
|
.replace("\${LAT_WEST}", String.format(locale, "%.4f", westCornerLat))
|
||||||
|
.replace("\${LONG_WEST}", String.format(locale, "%.4f", westCornerLong))
|
||||||
|
.replace("\${LAT_EAST}", String.format(locale, "%.4f", eastCornerLat))
|
||||||
|
.replace("\${LONG_EAST}", String.format(locale, "%.4f", eastCornerLong))
|
||||||
|
.replace("\${LANG}", language)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val westCornerLat = screenTopRight.latitude
|
|
||||||
val westCornerLong = screenTopRight.longitude
|
|
||||||
val eastCornerLat = screenBottomLeft.latitude
|
|
||||||
val eastCornerLong = screenBottomLeft.longitude
|
|
||||||
|
|
||||||
val query = wikidataQuery
|
|
||||||
.replace("\${LAT_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLat))
|
|
||||||
.replace("\${LONG_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLong))
|
|
||||||
.replace("\${LAT_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLat))
|
|
||||||
.replace("\${LONG_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLong))
|
|
||||||
.replace("\${LANG}", language)
|
|
||||||
val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!!
|
val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!!
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addQueryParameter("query", query)
|
.addQueryParameter("query", wikidataQuery)
|
||||||
.addQueryParameter("format", "json")
|
.addQueryParameter("format", "json")
|
||||||
|
|
||||||
val request: Request = Request.Builder()
|
val request: Request = Request.Builder()
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,10 @@ class MoreBottomSheetFragment : BottomSheetDialogFragment() {
|
||||||
override fun onFeedbackSubmit(feedback: Feedback) {
|
override fun onFeedbackSubmit(feedback: Feedback) {
|
||||||
uploadFeedback(feedback)
|
uploadFeedback(feedback)
|
||||||
}
|
}
|
||||||
}).show()
|
}).apply {
|
||||||
|
setCancelable(false)
|
||||||
|
show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ class MoreBottomSheetLoggedOutFragment : BottomSheetDialogFragment() {
|
||||||
.setMessage(R.string.feedback_sharing_data_alert)
|
.setMessage(R.string.feedback_sharing_data_alert)
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.setPositiveButton(R.string.ok) { _, _ -> sendFeedback() }
|
.setPositiveButton(R.string.ok) { _, _ -> sendFeedback() }
|
||||||
|
.setNegativeButton(R.string.cancel){_,_ -> }
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,8 @@ public class MarkerPlaceGroup {
|
||||||
public boolean getIsBookmarked() {
|
public boolean getIsBookmarked() {
|
||||||
return isBookmarked;
|
return isBookmarked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setIsBookmarked(boolean isBookmarked) {
|
||||||
|
this.isBookmarked = isBookmarked;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -20,8 +20,8 @@ import timber.log.Timber;
|
||||||
|
|
||||||
public class NearbyController extends MapController {
|
public class NearbyController extends MapController {
|
||||||
|
|
||||||
private static final int MAX_RESULTS = 1000;
|
|
||||||
private final NearbyPlaces nearbyPlaces;
|
private final NearbyPlaces nearbyPlaces;
|
||||||
|
public static final int MAX_RESULTS = 1000;
|
||||||
public static double currentLocationSearchRadius = 10.0; //in kilometers
|
public static double currentLocationSearchRadius = 10.0; //in kilometers
|
||||||
public static LatLng currentLocation; // Users latest fetched location
|
public static LatLng currentLocation; // Users latest fetched location
|
||||||
public static LatLng latestSearchLocation; // Can be current and camera target on search this area button is used
|
public static LatLng latestSearchLocation; // Can be current and camera target on search this area button is used
|
||||||
|
|
@ -196,8 +196,9 @@ public class NearbyController extends MapController {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Place> places = nearbyPlaces.getFromWikidataQuery(screenTopRight, screenBottomLeft,
|
List<Place> places = nearbyPlaces.getFromWikidataQuery(currentLatLng, screenTopRight,
|
||||||
Locale.getDefault().getLanguage(), shouldQueryForMonuments, customQuery);
|
screenBottomLeft, Locale.getDefault().getLanguage(), shouldQueryForMonuments,
|
||||||
|
customQuery);
|
||||||
|
|
||||||
if (null != places && places.size() > 0) {
|
if (null != places && places.size() > 0) {
|
||||||
LatLng[] boundaryCoordinates = {
|
LatLng[] boundaryCoordinates = {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
|
import android.location.Location;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import fr.free.nrw.commons.nearby.model.NearbyQueryParams;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -101,6 +103,7 @@ public class NearbyPlaces {
|
||||||
* Retrieves a list of places from a Wikidata query based on screen coordinates and optional
|
* Retrieves a list of places from a Wikidata query based on screen coordinates and optional
|
||||||
* parameters.
|
* parameters.
|
||||||
*
|
*
|
||||||
|
* @param centerPoint The center of the map, used for radius queries if required.
|
||||||
* @param screenTopRight The top right corner of the screen (latitude, longitude).
|
* @param screenTopRight The top right corner of the screen (latitude, longitude).
|
||||||
* @param screenBottomLeft The bottom left corner of the screen (latitude, longitude).
|
* @param screenBottomLeft The bottom left corner of the screen (latitude, longitude).
|
||||||
* @param lang The language for the query.
|
* @param lang The language for the query.
|
||||||
|
|
@ -111,13 +114,70 @@ public class NearbyPlaces {
|
||||||
* @throws Exception If an error occurs during the retrieval process.
|
* @throws Exception If an error occurs during the retrieval process.
|
||||||
*/
|
*/
|
||||||
public List<Place> getFromWikidataQuery(
|
public List<Place> getFromWikidataQuery(
|
||||||
|
final fr.free.nrw.commons.location.LatLng centerPoint,
|
||||||
final fr.free.nrw.commons.location.LatLng screenTopRight,
|
final fr.free.nrw.commons.location.LatLng screenTopRight,
|
||||||
final fr.free.nrw.commons.location.LatLng screenBottomLeft, final String lang,
|
final fr.free.nrw.commons.location.LatLng screenBottomLeft, final String lang,
|
||||||
final boolean shouldQueryForMonuments,
|
final boolean shouldQueryForMonuments,
|
||||||
@Nullable final String customQuery) throws Exception {
|
@Nullable final String customQuery) throws Exception {
|
||||||
return okHttpJsonApiClient
|
if (customQuery != null) {
|
||||||
.getNearbyPlaces(screenTopRight, screenBottomLeft, lang, shouldQueryForMonuments,
|
return okHttpJsonApiClient
|
||||||
customQuery);
|
.getNearbyPlaces(
|
||||||
|
new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft), lang,
|
||||||
|
shouldQueryForMonuments,
|
||||||
|
customQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int lowerLimit = 1000, upperLimit = 1500;
|
||||||
|
|
||||||
|
final float[] results = new float[1];
|
||||||
|
Location.distanceBetween(centerPoint.getLatitude(), screenTopRight.getLongitude(),
|
||||||
|
centerPoint.getLatitude(), screenBottomLeft.getLongitude(), results);
|
||||||
|
final float longGap = results[0] / 1000f;
|
||||||
|
Location.distanceBetween(screenTopRight.getLatitude(), centerPoint.getLongitude(),
|
||||||
|
screenBottomLeft.getLatitude(), centerPoint.getLongitude(), results);
|
||||||
|
final float latGap = results[0] / 1000f;
|
||||||
|
|
||||||
|
if (Math.max(longGap, latGap) < 100f) {
|
||||||
|
final int itemCount = okHttpJsonApiClient.getNearbyItemCount(
|
||||||
|
new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft));
|
||||||
|
if (itemCount < upperLimit) {
|
||||||
|
return okHttpJsonApiClient.getNearbyPlaces(
|
||||||
|
new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft), lang,
|
||||||
|
shouldQueryForMonuments, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// minRadius, targetRadius and maxRadius are radii in decameters
|
||||||
|
// unlike other radii here, which are in kilometers, to avoid looping over
|
||||||
|
// floating point values
|
||||||
|
int minRadius = 0, maxRadius = Math.round(Math.min(300f, Math.min(longGap, latGap))) * 100;
|
||||||
|
int targetRadius = maxRadius / 2;
|
||||||
|
while (minRadius < maxRadius) {
|
||||||
|
targetRadius = minRadius + (maxRadius - minRadius + 1) / 2;
|
||||||
|
final int itemCount = okHttpJsonApiClient.getNearbyItemCount(
|
||||||
|
new NearbyQueryParams.Radial(centerPoint, targetRadius / 100f));
|
||||||
|
if (itemCount >= lowerLimit && itemCount < upperLimit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (targetRadius > maxRadius / 2 && itemCount < lowerLimit / 5) { // fast forward
|
||||||
|
minRadius = targetRadius;
|
||||||
|
targetRadius = minRadius + (maxRadius - minRadius + 1) / 2;
|
||||||
|
minRadius = targetRadius;
|
||||||
|
if (itemCount < lowerLimit / 10 && minRadius < maxRadius) { // fast forward again
|
||||||
|
targetRadius = minRadius + (maxRadius - minRadius + 1) / 2;
|
||||||
|
minRadius = targetRadius;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (itemCount < upperLimit) {
|
||||||
|
minRadius = targetRadius;
|
||||||
|
} else {
|
||||||
|
maxRadius = targetRadius - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return okHttpJsonApiClient.getNearbyPlaces(
|
||||||
|
new NearbyQueryParams.Radial(centerPoint, targetRadius / 100f), lang, shouldQueryForMonuments,
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.room.Embedded;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.PrimaryKey;
|
import androidx.room.PrimaryKey;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
|
|
@ -24,6 +25,7 @@ public class Place implements Parcelable {
|
||||||
public String name;
|
public String name;
|
||||||
private Label label;
|
private Label label;
|
||||||
private String longDescription;
|
private String longDescription;
|
||||||
|
@Embedded
|
||||||
public LatLng location;
|
public LatLng location;
|
||||||
@PrimaryKey @NonNull
|
@PrimaryKey @NonNull
|
||||||
public String entityID;
|
public String entityID;
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ import android.view.View
|
||||||
import android.view.View.GONE
|
import android.view.View.GONE
|
||||||
import android.view.View.INVISIBLE
|
import android.view.View.INVISIBLE
|
||||||
import android.view.View.VISIBLE
|
import android.view.View.VISIBLE
|
||||||
|
import android.widget.RelativeLayout
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
|
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
|
|
@ -39,12 +41,18 @@ fun placeAdapterDelegate(
|
||||||
showOrHideAndScrollToIfLast()
|
showOrHideAndScrollToIfLast()
|
||||||
onItemClick?.invoke(item)
|
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) {
|
if (!hasFocus && nearbyButtonLayout.buttonLayout.isShown) {
|
||||||
nearbyButtonLayout.buttonLayout.visibility = GONE
|
nearbyButtonLayout.buttonLayout.visibility = GONE
|
||||||
} else if (hasFocus && !nearbyButtonLayout.buttonLayout.isShown) {
|
} else if (hasFocus && !nearbyButtonLayout.buttonLayout.isShown) {
|
||||||
showOrHideAndScrollToIfLast()
|
if (bottomSheetBehavior?.state != BottomSheetBehavior.STATE_HIDDEN) {
|
||||||
onItemClick?.invoke(item)
|
showOrHideAndScrollToIfLast()
|
||||||
|
onItemClick?.invoke(item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nearbyButtonLayout.cameraButton.setOnClickListener { onCameraClicked(item, inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult) }
|
nearbyButtonLayout.cameraButton.setOnClickListener { onCameraClicked(item, inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult) }
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.room.Insert;
|
||||||
import androidx.room.OnConflictStrategy;
|
import androidx.room.OnConflictStrategy;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
import io.reactivex.Completable;
|
import io.reactivex.Completable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data Access Object (DAO) for accessing the Place entity in the database.
|
* 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")
|
@Query("SELECT * from place WHERE entityID=:entity")
|
||||||
public abstract Place getPlace(String 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.
|
* Saves a Place object asynchronously into the database.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
package fr.free.nrw.commons.nearby;
|
package fr.free.nrw.commons.nearby;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import io.reactivex.Completable;
|
import io.reactivex.Completable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The LocalDataSource class for Places
|
* The LocalDataSource class for Places
|
||||||
|
|
@ -26,6 +30,81 @@ public class PlacesLocalDataSource {
|
||||||
return placeDao.getPlace(entityID);
|
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.
|
* 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 fr.free.nrw.commons.location.LatLng;
|
||||||
import io.reactivex.Completable;
|
import io.reactivex.Completable;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
import java.util.List;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -39,6 +40,17 @@ public class PlacesRepository {
|
||||||
return localDataSource.fetchPlace(entityID);
|
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.
|
* Clears the Nearby cache on an IO thread.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ class WikidataFeedback : BaseActivity() {
|
||||||
}, { throwable: Throwable? ->
|
}, { throwable: Throwable? ->
|
||||||
Timber.e(throwable!!)
|
Timber.e(throwable!!)
|
||||||
})
|
})
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@ package fr.free.nrw.commons.nearby.contract;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope;
|
||||||
import fr.free.nrw.commons.BaseMarker;
|
import fr.free.nrw.commons.BaseMarker;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
|
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
|
||||||
import fr.free.nrw.commons.nearby.Label;
|
import fr.free.nrw.commons.nearby.Label;
|
||||||
|
import fr.free.nrw.commons.nearby.MarkerPlaceGroup;
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
import fr.free.nrw.commons.nearby.Place;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -16,6 +18,8 @@ public interface NearbyParentFragmentContract {
|
||||||
|
|
||||||
boolean isNetworkConnectionEstablished();
|
boolean isNetworkConnectionEstablished();
|
||||||
|
|
||||||
|
void updateSnackbar(boolean offlinePinsShown);
|
||||||
|
|
||||||
void listOptionMenuItemClicked();
|
void listOptionMenuItemClicked();
|
||||||
|
|
||||||
void populatePlaces(LatLng currentLatLng);
|
void populatePlaces(LatLng currentLatLng);
|
||||||
|
|
@ -68,7 +72,7 @@ public interface NearbyParentFragmentContract {
|
||||||
|
|
||||||
Context getContext();
|
Context getContext();
|
||||||
|
|
||||||
void updateMapMarkers(List<BaseMarker> BaseMarkers);
|
void replaceMarkerOverlays(List<MarkerPlaceGroup> markerPlaceGroups);
|
||||||
|
|
||||||
void filterOutAllMarkers();
|
void filterOutAllMarkers();
|
||||||
|
|
||||||
|
|
@ -89,6 +93,10 @@ public interface NearbyParentFragmentContract {
|
||||||
|
|
||||||
LatLng getMapFocus();
|
LatLng getMapFocus();
|
||||||
|
|
||||||
|
LatLng getScreenTopRight();
|
||||||
|
|
||||||
|
LatLng getScreenBottomLeft();
|
||||||
|
|
||||||
boolean isAdvancedQueryFragmentVisible();
|
boolean isAdvancedQueryFragmentVisible();
|
||||||
|
|
||||||
void showHideAdvancedQueryFragment(boolean shouldShow);
|
void showHideAdvancedQueryFragment(boolean shouldShow);
|
||||||
|
|
@ -120,12 +128,14 @@ public interface NearbyParentFragmentContract {
|
||||||
void filterByMarkerType(List<Label> selectedLabels, int state, boolean filterForPlaceState,
|
void filterByMarkerType(List<Label> selectedLabels, int state, boolean filterForPlaceState,
|
||||||
boolean filterForAllNoneType);
|
boolean filterForAllNoneType);
|
||||||
|
|
||||||
void updateMapMarkersToController(List<BaseMarker> baseMarkers);
|
|
||||||
|
|
||||||
void searchViewGainedFocus();
|
void searchViewGainedFocus();
|
||||||
|
|
||||||
void setCheckboxUnknown();
|
void setCheckboxUnknown();
|
||||||
|
|
||||||
void setAdvancedQuery(String query);
|
void setAdvancedQuery(String query);
|
||||||
|
|
||||||
|
void toggleBookmarkedStatus(Place place);
|
||||||
|
|
||||||
|
void handleMapScrolled(LifecycleCoroutineScope scope, boolean isNetworkAvailable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,8 @@ class CommonPlaceClickActions
|
||||||
AlertDialog
|
AlertDialog
|
||||||
.Builder(activity)
|
.Builder(activity)
|
||||||
.setMessage(R.string.login_alert_message)
|
.setMessage(R.string.login_alert_message)
|
||||||
|
.setCancelable(false)
|
||||||
|
.setNegativeButton(R.string.cancel){_,_ -> }
|
||||||
.setPositiveButton(R.string.login) { dialog, which ->
|
.setPositiveButton(R.string.login) { dialog, which ->
|
||||||
setPositiveButton()
|
setPositiveButton()
|
||||||
}.show()
|
}.show()
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,7 @@ import android.graphics.drawable.Drawable;
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.location.LocationManager;
|
import android.location.LocationManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build.VERSION;
|
import android.os.Build;
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
|
@ -56,6 +55,8 @@ import androidx.appcompat.app.AlertDialog.Builder;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.content.FileProvider;
|
import androidx.core.content.FileProvider;
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope;
|
||||||
|
import androidx.lifecycle.LifecycleOwnerKt;
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
|
@ -64,7 +65,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCa
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.jakewharton.rxbinding2.view.RxView;
|
import com.jakewharton.rxbinding2.view.RxView;
|
||||||
import com.jakewharton.rxbinding3.appcompat.RxSearchView;
|
import com.jakewharton.rxbinding3.appcompat.RxSearchView;
|
||||||
import fr.free.nrw.commons.BaseMarker;
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.CommonsApplication.BaseLogoutListener;
|
import fr.free.nrw.commons.CommonsApplication.BaseLogoutListener;
|
||||||
import fr.free.nrw.commons.MapController.NearbyPlacesInfo;
|
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.NearbyFilterState;
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
import fr.free.nrw.commons.nearby.Place;
|
||||||
import fr.free.nrw.commons.nearby.PlacesRepository;
|
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.WikidataFeedback;
|
||||||
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
|
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
|
||||||
import fr.free.nrw.commons.nearby.fragments.AdvanceQueryFragment.Callback;
|
import fr.free.nrw.commons.nearby.fragments.AdvanceQueryFragment.Callback;
|
||||||
|
|
@ -110,16 +111,12 @@ import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
||||||
import io.reactivex.Completable;
|
import io.reactivex.Completable;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.Disposable;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
@ -155,6 +152,29 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
|
|
||||||
FragmentNearbyParentBinding binding;
|
FragmentNearbyParentBinding binding;
|
||||||
|
|
||||||
|
public final MapEventsOverlay mapEventsOverlay = new MapEventsOverlay(new MapEventsReceiver() {
|
||||||
|
@Override
|
||||||
|
public boolean singleTapConfirmedHelper(GeoPoint p) {
|
||||||
|
if (clickedMarker != null) {
|
||||||
|
clickedMarker.closeInfoWindow();
|
||||||
|
} else {
|
||||||
|
Timber.e("CLICKED MARKER IS NULL");
|
||||||
|
}
|
||||||
|
if (isListBottomSheetExpanded()) {
|
||||||
|
// Back should first hide the bottom sheet if it is expanded
|
||||||
|
hideBottomSheet();
|
||||||
|
} else if (isDetailsBottomSheetVisible()) {
|
||||||
|
hideBottomDetailsSheet();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean longPressHelper(GeoPoint p) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
LocationServiceManager locationManager;
|
LocationServiceManager locationManager;
|
||||||
@Inject
|
@Inject
|
||||||
|
|
@ -189,6 +209,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
private boolean isNetworkErrorOccurred;
|
private boolean isNetworkErrorOccurred;
|
||||||
private Snackbar snackbar;
|
private Snackbar snackbar;
|
||||||
private View view;
|
private View view;
|
||||||
|
private LifecycleCoroutineScope scope;
|
||||||
private NearbyParentFragmentPresenter presenter;
|
private NearbyParentFragmentPresenter presenter;
|
||||||
private boolean isDarkTheme;
|
private boolean isDarkTheme;
|
||||||
private boolean isFABsExpanded;
|
private boolean isFABsExpanded;
|
||||||
|
|
@ -212,12 +233,9 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
private Place nearestPlace;
|
private Place nearestPlace;
|
||||||
private volatile boolean stopQuery;
|
private volatile boolean stopQuery;
|
||||||
|
|
||||||
private boolean isSearchInProgress = false;
|
|
||||||
private final Handler searchHandler = new Handler();
|
private final Handler searchHandler = new Handler();
|
||||||
private Runnable searchRunnable;
|
private Runnable searchRunnable;
|
||||||
private static final long SCROLL_DELAY = 800; // Delay for debounce of onscroll, in milliseconds.
|
|
||||||
|
|
||||||
private List<Place> updatedPlacesList;
|
|
||||||
private LatLng updatedLatLng;
|
private LatLng updatedLatLng;
|
||||||
private boolean searchable;
|
private boolean searchable;
|
||||||
|
|
||||||
|
|
@ -308,10 +326,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
* WLM URL
|
* WLM URL
|
||||||
*/
|
*/
|
||||||
public static final String WLM_URL = "https://commons.wikimedia.org/wiki/Commons:Mobile_app/Contributing_to_WLM_using_the_app";
|
public static final String WLM_URL = "https://commons.wikimedia.org/wiki/Commons:Mobile_app/Contributing_to_WLM_using_the_app";
|
||||||
/**
|
|
||||||
* Saves response of list of places for the first time
|
|
||||||
*/
|
|
||||||
private List<Place> places = new ArrayList<>();
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static NearbyParentFragment newInstance() {
|
public static NearbyParentFragment newInstance() {
|
||||||
|
|
@ -327,7 +341,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
view = binding.getRoot();
|
view = binding.getRoot();
|
||||||
|
|
||||||
initNetworkBroadCastReceiver();
|
initNetworkBroadCastReceiver();
|
||||||
presenter = new NearbyParentFragmentPresenter(bookmarkLocationDao);
|
scope = LifecycleOwnerKt.getLifecycleScope(getViewLifecycleOwner());
|
||||||
|
presenter = new NearbyParentFragmentPresenter(bookmarkLocationDao, placesRepository, nearbyController);
|
||||||
progressDialog = new ProgressDialog(getActivity());
|
progressDialog = new ProgressDialog(getActivity());
|
||||||
progressDialog.setCancelable(false);
|
progressDialog.setCancelable(false);
|
||||||
progressDialog.setMessage("Saving in progress...");
|
progressDialog.setMessage("Saving in progress...");
|
||||||
|
|
@ -452,54 +467,12 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
binding.map.getOverlays().add(scaleBarOverlay);
|
binding.map.getOverlays().add(scaleBarOverlay);
|
||||||
binding.map.getZoomController().setVisibility(Visibility.NEVER);
|
binding.map.getZoomController().setVisibility(Visibility.NEVER);
|
||||||
binding.map.getController().setZoom(ZOOM_LEVEL);
|
binding.map.getController().setZoom(ZOOM_LEVEL);
|
||||||
binding.map.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() {
|
binding.map.getOverlays().add(mapEventsOverlay);
|
||||||
@Override
|
|
||||||
public boolean singleTapConfirmedHelper(GeoPoint p) {
|
|
||||||
if (clickedMarker != null) {
|
|
||||||
clickedMarker.closeInfoWindow();
|
|
||||||
} else {
|
|
||||||
Timber.e("CLICKED MARKER IS NULL");
|
|
||||||
}
|
|
||||||
if (isListBottomSheetExpanded()) {
|
|
||||||
// Back should first hide the bottom sheet if it is expanded
|
|
||||||
hideBottomSheet();
|
|
||||||
} else if (isDetailsBottomSheetVisible()) {
|
|
||||||
hideBottomDetailsSheet();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean longPressHelper(GeoPoint p) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
binding.map.addMapListener(new MapListener() {
|
binding.map.addMapListener(new MapListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onScroll(ScrollEvent event) {
|
public boolean onScroll(ScrollEvent event) {
|
||||||
|
presenter.handleMapScrolled(scope, !isNetworkErrorOccurred);
|
||||||
// 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);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -519,7 +492,12 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
moveCameraToPosition(lastMapFocus);
|
moveCameraToPosition(lastMapFocus);
|
||||||
initRvNearbyList();
|
initRvNearbyList();
|
||||||
onResume();
|
onResume();
|
||||||
binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution), Html.FROM_HTML_MODE_LEGACY));
|
||||||
|
} else {
|
||||||
|
//noinspection deprecation
|
||||||
|
binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
|
||||||
|
}
|
||||||
binding.tvAttribution.setMovementMethod(LinkMovementMethod.getInstance());
|
binding.tvAttribution.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
binding.nearbyFilterList.btnAdvancedOptions.setOnClickListener(v -> {
|
binding.nearbyFilterList.btnAdvancedOptions.setOnClickListener(v -> {
|
||||||
binding.nearbyFilter.searchViewLayout.searchView.clearFocus();
|
binding.nearbyFilter.searchViewLayout.searchView.clearFocus();
|
||||||
|
|
@ -605,8 +583,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
},
|
},
|
||||||
(place, isBookmarked) -> {
|
(place, isBookmarked) -> {
|
||||||
updateMarker(isBookmarked, place, null);
|
presenter.toggleBookmarkedStatus(place);
|
||||||
binding.map.invalidate();
|
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
},
|
},
|
||||||
commonPlaceClickActions,
|
commonPlaceClickActions,
|
||||||
|
|
@ -670,19 +647,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
registerNetworkReceiver();
|
registerNetworkReceiver();
|
||||||
if (isResumed() && ((MainActivity) getActivity()).activeFragment == ActiveFragment.NEARBY) {
|
if (isResumed() && ((MainActivity) getActivity()).activeFragment == ActiveFragment.NEARBY) {
|
||||||
if (locationPermissionsHelper.checkLocationPermission(getActivity())) {
|
if (locationPermissionsHelper.checkLocationPermission(getActivity())) {
|
||||||
if (lastFocusLocation == null && lastKnownLocation == null) {
|
locationPermissionGranted();
|
||||||
locationPermissionGranted();
|
|
||||||
} else{
|
|
||||||
if (updatedPlacesList != null) {
|
|
||||||
if (!updatedPlacesList.isEmpty()) {
|
|
||||||
loadPlacesDataAsync(updatedPlacesList, updatedLatLng);
|
|
||||||
} else {
|
|
||||||
updateMapMarkers(updatedPlacesList, getLastMapFocus(), false);
|
|
||||||
}
|
|
||||||
}else {
|
|
||||||
locationPermissionGranted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
startMapWithoutPermission();
|
startMapWithoutPermission();
|
||||||
}
|
}
|
||||||
|
|
@ -973,7 +938,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateListFragment(final List<Place> placeList) {
|
public void updateListFragment(final List<Place> placeList) {
|
||||||
places = placeList;
|
adapter.clear();
|
||||||
adapter.setItems(placeList);
|
adapter.setItems(placeList);
|
||||||
binding.bottomSheetNearby.noResultsMessage.setVisibility(
|
binding.bottomSheetNearby.noResultsMessage.setVisibility(
|
||||||
placeList.isEmpty() ? View.VISIBLE : View.GONE);
|
placeList.isEmpty() ? View.VISIBLE : View.GONE);
|
||||||
|
|
@ -1075,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
|
* Hide or expand bottom sheet according to states of all sheets
|
||||||
*/
|
*/
|
||||||
|
|
@ -1089,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
|
@Override
|
||||||
public void populatePlaces(final LatLng currentLatLng) {
|
public void populatePlaces(final LatLng currentLatLng) {
|
||||||
IGeoPoint screenTopRight = binding.map.getProjection()
|
// these two variables have historically been assigned values the opposite of what their
|
||||||
.fromPixels(binding.map.getWidth(), 0);
|
// names imply, and quite some existing code depends on this fact
|
||||||
IGeoPoint screenBottomLeft = binding.map.getProjection()
|
LatLng screenTopRightLatLng = getScreenBottomLeft();
|
||||||
.fromPixels(0, binding.map.getHeight());
|
LatLng screenBottomLeftLatLng = getScreenTopRight();
|
||||||
LatLng screenTopRightLatLng = new LatLng(
|
|
||||||
screenBottomLeft.getLatitude(), screenBottomLeft.getLongitude(), 0);
|
|
||||||
LatLng screenBottomLeftLatLng = new LatLng(
|
|
||||||
screenTopRight.getLatitude(), screenTopRight.getLongitude(), 0);
|
|
||||||
|
|
||||||
// When the nearby fragment is opened immediately upon app launch, the {screenTopRightLatLng}
|
// When the nearby fragment is opened immediately upon app launch, the {screenTopRightLatLng}
|
||||||
// and {screenBottomLeftLatLng} variables return {LatLng(0.0,0.0)} as output.
|
// and {screenBottomLeftLatLng} variables return {LatLng(0.0,0.0)} as output.
|
||||||
|
|
@ -1122,19 +1126,19 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
eastCornerLong, 0);
|
eastCornerLong, 0);
|
||||||
if (currentLatLng.equals(
|
if (currentLatLng.equals(
|
||||||
getLastMapFocus())) { // Means we are checking around current location
|
getLastMapFocus())) { // Means we are checking around current location
|
||||||
populatePlacesForCurrentLocation(getLastMapFocus(), screenTopRightLatLng,
|
populatePlacesForCurrentLocation(getMapFocus(), screenTopRightLatLng,
|
||||||
screenBottomLeftLatLng, currentLatLng, null);
|
screenBottomLeftLatLng, currentLatLng, null);
|
||||||
} else {
|
} else {
|
||||||
populatePlacesForAnotherLocation(getLastMapFocus(), screenTopRightLatLng,
|
populatePlacesForAnotherLocation(getMapFocus(), screenTopRightLatLng,
|
||||||
screenBottomLeftLatLng, currentLatLng, null);
|
screenBottomLeftLatLng, currentLatLng, null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (currentLatLng.equals(
|
if (currentLatLng.equals(
|
||||||
getLastMapFocus())) { // Means we are checking around current location
|
getLastMapFocus())) { // Means we are checking around current location
|
||||||
populatePlacesForCurrentLocation(getLastMapFocus(), screenTopRightLatLng,
|
populatePlacesForCurrentLocation(getMapFocus(), screenTopRightLatLng,
|
||||||
screenBottomLeftLatLng, currentLatLng, null);
|
screenBottomLeftLatLng, currentLatLng, null);
|
||||||
} else {
|
} else {
|
||||||
populatePlacesForAnotherLocation(getLastMapFocus(), screenTopRightLatLng,
|
populatePlacesForAnotherLocation(getMapFocus(), screenTopRightLatLng,
|
||||||
screenBottomLeftLatLng, currentLatLng, null);
|
screenBottomLeftLatLng, currentLatLng, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1151,14 +1155,10 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
populatePlaces(currentLatLng);
|
populatePlaces(currentLatLng);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
IGeoPoint screenTopRight = binding.map.getProjection()
|
// these two variables have historically been assigned values the opposite of what their
|
||||||
.fromPixels(binding.map.getWidth(), 0);
|
// names imply, and quite some existing code depends on this fact
|
||||||
IGeoPoint screenBottomLeft = binding.map.getProjection()
|
final LatLng screenTopRightLatLng = getScreenBottomLeft();
|
||||||
.fromPixels(0, binding.map.getHeight());
|
final LatLng screenBottomLeftLatLng = getScreenTopRight();
|
||||||
LatLng screenTopRightLatLng = new LatLng(
|
|
||||||
screenBottomLeft.getLatitude(), screenBottomLeft.getLongitude(), 0);
|
|
||||||
LatLng screenBottomLeftLatLng = new LatLng(
|
|
||||||
screenTopRight.getLatitude(), screenTopRight.getLongitude(), 0);
|
|
||||||
|
|
||||||
if (currentLatLng.equals(lastFocusLocation) || lastFocusLocation == null
|
if (currentLatLng.equals(lastFocusLocation) || lastFocusLocation == null
|
||||||
|| recenterToUserLocation) { // Means we are checking around current location
|
|| recenterToUserLocation) { // Means we are checking around current location
|
||||||
|
|
@ -1174,27 +1174,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reloads the Nearby map
|
* Clears the Nearby local cache and then calls for pin details to be fetched afresh.
|
||||||
* Clears all location markers, refreshes them, reinserts them into the map.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private void reloadMap() {
|
|
||||||
clearAllMarkers(); // Clear the list of markers
|
|
||||||
binding.map.getController().setZoom(ZOOM_LEVEL); // Reset the zoom level
|
|
||||||
binding.map.getController().setCenter(lastMapFocus); // Recenter the focus
|
|
||||||
if (locationPermissionsHelper.checkLocationPermission(getActivity())) {
|
|
||||||
locationPermissionGranted(); // Reload map with user's location
|
|
||||||
} else {
|
|
||||||
startMapWithoutPermission(); // Reload map without user's location
|
|
||||||
}
|
|
||||||
binding.map.invalidate(); // Invalidate the map
|
|
||||||
presenter.updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED); // Restart the map
|
|
||||||
Timber.d("Reloaded Map Successfully");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the Nearby local cache and then calls for the map to be reloaded
|
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
private void emptyCache() {
|
private void emptyCache() {
|
||||||
|
|
@ -1203,7 +1183,22 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
placesRepository.clearCache()
|
placesRepository.clearCache()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.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(
|
.subscribe(
|
||||||
() -> {
|
() -> {
|
||||||
Timber.d("Nearby Cache cleared successfully.");
|
Timber.d("Nearby Cache cleared successfully.");
|
||||||
|
|
@ -1367,13 +1362,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
? getTextBetweenParentheses(
|
? getTextBetweenParentheses(
|
||||||
updatedPlace.getLongDescription()) : updatedPlace.getLongDescription());
|
updatedPlace.getLongDescription()) : updatedPlace.getLongDescription());
|
||||||
marker.showInfoWindow();
|
marker.showInfoWindow();
|
||||||
for (int i = 0; i < updatedPlacesList.size(); i++) {
|
presenter.handlePinClicked(updatedPlace);
|
||||||
Place pl = updatedPlacesList.get(i);
|
savePlaceToDatabase(place);
|
||||||
if (pl.location == updatedPlace.location) {
|
|
||||||
updatedPlacesList.set(i, updatedPlace);
|
|
||||||
savePlaceToDatabase(place);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Drawable icon = ContextCompat.getDrawable(getContext(),
|
Drawable icon = ContextCompat.getDrawable(getContext(),
|
||||||
getIconFor(updatedPlace, isBookMarked));
|
getIconFor(updatedPlace, isBookMarked));
|
||||||
marker.setIcon(icon);
|
marker.setIcon(icon);
|
||||||
|
|
@ -1412,12 +1402,10 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
setProgressBarVisibility(false);
|
setProgressBarVisibility(false);
|
||||||
presenter.lockUnlockNearby(false);
|
presenter.lockUnlockNearby(false);
|
||||||
} else {
|
} else {
|
||||||
updateMapMarkers(nearbyPlacesInfo.placeList, nearbyPlacesInfo.currentLatLng,
|
updateMapMarkers(nearbyPlacesInfo.placeList, searchLatLng, true);
|
||||||
true);
|
|
||||||
lastFocusLocation = searchLatLng;
|
lastFocusLocation = searchLatLng;
|
||||||
lastMapFocus = new GeoPoint(searchLatLng.getLatitude(),
|
lastMapFocus = new GeoPoint(searchLatLng.getLatitude(),
|
||||||
searchLatLng.getLongitude());
|
searchLatLng.getLongitude());
|
||||||
loadPlacesDataAsync(nearbyPlacesInfo.placeList, nearbyPlacesInfo.currentLatLng);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
throwable -> {
|
throwable -> {
|
||||||
|
|
@ -1457,12 +1445,10 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
|
|
||||||
// curLatLng is used to calculate distance from the current location to the place
|
// curLatLng is used to calculate distance from the current location to the place
|
||||||
// and distance is later on populated to the place
|
// and distance is later on populated to the place
|
||||||
updateMapMarkers(nearbyPlacesInfo.placeList, nearbyPlacesInfo.currentLatLng,
|
updateMapMarkers(nearbyPlacesInfo.placeList, searchLatLng, false);
|
||||||
false);
|
|
||||||
lastMapFocus = new GeoPoint(searchLatLng.getLatitude(),
|
lastMapFocus = new GeoPoint(searchLatLng.getLatitude(),
|
||||||
searchLatLng.getLongitude());
|
searchLatLng.getLongitude());
|
||||||
stopQuery();
|
stopQuery();
|
||||||
loadPlacesDataAsync(nearbyPlacesInfo.placeList, nearbyPlacesInfo.currentLatLng);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
throwable -> {
|
throwable -> {
|
||||||
|
|
@ -1475,167 +1461,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loadPlacesDataAsync(List<Place> placeList, LatLng curLatLng) {
|
public void savePlaceToDatabase(Place place) {
|
||||||
List<Place> places = new ArrayList<>(placeList);
|
|
||||||
|
|
||||||
// Instead of loading all pins in a single SPARQL query, we query in batches.
|
|
||||||
// This variable controls the number of pins queried per batch.
|
|
||||||
int batchSize = 3;
|
|
||||||
|
|
||||||
updatedLatLng = curLatLng;
|
|
||||||
updatedPlacesList = new ArrayList<>(placeList);
|
|
||||||
|
|
||||||
// Sorts the places by distance to ensure the nearest pins are ready for the user as soon
|
|
||||||
// as possible.
|
|
||||||
if (VERSION.SDK_INT >= VERSION_CODES.N) {
|
|
||||||
Collections.sort(places,
|
|
||||||
Comparator.comparingDouble(place -> place.getDistanceInDouble(getMapFocus())));
|
|
||||||
}
|
|
||||||
stopQuery = false;
|
|
||||||
processBatchesSequentially(places, batchSize, updatedPlacesList, curLatLng, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes a list of places in batches sequentially. This method handles the asynchronous
|
|
||||||
* processing of places, updating the map markers and updates the list of updated places accordingly.
|
|
||||||
*
|
|
||||||
* @param places The list of Place objects to be processed.
|
|
||||||
* @param batchSize The size of each batch to be processed.
|
|
||||||
* @param updatedPlaceList The list of Place objects to be updated.
|
|
||||||
* @param curLatLng The current location of the user.
|
|
||||||
* @param startIndex The starting index for the current batch.
|
|
||||||
*/
|
|
||||||
@SuppressLint("CheckResult")
|
|
||||||
private void processBatchesSequentially(List<Place> places, int batchSize,
|
|
||||||
List<Place> updatedPlaceList, LatLng curLatLng, int startIndex) {
|
|
||||||
if (startIndex >= places.size() || stopQuery) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int endIndex = Math.min(startIndex + batchSize, places.size());
|
|
||||||
List<Place> batch = places.subList(startIndex, endIndex);
|
|
||||||
for (int i = 0; i < batch.size(); i++) {
|
|
||||||
if (i == batch.size() - 1 && batch.get(i).name != "") {
|
|
||||||
processBatchesSequentially(places, batchSize, updatedPlaceList, curLatLng,
|
|
||||||
endIndex + batchSize);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (batch.get(i).name == "") {
|
|
||||||
if (i == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
processBatchesSequentially(places, batchSize, updatedPlaceList, curLatLng,
|
|
||||||
endIndex + i);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Disposable disposable = processBatch(batch, updatedPlaceList)
|
|
||||||
.subscribe(p -> {
|
|
||||||
if (stopQuery) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!p.isEmpty() && p != updatedPlaceList) {
|
|
||||||
synchronized (updatedPlaceList) {
|
|
||||||
updatedPlaceList.clear();
|
|
||||||
updatedPlaceList.addAll((Collection<? extends Place>) p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateMapMarkers(new ArrayList<>(updatedPlaceList), curLatLng, false);
|
|
||||||
processBatchesSequentially(places, batchSize, updatedPlaceList, curLatLng, endIndex);
|
|
||||||
}, throwable -> {
|
|
||||||
Timber.e(throwable);
|
|
||||||
showErrorMessage(getString(R.string.error_fetching_nearby_places) + throwable.getLocalizedMessage());
|
|
||||||
setFilterState();
|
|
||||||
});
|
|
||||||
|
|
||||||
compositeDisposable.add(disposable);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes a batch of places, updating the provided place list with fetched or updated data.
|
|
||||||
* This method handles the asynchronous fetching and updating of places from the repository.
|
|
||||||
*
|
|
||||||
* @param batch The batch of Place objects to be processed.
|
|
||||||
* @param placeList The list of Place objects to be updated.
|
|
||||||
* @return An Observable emitting the updated list of Place objects.
|
|
||||||
*/
|
|
||||||
private Observable<List<?>> processBatch(List<Place> batch, List<Place> placeList) {
|
|
||||||
List<Place> toBeProcessed = new ArrayList<>();
|
|
||||||
|
|
||||||
List<Observable<Place>> placeObservables = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Place place : batch) {
|
|
||||||
Observable<Place> placeObservable = Observable
|
|
||||||
.fromCallable(() -> {
|
|
||||||
Place fetchedPlace = placesRepository.fetchPlace(place.entityID);
|
|
||||||
return fetchedPlace != null ? fetchedPlace : place;
|
|
||||||
})
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.doOnNext(placeData -> {
|
|
||||||
if (placeData.equals(place)) {
|
|
||||||
toBeProcessed.add(place);
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < placeList.size(); i++) {
|
|
||||||
Place pl = placeList.get(i);
|
|
||||||
if (pl.location.equals(place.location)) {
|
|
||||||
placeList.set(i, placeData);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
placeObservables.add(placeObservable);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Observable.zip(placeObservables, objects -> toBeProcessed)
|
|
||||||
.flatMap(processedList -> {
|
|
||||||
if (processedList.isEmpty()) {
|
|
||||||
return Observable.just(placeList);
|
|
||||||
}
|
|
||||||
return Observable.fromCallable(() -> nearbyController.getPlaces(processedList))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.map(places -> {
|
|
||||||
if (stopQuery) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
if (places == null || places.isEmpty()) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
} else {
|
|
||||||
List<Place> updatedPlaceList = new ArrayList<>(placeList);
|
|
||||||
for (Place place : places) {
|
|
||||||
for (Place foundPlace : placeList) {
|
|
||||||
if (place.siteLinks.getWikidataLink()
|
|
||||||
.equals(foundPlace.siteLinks.getWikidataLink())) {
|
|
||||||
place.location = foundPlace.location;
|
|
||||||
place.distance = foundPlace.distance;
|
|
||||||
place.setMonument(foundPlace.isMonument());
|
|
||||||
int index = updatedPlaceList.indexOf(foundPlace);
|
|
||||||
if (index != -1) {
|
|
||||||
updatedPlaceList.set(index, place);
|
|
||||||
savePlaceToDatabase(place);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updatedPlaceList;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onErrorReturn(throwable -> {
|
|
||||||
Timber.e(throwable);
|
|
||||||
showErrorMessage(getString(R.string.error_fetching_nearby_places) + " "
|
|
||||||
+ throwable.getLocalizedMessage());
|
|
||||||
setFilterState();
|
|
||||||
return Collections.emptyList();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void savePlaceToDatabase(Place place) {
|
|
||||||
compositeDisposable.add(placesRepository
|
compositeDisposable.add(placesRepository
|
||||||
.save(place)
|
.save(place)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
|
|
@ -1661,8 +1487,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
*/
|
*/
|
||||||
private void updateMapMarkers(final List<Place> nearbyPlaces, final LatLng curLatLng,
|
private void updateMapMarkers(final List<Place> nearbyPlaces, final LatLng curLatLng,
|
||||||
final boolean shouldUpdateSelectedMarker) {
|
final boolean shouldUpdateSelectedMarker) {
|
||||||
presenter.updateMapMarkers(nearbyPlaces, curLatLng, shouldUpdateSelectedMarker);
|
presenter.updateMapMarkers(nearbyPlaces, curLatLng, scope);
|
||||||
setFilterState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1798,6 +1623,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
// prompt the user to login
|
// prompt the user to login
|
||||||
new Builder(getContext())
|
new Builder(getContext())
|
||||||
.setMessage(R.string.login_alert_message)
|
.setMessage(R.string.login_alert_message)
|
||||||
|
.setCancelable(false)
|
||||||
|
.setNegativeButton(R.string.cancel, (dialog, which) -> {})
|
||||||
.setPositiveButton(R.string.login, (dialog, which) -> {
|
.setPositiveButton(R.string.login, (dialog, which) -> {
|
||||||
// logout of the app
|
// logout of the app
|
||||||
BaseLogoutListener logoutListener = new BaseLogoutListener(getActivity());
|
BaseLogoutListener logoutListener = new BaseLogoutListener(getActivity());
|
||||||
|
|
@ -1899,13 +1726,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateMapMarkers(final List<BaseMarker> BaseMarkers) {
|
|
||||||
if (binding.map != null) {
|
|
||||||
presenter.updateMapMarkersToController(BaseMarkers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void filterOutAllMarkers() {
|
public void filterOutAllMarkers() {
|
||||||
clearAllMarkers();
|
clearAllMarkers();
|
||||||
|
|
@ -1925,8 +1745,11 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
final boolean displayExists = false;
|
final boolean displayExists = false;
|
||||||
final boolean displayNeedsPhoto= false;
|
final boolean displayNeedsPhoto= false;
|
||||||
final boolean displayWlm = false;
|
final boolean displayWlm = false;
|
||||||
// Remove the previous markers before updating them
|
if (selectedLabels == null || selectedLabels.size() == 0) {
|
||||||
clearAllMarkers();
|
replaceMarkerOverlays(NearbyController.markerLabelList);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final ArrayList<MarkerPlaceGroup> placeGroupsToShow = new ArrayList<>();
|
||||||
for (final MarkerPlaceGroup markerPlaceGroup : NearbyController.markerLabelList) {
|
for (final MarkerPlaceGroup markerPlaceGroup : NearbyController.markerLabelList) {
|
||||||
final Place place = markerPlaceGroup.getPlace();
|
final Place place = markerPlaceGroup.getPlace();
|
||||||
// When label filter is engaged
|
// When label filter is engaged
|
||||||
|
|
@ -1967,19 +1790,12 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldUpdateMarker) {
|
if (shouldUpdateMarker) {
|
||||||
updateMarker(markerPlaceGroup.getIsBookmarked(), place,
|
placeGroupsToShow.add(
|
||||||
NearbyController.currentLocation);
|
new MarkerPlaceGroup(markerPlaceGroup.getIsBookmarked(), place)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (selectedLabels == null || selectedLabels.size() == 0) {
|
replaceMarkerOverlays(placeGroupsToShow);
|
||||||
ArrayList<BaseMarker> markerArrayList = new ArrayList<>();
|
|
||||||
for (final MarkerPlaceGroup markerPlaceGroup : NearbyController.markerLabelList) {
|
|
||||||
BaseMarker nearbyBaseMarker = new BaseMarker();
|
|
||||||
nearbyBaseMarker.setPlace(markerPlaceGroup.getPlace());
|
|
||||||
markerArrayList.add(nearbyBaseMarker);
|
|
||||||
}
|
|
||||||
addMarkersToMap(markerArrayList);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -1987,18 +1803,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
return binding.map == null ? null : getMapFocus();
|
return binding.map == null ? null : getMapFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets marker icon according to marker status. Sets title and distance.
|
|
||||||
*
|
|
||||||
* @param isBookmarked true if place is bookmarked
|
|
||||||
* @param place
|
|
||||||
* @param currentLatLng current location
|
|
||||||
*/
|
|
||||||
public void updateMarker(final boolean isBookmarked, final Place place,
|
|
||||||
@Nullable final LatLng currentLatLng) {
|
|
||||||
addMarkerToMap(place, isBookmarked);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Highlights nearest place when user clicks on home nearby banner
|
* Highlights nearest place when user clicks on home nearby banner
|
||||||
*
|
*
|
||||||
|
|
@ -2052,13 +1856,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Marker convertToMarker(Place place, boolean isBookMarked) {
|
||||||
* Adds a marker representing a place to the map with optional bookmark icon.
|
|
||||||
*
|
|
||||||
* @param place The Place object containing information about the location.
|
|
||||||
* @param isBookMarked A Boolean flag indicating whether the place is bookmarked or not.
|
|
||||||
*/
|
|
||||||
private void addMarkerToMap(Place place, Boolean isBookMarked) {
|
|
||||||
Drawable icon = ContextCompat.getDrawable(getContext(), getIconFor(place, isBookMarked));
|
Drawable icon = ContextCompat.getDrawable(getContext(), getIconFor(place, isBookMarked));
|
||||||
GeoPoint point = new GeoPoint(place.location.getLatitude(), place.location.getLongitude());
|
GeoPoint point = new GeoPoint(place.location.getLatitude(), place.location.getLongitude());
|
||||||
Marker marker = new Marker(binding.map);
|
Marker marker = new Marker(binding.map);
|
||||||
|
|
@ -2072,44 +1870,57 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
place.getLongDescription()) : place.getLongDescription());
|
place.getLongDescription()) : place.getLongDescription());
|
||||||
}
|
}
|
||||||
marker.setTextLabelFontSize(40);
|
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) -> {
|
marker.setOnMarkerClickListener((marker1, mapView) -> {
|
||||||
if (clickedMarker != null) {
|
if (clickedMarker != null) {
|
||||||
clickedMarker.closeInfoWindow();
|
clickedMarker.closeInfoWindow();
|
||||||
}
|
}
|
||||||
clickedMarker = marker1;
|
clickedMarker = marker1;
|
||||||
binding.bottomSheetDetails.dataCircularProgress.setVisibility(View.VISIBLE);
|
if (!isNetworkErrorOccurred) {
|
||||||
binding.bottomSheetDetails.icon.setVisibility(View.GONE);
|
binding.bottomSheetDetails.dataCircularProgress.setVisibility(View.VISIBLE);
|
||||||
binding.bottomSheetDetails.wikiDataLl.setVisibility(View.GONE);
|
binding.bottomSheetDetails.icon.setVisibility(View.GONE);
|
||||||
if (Objects.equals(place.name, "")) {
|
binding.bottomSheetDetails.wikiDataLl.setVisibility(View.GONE);
|
||||||
getPlaceData(place.getWikiDataEntityId(), place, marker1, isBookMarked);
|
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 {
|
} else {
|
||||||
marker.showInfoWindow();
|
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 true;
|
||||||
});
|
});
|
||||||
binding.map.getOverlays().add(marker);
|
return marker;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds multiple markers representing places to the map and handles item gestures.
|
* Adds multiple markers representing places to the map and handles item gestures.
|
||||||
*
|
*
|
||||||
* @param nearbyBaseMarkers The list of Place objects containing information about the
|
* @param markerPlaceGroups The list of marker place groups containing the places and
|
||||||
* locations.
|
* their bookmarked status
|
||||||
*/
|
*/
|
||||||
private void addMarkersToMap(List<BaseMarker> nearbyBaseMarkers) {
|
@Override
|
||||||
|
public void replaceMarkerOverlays(final List<MarkerPlaceGroup> markerPlaceGroups) {
|
||||||
for(int i = 0; i< nearbyBaseMarkers.size(); i++){
|
ArrayList<Marker> newMarkers = new ArrayList<>(markerPlaceGroups.size());
|
||||||
addMarkerToMap(nearbyBaseMarkers.get(i).getPlace(), false);
|
// iterate in reverse so that the nearest pins get rendered on top
|
||||||
|
for (int i = markerPlaceGroups.size() - 1; i >= 0; i--) {
|
||||||
|
newMarkers.add(
|
||||||
|
convertToMarker(markerPlaceGroups.get(i).getPlace(),
|
||||||
|
markerPlaceGroups.get(i).getIsBookmarked())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
clearAllMarkers();
|
||||||
|
binding.map.getOverlays().addAll(newMarkers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts text between the first occurrence of '(' and its corresponding ')' in the input
|
* Extracts text between the first occurrence of '(' and its corresponding ')' in the input
|
||||||
* string.
|
* string.
|
||||||
|
|
@ -2400,7 +2211,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
binding.map.invalidate();
|
binding.map.invalidate();
|
||||||
GeoPoint geoPoint = mapCenter;
|
GeoPoint geoPoint = mapCenter;
|
||||||
if (geoPoint != null) {
|
if (geoPoint != null) {
|
||||||
List<Overlay> overlays = binding.map.getOverlays();
|
|
||||||
ScaleDiskOverlay diskOverlay =
|
ScaleDiskOverlay diskOverlay =
|
||||||
new ScaleDiskOverlay(this.getContext(),
|
new ScaleDiskOverlay(this.getContext(),
|
||||||
geoPoint, 2000, UnitOfMeasure.foot);
|
geoPoint, 2000, UnitOfMeasure.foot);
|
||||||
|
|
@ -2434,28 +2244,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
scaleBarOverlay.setBackgroundPaint(barPaint);
|
scaleBarOverlay.setBackgroundPaint(barPaint);
|
||||||
scaleBarOverlay.enableScaleBar();
|
scaleBarOverlay.enableScaleBar();
|
||||||
binding.map.getOverlays().add(scaleBarOverlay);
|
binding.map.getOverlays().add(scaleBarOverlay);
|
||||||
binding.map.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() {
|
binding.map.getOverlays().add(mapEventsOverlay);
|
||||||
@Override
|
|
||||||
public boolean singleTapConfirmedHelper(GeoPoint p) {
|
|
||||||
if (clickedMarker != null) {
|
|
||||||
clickedMarker.closeInfoWindow();
|
|
||||||
} else {
|
|
||||||
Timber.e("CLICKED MARKER IS NULL");
|
|
||||||
}
|
|
||||||
if (isListBottomSheetExpanded()) {
|
|
||||||
// Back should first hide the bottom sheet if it is expanded
|
|
||||||
hideBottomSheet();
|
|
||||||
} else if (isDetailsBottomSheetVisible()) {
|
|
||||||
hideBottomDetailsSheet();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean longPressHelper(GeoPoint p) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
binding.map.setMultiTouchControls(true);
|
binding.map.setMultiTouchControls(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2510,21 +2299,14 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
@Override
|
@Override
|
||||||
public void onBottomSheetItemClick(@Nullable View view, int position) {
|
public void onBottomSheetItemClick(@Nullable View view, int position) {
|
||||||
BottomSheetItem item = dataList.get(position);
|
BottomSheetItem item = dataList.get(position);
|
||||||
boolean isBookmarked = bookmarkLocationDao.findBookmarkLocation(selectedPlace);
|
|
||||||
switch (item.getImageResourceId()) {
|
switch (item.getImageResourceId()) {
|
||||||
case R.drawable.ic_round_star_border_24px:
|
case R.drawable.ic_round_star_border_24px:
|
||||||
bookmarkLocationDao.updateBookmarkLocation(selectedPlace);
|
presenter.toggleBookmarkedStatus(selectedPlace);
|
||||||
updateBookmarkButtonImage(selectedPlace);
|
updateBookmarkButtonImage(selectedPlace);
|
||||||
isBookmarked = bookmarkLocationDao.findBookmarkLocation(selectedPlace);
|
|
||||||
updateMarker(isBookmarked, selectedPlace, locationManager.getLastLocation());
|
|
||||||
binding.map.invalidate();
|
|
||||||
break;
|
break;
|
||||||
case R.drawable.ic_round_star_filled_24px:
|
case R.drawable.ic_round_star_filled_24px:
|
||||||
bookmarkLocationDao.updateBookmarkLocation(selectedPlace);
|
presenter.toggleBookmarkedStatus(selectedPlace);
|
||||||
updateBookmarkButtonImage(selectedPlace);
|
updateBookmarkButtonImage(selectedPlace);
|
||||||
isBookmarked = bookmarkLocationDao.findBookmarkLocation(selectedPlace);
|
|
||||||
updateMarker(isBookmarked, selectedPlace, locationManager.getLastLocation());
|
|
||||||
binding.map.invalidate();
|
|
||||||
break;
|
break;
|
||||||
case R.drawable.ic_directions_black_24dp:
|
case R.drawable.ic_directions_black_24dp:
|
||||||
Utils.handleGeoCoordinates(this.getContext(), selectedPlace.getLocation());
|
Utils.handleGeoCoordinates(this.getContext(), selectedPlace.getLocation());
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package fr.free.nrw.commons.nearby.model
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.location.LatLng
|
||||||
|
|
||||||
|
sealed class NearbyQueryParams {
|
||||||
|
class Rectangular(val screenTopRight: LatLng, val screenBottomLeft: LatLng) :
|
||||||
|
NearbyQueryParams()
|
||||||
|
|
||||||
|
class Radial(val center: LatLng, val radiusInKm: Float) : NearbyQueryParams()
|
||||||
|
}
|
||||||
|
|
@ -1,368 +0,0 @@
|
||||||
package fr.free.nrw.commons.nearby.presenter;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.CUSTOM_QUERY;
|
|
||||||
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
|
|
||||||
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED;
|
|
||||||
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED;
|
|
||||||
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.SEARCH_CUSTOM_AREA;
|
|
||||||
import static fr.free.nrw.commons.nearby.CheckBoxTriStates.CHECKED;
|
|
||||||
import static fr.free.nrw.commons.nearby.CheckBoxTriStates.UNCHECKED;
|
|
||||||
import static fr.free.nrw.commons.nearby.CheckBoxTriStates.UNKNOWN;
|
|
||||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
|
||||||
|
|
||||||
import android.location.Location;
|
|
||||||
import androidx.annotation.MainThread;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
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;
|
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
|
|
||||||
import fr.free.nrw.commons.location.LocationUpdateListener;
|
|
||||||
import fr.free.nrw.commons.nearby.CheckBoxTriStates;
|
|
||||||
import fr.free.nrw.commons.nearby.Label;
|
|
||||||
import fr.free.nrw.commons.nearby.MarkerPlaceGroup;
|
|
||||||
import fr.free.nrw.commons.nearby.NearbyController;
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
|
||||||
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
|
|
||||||
import fr.free.nrw.commons.utils.LocationUtils;
|
|
||||||
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
|
||||||
import java.lang.reflect.Proxy;
|
|
||||||
import java.util.List;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class NearbyParentFragmentPresenter
|
|
||||||
implements NearbyParentFragmentContract.UserActions,
|
|
||||||
WikidataEditListener.WikidataP18EditListener,
|
|
||||||
LocationUpdateListener {
|
|
||||||
|
|
||||||
private boolean isNearbyLocked;
|
|
||||||
private LatLng currentLatLng;
|
|
||||||
|
|
||||||
private boolean placesLoadedOnce;
|
|
||||||
|
|
||||||
BookmarkLocationsDao bookmarkLocationDao;
|
|
||||||
|
|
||||||
private @Nullable String customQuery;
|
|
||||||
|
|
||||||
private static final NearbyParentFragmentContract.View DUMMY = (NearbyParentFragmentContract.View) Proxy.newProxyInstance(
|
|
||||||
NearbyParentFragmentContract.View.class.getClassLoader(),
|
|
||||||
new Class[]{NearbyParentFragmentContract.View.class}, (proxy, method, args) -> {
|
|
||||||
if (method.getName().equals("onMyEvent")) {
|
|
||||||
return null;
|
|
||||||
} else if (String.class == method.getReturnType()) {
|
|
||||||
return "";
|
|
||||||
} else if (Integer.class == method.getReturnType()) {
|
|
||||||
return Integer.valueOf(0);
|
|
||||||
} else if (int.class == method.getReturnType()) {
|
|
||||||
return 0;
|
|
||||||
} else if (Boolean.class == method.getReturnType()) {
|
|
||||||
return Boolean.FALSE;
|
|
||||||
} else if (boolean.class == method.getReturnType()) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
private NearbyParentFragmentContract.View nearbyParentFragmentView = DUMMY;
|
|
||||||
|
|
||||||
|
|
||||||
public NearbyParentFragmentPresenter(BookmarkLocationsDao bookmarkLocationDao) {
|
|
||||||
this.bookmarkLocationDao = bookmarkLocationDao;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void attachView(NearbyParentFragmentContract.View view) {
|
|
||||||
this.nearbyParentFragmentView = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void detachView() {
|
|
||||||
this.nearbyParentFragmentView = DUMMY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeNearbyPreferences(JsonKvStore applicationKvStore) {
|
|
||||||
Timber.d("Remove place objects");
|
|
||||||
applicationKvStore.remove(PLACE_OBJECT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initializeMapOperations() {
|
|
||||||
lockUnlockNearby(false);
|
|
||||||
updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED);
|
|
||||||
nearbyParentFragmentView.setCheckBoxAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets click listeners of FABs, and 2 bottom sheets
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void setActionListeners(JsonKvStore applicationKvStore) {
|
|
||||||
nearbyParentFragmentView.setFABPlusAction(v -> {
|
|
||||||
if (applicationKvStore.getBoolean("login_skipped", false)) {
|
|
||||||
// prompt the user to login
|
|
||||||
nearbyParentFragmentView.displayLoginSkippedWarning();
|
|
||||||
} else {
|
|
||||||
nearbyParentFragmentView.animateFABs();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
nearbyParentFragmentView.setFABRecenterAction(v -> {
|
|
||||||
nearbyParentFragmentView.recenterMap(currentLatLng);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean backButtonClicked() {
|
|
||||||
if (nearbyParentFragmentView.isAdvancedQueryFragmentVisible()) {
|
|
||||||
nearbyParentFragmentView.showHideAdvancedQueryFragment(false);
|
|
||||||
return true;
|
|
||||||
} else if (nearbyParentFragmentView.isListBottomSheetExpanded()) {
|
|
||||||
// Back should first hide the bottom sheet if it is expanded
|
|
||||||
nearbyParentFragmentView.listOptionMenuItemClicked();
|
|
||||||
return true;
|
|
||||||
} else if (nearbyParentFragmentView.isDetailsBottomSheetVisible()) {
|
|
||||||
nearbyParentFragmentView.setBottomSheetDetailsSmaller();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void markerUnselected() {
|
|
||||||
nearbyParentFragmentView.hideBottomSheet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nearby updates takes time, since they are network operations. During update time, we don't
|
|
||||||
* want to get any other calls from user. So locking nearby.
|
|
||||||
*
|
|
||||||
* @param isNearbyLocked true means lock, false means unlock
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void lockUnlockNearby(boolean isNearbyLocked) {
|
|
||||||
this.isNearbyLocked = isNearbyLocked;
|
|
||||||
if (isNearbyLocked) {
|
|
||||||
nearbyParentFragmentView.disableFABRecenter();
|
|
||||||
} else {
|
|
||||||
nearbyParentFragmentView.enableFABRecenter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method should be the single point to update Map and List. Triggered by location changes
|
|
||||||
*
|
|
||||||
* @param locationChangeType defines if location changed significantly or slightly
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void updateMapAndList(LocationChangeType locationChangeType) {
|
|
||||||
Timber.d("Presenter updates map and list");
|
|
||||||
if (isNearbyLocked) {
|
|
||||||
Timber.d("Nearby is locked, so updateMapAndList returns");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nearbyParentFragmentView.isNetworkConnectionEstablished()) {
|
|
||||||
Timber.d("Network connection is not established");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LatLng lastLocation = nearbyParentFragmentView.getLastMapFocus();
|
|
||||||
if (nearbyParentFragmentView.getMapCenter() != null) {
|
|
||||||
currentLatLng = nearbyParentFragmentView.getMapCenter();
|
|
||||||
} else {
|
|
||||||
currentLatLng = lastLocation;
|
|
||||||
}
|
|
||||||
if (currentLatLng == null) {
|
|
||||||
Timber.d("Skipping update of nearby places as location is unavailable");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Significant changed - Markers and current location will be updated together
|
|
||||||
* Slightly changed - Only current position marker will be updated
|
|
||||||
*/
|
|
||||||
if (locationChangeType.equals(CUSTOM_QUERY)) {
|
|
||||||
Timber.d("ADVANCED_QUERY_SEARCH");
|
|
||||||
lockUnlockNearby(true);
|
|
||||||
nearbyParentFragmentView.setProgressBarVisibility(true);
|
|
||||||
LatLng updatedLocationByUser = LocationUtils.deriveUpdatedLocationFromSearchQuery(
|
|
||||||
customQuery);
|
|
||||||
if (updatedLocationByUser == null) {
|
|
||||||
updatedLocationByUser = lastLocation;
|
|
||||||
}
|
|
||||||
nearbyParentFragmentView.populatePlaces(updatedLocationByUser, customQuery);
|
|
||||||
} else if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)
|
|
||||||
|| locationChangeType.equals(MAP_UPDATED)) {
|
|
||||||
lockUnlockNearby(true);
|
|
||||||
nearbyParentFragmentView.setProgressBarVisibility(true);
|
|
||||||
nearbyParentFragmentView.populatePlaces(nearbyParentFragmentView.getMapCenter());
|
|
||||||
} else if (locationChangeType.equals(SEARCH_CUSTOM_AREA)) {
|
|
||||||
Timber.d("SEARCH_CUSTOM_AREA");
|
|
||||||
lockUnlockNearby(true);
|
|
||||||
nearbyParentFragmentView.setProgressBarVisibility(true);
|
|
||||||
nearbyParentFragmentView.populatePlaces(nearbyParentFragmentView.getMapFocus());
|
|
||||||
} else { // Means location changed slightly, ie user is walking or driving.
|
|
||||||
Timber.d("Means location changed slightly");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Populates places for custom location, should be used for finding nearby places around a
|
|
||||||
* location where you are not at.
|
|
||||||
*
|
|
||||||
* @param nearbyPlaces This variable has placeToCenter list information and distances.
|
|
||||||
*/
|
|
||||||
public void updateMapMarkers(List<Place> nearbyPlaces, LatLng currentLatLng,
|
|
||||||
boolean shouldTrackPosition) {
|
|
||||||
if (null != nearbyParentFragmentView) {
|
|
||||||
nearbyParentFragmentView.clearAllMarkers();
|
|
||||||
List<BaseMarker> baseMarkers = NearbyController
|
|
||||||
.loadAttractionsFromLocationToBaseMarkerOptions(currentLatLng,
|
|
||||||
// Curlatlang will be used to calculate distances
|
|
||||||
nearbyPlaces);
|
|
||||||
nearbyParentFragmentView.updateMapMarkers(baseMarkers);
|
|
||||||
lockUnlockNearby(false); // So that new location updates wont come
|
|
||||||
nearbyParentFragmentView.setProgressBarVisibility(false);
|
|
||||||
nearbyParentFragmentView.updateListFragment(nearbyPlaces);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Some centering task may need to wait for map to be ready, if they are requested before map is
|
|
||||||
* ready. So we will remember it when the map is ready
|
|
||||||
*/
|
|
||||||
private void handleCenteringTaskIfAny() {
|
|
||||||
if (!placesLoadedOnce) {
|
|
||||||
placesLoadedOnce = true;
|
|
||||||
nearbyParentFragmentView.centerMapToPlace(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onWikidataEditSuccessful() {
|
|
||||||
updateMapAndList(MAP_UPDATED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLocationChangedSignificantly(LatLng latLng) {
|
|
||||||
Timber.d("Location significantly changed");
|
|
||||||
updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLocationChangedSlightly(LatLng latLng) {
|
|
||||||
Timber.d("Location significantly changed");
|
|
||||||
updateMapAndList(LOCATION_SLIGHTLY_CHANGED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLocationChangedMedium(LatLng latLng) {
|
|
||||||
Timber.d("Location changed medium");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void filterByMarkerType(List<Label> selectedLabels, int state,
|
|
||||||
boolean filterForPlaceState, boolean filterForAllNoneType) {
|
|
||||||
if (filterForAllNoneType) {// Means we will set labels based on states
|
|
||||||
switch (state) {
|
|
||||||
case UNKNOWN:
|
|
||||||
// Do nothing
|
|
||||||
break;
|
|
||||||
case UNCHECKED:
|
|
||||||
//TODO
|
|
||||||
nearbyParentFragmentView.filterOutAllMarkers();
|
|
||||||
nearbyParentFragmentView.setRecyclerViewAdapterItemsGreyedOut();
|
|
||||||
break;
|
|
||||||
case CHECKED:
|
|
||||||
// Despite showing all labels NearbyFilterState still should be applied
|
|
||||||
nearbyParentFragmentView.filterMarkersByLabels(selectedLabels,
|
|
||||||
filterForPlaceState, false);
|
|
||||||
nearbyParentFragmentView.setRecyclerViewAdapterAllSelected();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nearbyParentFragmentView.filterMarkersByLabels(selectedLabels,
|
|
||||||
filterForPlaceState, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@MainThread
|
|
||||||
public void updateMapMarkersToController(List<BaseMarker> baseMarkers) {
|
|
||||||
NearbyController.markerLabelList.clear();
|
|
||||||
for (int i = 0; i < baseMarkers.size(); i++) {
|
|
||||||
BaseMarker nearbyBaseMarker = baseMarkers.get(i);
|
|
||||||
NearbyController.markerLabelList.add(
|
|
||||||
new MarkerPlaceGroup(
|
|
||||||
bookmarkLocationDao.findBookmarkLocation(nearbyBaseMarker.getPlace()),
|
|
||||||
nearbyBaseMarker.getPlace()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setCheckboxUnknown() {
|
|
||||||
nearbyParentFragmentView.setCheckBoxState(CheckBoxTriStates.UNKNOWN);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setAdvancedQuery(String query) {
|
|
||||||
this.customQuery = query;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void searchViewGainedFocus() {
|
|
||||||
if (nearbyParentFragmentView.isListBottomSheetExpanded()) {
|
|
||||||
// Back should first hide the bottom sheet if it is expanded
|
|
||||||
nearbyParentFragmentView.hideBottomSheet();
|
|
||||||
} else if (nearbyParentFragmentView.isDetailsBottomSheetVisible()) {
|
|
||||||
nearbyParentFragmentView.hideBottomDetailsSheet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiates a search for places within the area. Depending on whether the search
|
|
||||||
* is close to the current location, the map and list are updated
|
|
||||||
* accordingly.
|
|
||||||
*/
|
|
||||||
public void searchInTheArea(){
|
|
||||||
if (searchCloseToCurrentLocation()) {
|
|
||||||
updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED);
|
|
||||||
} else {
|
|
||||||
updateMapAndList(SEARCH_CUSTOM_AREA);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if search this area button is used around our current location, so that we can
|
|
||||||
* continue following our current location again
|
|
||||||
*
|
|
||||||
* @return Returns true if search this area button is used around our current location
|
|
||||||
*/
|
|
||||||
public boolean searchCloseToCurrentLocation() {
|
|
||||||
if (null == nearbyParentFragmentView.getLastMapFocus()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
//TODO
|
|
||||||
Location mylocation = new Location("");
|
|
||||||
Location dest_location = new Location("");
|
|
||||||
dest_location.setLatitude(nearbyParentFragmentView.getMapFocus().getLatitude());
|
|
||||||
dest_location.setLongitude(nearbyParentFragmentView.getMapFocus().getLongitude());
|
|
||||||
mylocation.setLatitude(nearbyParentFragmentView.getLastMapFocus().getLatitude());
|
|
||||||
mylocation.setLongitude(nearbyParentFragmentView.getLastMapFocus().getLongitude());
|
|
||||||
Float distance = mylocation.distanceTo(dest_location);
|
|
||||||
|
|
||||||
if (distance > 2000.0 * 3 / 4) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onMapReady() {
|
|
||||||
if (null != nearbyParentFragmentView) {
|
|
||||||
initializeMapOperations();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,657 @@
|
||||||
|
package fr.free.nrw.commons.nearby.presenter
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.MainThread
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import fr.free.nrw.commons.location.LatLng
|
||||||
|
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType
|
||||||
|
import fr.free.nrw.commons.location.LocationUpdateListener
|
||||||
|
import fr.free.nrw.commons.nearby.CheckBoxTriStates
|
||||||
|
import fr.free.nrw.commons.nearby.Label
|
||||||
|
import fr.free.nrw.commons.nearby.MarkerPlaceGroup
|
||||||
|
import fr.free.nrw.commons.nearby.NearbyController
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
import fr.free.nrw.commons.nearby.PlacesRepository
|
||||||
|
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract
|
||||||
|
import fr.free.nrw.commons.utils.LocationUtils
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataEditListener.WikidataP18EditListener
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.lang.reflect.InvocationHandler
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.lang.reflect.Proxy
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
|
class NearbyParentFragmentPresenter
|
||||||
|
(
|
||||||
|
val bookmarkLocationDao: BookmarkLocationsDao,
|
||||||
|
val placesRepository: PlacesRepository,
|
||||||
|
val nearbyController: NearbyController
|
||||||
|
) :
|
||||||
|
NearbyParentFragmentContract.UserActions,
|
||||||
|
WikidataP18EditListener, LocationUpdateListener {
|
||||||
|
|
||||||
|
private var isNearbyLocked = false
|
||||||
|
private var currentLatLng: LatLng? = null
|
||||||
|
|
||||||
|
private var placesLoadedOnce = false
|
||||||
|
|
||||||
|
private var customQuery: String? = null
|
||||||
|
|
||||||
|
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>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* used to tell the asynchronous place detail loading job that a pin was clicked
|
||||||
|
* so as to prevent it from turning grey on the next pin detail update
|
||||||
|
*
|
||||||
|
* @param place the place whose details have already been loaded because clicked pin
|
||||||
|
*/
|
||||||
|
fun handlePinClicked(place: Place) {
|
||||||
|
clickedPlaces.add(place)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the currently running job for async loading of pin details, cancelled when new pins are come
|
||||||
|
private var loadPlacesDataAyncJob: Job? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - **batchSize**: number of places to fetch details of in a single request
|
||||||
|
* - **connnectionCount**: number of parallel requests
|
||||||
|
*/
|
||||||
|
private object LoadPlacesAsyncOptions {
|
||||||
|
const val BATCH_SIZE = 3
|
||||||
|
const val CONNECTION_COUNT = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
private var schedulePlacesUpdateJob: Job? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - **skippedCount**: stores the number of updates skipped
|
||||||
|
* - **skipLimit**: maximum number of consecutive updates that can be skipped
|
||||||
|
* - **skipDelayMs**: The delay (in milliseconds) to wait for a new update.
|
||||||
|
*
|
||||||
|
* @see schedulePlacesUpdate
|
||||||
|
*/
|
||||||
|
private object SchedulePlacesUpdateOptions {
|
||||||
|
var skippedCount = 0
|
||||||
|
const val SKIP_LIMIT = 3
|
||||||
|
const val SKIP_DELAY_MS = 500L
|
||||||
|
}
|
||||||
|
|
||||||
|
// used to tell the asynchronous place detail loading job that the places' bookmarked status
|
||||||
|
// changed so as to prevent inconsistencies
|
||||||
|
private var bookmarkChangedPlaces = CopyOnWriteArrayList<Place>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a UI update for the provided list of `MarkerPlaceGroup` objects. Since, the update
|
||||||
|
* is performed on the main thread, it waits for a `SchedulePlacesUpdateOptions.skipDelayMs`
|
||||||
|
* to see if a new update comes, and if one does, it discards the scheduled UI update.
|
||||||
|
*
|
||||||
|
* @param markerPlaceGroups The new list of `MarkerPlaceGroup` objects. If the list is empty, no
|
||||||
|
* update will be performed.
|
||||||
|
*
|
||||||
|
* @see SchedulePlacesUpdateOptions
|
||||||
|
*/
|
||||||
|
private suspend fun schedulePlacesUpdate(
|
||||||
|
markerPlaceGroups: List<MarkerPlaceGroup>,
|
||||||
|
force: Boolean = false
|
||||||
|
) =
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (markerPlaceGroups.isEmpty()) return@withContext
|
||||||
|
schedulePlacesUpdateJob?.cancel()
|
||||||
|
schedulePlacesUpdateJob = launch {
|
||||||
|
if (!force && SchedulePlacesUpdateOptions.skippedCount++
|
||||||
|
< SchedulePlacesUpdateOptions.SKIP_LIMIT
|
||||||
|
) {
|
||||||
|
delay(SchedulePlacesUpdateOptions.SKIP_DELAY_MS)
|
||||||
|
}
|
||||||
|
SchedulePlacesUpdateOptions.skippedCount = 0
|
||||||
|
updatePlaceGroupsToControllerAndRender(markerPlaceGroups)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the user action of toggling the bookmarked status of a given place. Updates the
|
||||||
|
* bookmark status in the database, updates the UI to reflect the new state.
|
||||||
|
*
|
||||||
|
* @param place The place whose bookmarked status is to be toggled. If the place is `null`,
|
||||||
|
* the operation is skipped.
|
||||||
|
*/
|
||||||
|
override fun toggleBookmarkedStatus(place: Place?) {
|
||||||
|
if (place == null) return
|
||||||
|
val nowBookmarked = bookmarkLocationDao.updateBookmarkLocation(place)
|
||||||
|
bookmarkChangedPlaces.add(place)
|
||||||
|
val placeIndex =
|
||||||
|
NearbyController.markerLabelList.indexOfFirst { it.place.location == place.location }
|
||||||
|
NearbyController.markerLabelList[placeIndex] = MarkerPlaceGroup(
|
||||||
|
nowBookmarked,
|
||||||
|
NearbyController.markerLabelList[placeIndex].place
|
||||||
|
)
|
||||||
|
nearbyParentFragmentView.setFilterState()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachView(view: NearbyParentFragmentContract.View) {
|
||||||
|
this.nearbyParentFragmentView = view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun detachView() {
|
||||||
|
this.nearbyParentFragmentView = DUMMY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeNearbyPreferences(applicationKvStore: JsonKvStore) {
|
||||||
|
Timber.d("Remove place objects")
|
||||||
|
applicationKvStore.remove(PLACE_OBJECT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initializeMapOperations() {
|
||||||
|
lockUnlockNearby(false)
|
||||||
|
updateMapAndList(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)
|
||||||
|
nearbyParentFragmentView.setCheckBoxAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets click listeners of FABs, and 2 bottom sheets
|
||||||
|
*/
|
||||||
|
override fun setActionListeners(applicationKvStore: JsonKvStore?) {
|
||||||
|
nearbyParentFragmentView.setFABPlusAction(View.OnClickListener { v: View? ->
|
||||||
|
if (applicationKvStore != null && applicationKvStore.getBoolean(
|
||||||
|
"login_skipped", false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// prompt the user to login
|
||||||
|
nearbyParentFragmentView.displayLoginSkippedWarning()
|
||||||
|
} else {
|
||||||
|
nearbyParentFragmentView.animateFABs()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
nearbyParentFragmentView.setFABRecenterAction(View.OnClickListener { v: View? ->
|
||||||
|
nearbyParentFragmentView.recenterMap(currentLatLng)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun backButtonClicked(): Boolean {
|
||||||
|
if (nearbyParentFragmentView.isAdvancedQueryFragmentVisible()) {
|
||||||
|
nearbyParentFragmentView.showHideAdvancedQueryFragment(false)
|
||||||
|
return true
|
||||||
|
} else if (nearbyParentFragmentView.isListBottomSheetExpanded()) {
|
||||||
|
// Back should first hide the bottom sheet if it is expanded
|
||||||
|
nearbyParentFragmentView.listOptionMenuItemClicked()
|
||||||
|
return true
|
||||||
|
} else if (nearbyParentFragmentView.isDetailsBottomSheetVisible()) {
|
||||||
|
nearbyParentFragmentView.setBottomSheetDetailsSmaller()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markerUnselected() {
|
||||||
|
nearbyParentFragmentView.hideBottomSheet()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nearby updates takes time, since they are network operations. During update time, we don't
|
||||||
|
* want to get any other calls from user. So locking nearby.
|
||||||
|
*
|
||||||
|
* @param isNearbyLocked true means lock, false means unlock
|
||||||
|
*/
|
||||||
|
override fun lockUnlockNearby(isNearbyLocked: Boolean) {
|
||||||
|
this.isNearbyLocked = isNearbyLocked
|
||||||
|
if (isNearbyLocked) {
|
||||||
|
nearbyParentFragmentView.disableFABRecenter()
|
||||||
|
} else {
|
||||||
|
nearbyParentFragmentView.enableFABRecenter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method should be the single point to update Map and List. Triggered by location changes
|
||||||
|
*
|
||||||
|
* @param locationChangeType defines if location changed significantly or slightly
|
||||||
|
*/
|
||||||
|
override fun updateMapAndList(locationChangeType: LocationChangeType?) {
|
||||||
|
Timber.d("Presenter updates map and list")
|
||||||
|
if (isNearbyLocked) {
|
||||||
|
Timber.d("Nearby is locked, so updateMapAndList returns")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nearbyParentFragmentView.isNetworkConnectionEstablished()) {
|
||||||
|
Timber.d("Network connection is not established")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val lastLocation = nearbyParentFragmentView.getLastMapFocus()
|
||||||
|
currentLatLng = if (nearbyParentFragmentView.getMapCenter() != null) {
|
||||||
|
nearbyParentFragmentView.getMapCenter()
|
||||||
|
} else {
|
||||||
|
lastLocation
|
||||||
|
}
|
||||||
|
if (currentLatLng == null) {
|
||||||
|
Timber.d("Skipping update of nearby places as location is unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Significant changed - Markers and current location will be updated together
|
||||||
|
* Slightly changed - Only current position marker will be updated
|
||||||
|
*/
|
||||||
|
if (locationChangeType == LocationChangeType.CUSTOM_QUERY) {
|
||||||
|
Timber.d("ADVANCED_QUERY_SEARCH")
|
||||||
|
lockUnlockNearby(true)
|
||||||
|
nearbyParentFragmentView.setProgressBarVisibility(true)
|
||||||
|
var updatedLocationByUser = LocationUtils.deriveUpdatedLocationFromSearchQuery(
|
||||||
|
customQuery!!
|
||||||
|
)
|
||||||
|
if (updatedLocationByUser == null) {
|
||||||
|
updatedLocationByUser = lastLocation
|
||||||
|
}
|
||||||
|
nearbyParentFragmentView.populatePlaces(updatedLocationByUser, customQuery)
|
||||||
|
} else if (locationChangeType == LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED
|
||||||
|
|| locationChangeType == LocationChangeType.MAP_UPDATED
|
||||||
|
) {
|
||||||
|
lockUnlockNearby(true)
|
||||||
|
nearbyParentFragmentView.setProgressBarVisibility(true)
|
||||||
|
nearbyParentFragmentView.populatePlaces(nearbyParentFragmentView.getMapCenter())
|
||||||
|
} else if (locationChangeType == LocationChangeType.SEARCH_CUSTOM_AREA) {
|
||||||
|
Timber.d("SEARCH_CUSTOM_AREA")
|
||||||
|
lockUnlockNearby(true)
|
||||||
|
nearbyParentFragmentView.setProgressBarVisibility(true)
|
||||||
|
nearbyParentFragmentView.populatePlaces(nearbyParentFragmentView.getMapFocus())
|
||||||
|
} else { // Means location changed slightly, ie user is walking or driving.
|
||||||
|
Timber.d("Means location changed slightly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update places on the map, and asynchronously load their details from cache and Wikidata query
|
||||||
|
*
|
||||||
|
* @param nearbyPlaces This variable has the list of placecs
|
||||||
|
* @param scope the lifecycle scope of `nearbyParentFragment`'s `viewLifecycleOwner`
|
||||||
|
*/
|
||||||
|
fun updateMapMarkers(
|
||||||
|
nearbyPlaces: List<Place>?, currentLatLng: LatLng,
|
||||||
|
scope: LifecycleCoroutineScope?
|
||||||
|
) {
|
||||||
|
val nearbyPlaceGroups = nearbyPlaces?.sortedBy { it.getDistanceInDouble(currentLatLng) }
|
||||||
|
?.take(NearbyController.MAX_RESULTS)
|
||||||
|
?.map {
|
||||||
|
// currently only the place's location is known but bookmarks are stored by name
|
||||||
|
MarkerPlaceGroup(
|
||||||
|
false,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
?: return
|
||||||
|
|
||||||
|
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()
|
||||||
|
bookmarkChangedPlaces.clear()
|
||||||
|
var clickedPlacesIndex = 0
|
||||||
|
var bookmarkChangedPlacesIndex = 0
|
||||||
|
|
||||||
|
val updatedGroups = nearbyPlaceGroups.toMutableList()
|
||||||
|
// first load cached places:
|
||||||
|
val indicesToUpdate = mutableListOf<Int>()
|
||||||
|
for (i in 0..updatedGroups.lastIndex) {
|
||||||
|
val repoPlace = placesRepository.fetchPlace(updatedGroups[i].place.entityID)
|
||||||
|
if (repoPlace != null && repoPlace.name != null && repoPlace.name != ""){
|
||||||
|
updatedGroups[i].isBookmarked = bookmarkLocationDao.findBookmarkLocation(repoPlace)
|
||||||
|
updatedGroups[i].place.apply {
|
||||||
|
name = repoPlace.name
|
||||||
|
isMonument = repoPlace.isMonument
|
||||||
|
pic = repoPlace.pic ?: ""
|
||||||
|
exists = repoPlace.exists ?: true
|
||||||
|
longDescription = repoPlace.longDescription ?: ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
indicesToUpdate.add(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schedulePlacesUpdate(updatedGroups, force = true)
|
||||||
|
// channel for lists of indices of places, each list to be fetched in a single request
|
||||||
|
val fetchPlacesChannel = Channel<List<Int>>(Channel.UNLIMITED)
|
||||||
|
var totalBatches = 0
|
||||||
|
for (i in indicesToUpdate.indices step LoadPlacesAsyncOptions.BATCH_SIZE) {
|
||||||
|
++totalBatches
|
||||||
|
fetchPlacesChannel.send(
|
||||||
|
indicesToUpdate.slice(
|
||||||
|
i until (i + LoadPlacesAsyncOptions.BATCH_SIZE).coerceAtMost(
|
||||||
|
indicesToUpdate.size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fetchPlacesChannel.close()
|
||||||
|
val collectResults = Channel<List<Pair<Int, MarkerPlaceGroup>>>(totalBatches)
|
||||||
|
repeat(LoadPlacesAsyncOptions.CONNECTION_COUNT) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
for (indices in fetchPlacesChannel) {
|
||||||
|
ensureActive()
|
||||||
|
try {
|
||||||
|
val fetchedPlaces =
|
||||||
|
nearbyController.getPlaces(indices.map { updatedGroups[it].place })
|
||||||
|
collectResults.send(
|
||||||
|
fetchedPlaces.mapIndexed { index, place ->
|
||||||
|
Pair(indices[index], MarkerPlaceGroup(
|
||||||
|
bookmarkLocationDao.findBookmarkLocation(place),
|
||||||
|
place
|
||||||
|
))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("NearbyPinDetails").e(e)
|
||||||
|
collectResults.send(indices.map { Pair(it, updatedGroups[it]) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var collectCount = 0
|
||||||
|
for (resultList in collectResults) {
|
||||||
|
for ((index, fetchedPlaceGroup) in resultList) {
|
||||||
|
val existingPlace = updatedGroups[index].place
|
||||||
|
val finalPlaceGroup = MarkerPlaceGroup(
|
||||||
|
fetchedPlaceGroup.isBookmarked,
|
||||||
|
fetchedPlaceGroup.place.apply {
|
||||||
|
location = existingPlace.location
|
||||||
|
distance = existingPlace.distance
|
||||||
|
isMonument = existingPlace.isMonument
|
||||||
|
}
|
||||||
|
)
|
||||||
|
updatedGroups[index] = finalPlaceGroup
|
||||||
|
placesRepository
|
||||||
|
.save(finalPlaceGroup.place)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
// handle any places clicked
|
||||||
|
if (clickedPlacesIndex < clickedPlaces.size) {
|
||||||
|
val clickedPlacesBacklog = hashMapOf<LatLng, Place>()
|
||||||
|
while (clickedPlacesIndex < clickedPlaces.size) {
|
||||||
|
clickedPlacesBacklog.put(
|
||||||
|
clickedPlaces[clickedPlacesIndex].location,
|
||||||
|
clickedPlaces[clickedPlacesIndex]
|
||||||
|
)
|
||||||
|
++clickedPlacesIndex
|
||||||
|
}
|
||||||
|
for ((index, group) in updatedGroups.withIndex()) {
|
||||||
|
if (clickedPlacesBacklog.containsKey(group.place.location)) {
|
||||||
|
updatedGroups[index] = MarkerPlaceGroup(
|
||||||
|
updatedGroups[index].isBookmarked,
|
||||||
|
clickedPlacesBacklog[group.place.location]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// handle any bookmarks toggled
|
||||||
|
if (bookmarkChangedPlacesIndex < bookmarkChangedPlaces.size) {
|
||||||
|
val bookmarkChangedPlacesBacklog = hashMapOf<LatLng, Place>()
|
||||||
|
while (bookmarkChangedPlacesIndex < bookmarkChangedPlaces.size) {
|
||||||
|
bookmarkChangedPlacesBacklog.put(
|
||||||
|
bookmarkChangedPlaces[bookmarkChangedPlacesIndex].location,
|
||||||
|
bookmarkChangedPlaces[bookmarkChangedPlacesIndex]
|
||||||
|
)
|
||||||
|
++bookmarkChangedPlacesIndex
|
||||||
|
}
|
||||||
|
for ((index, group) in updatedGroups.withIndex()) {
|
||||||
|
if (bookmarkChangedPlacesBacklog.containsKey(group.place.location)) {
|
||||||
|
updatedGroups[index] = MarkerPlaceGroup(
|
||||||
|
bookmarkLocationDao
|
||||||
|
.findBookmarkLocation(updatedGroups[index].place),
|
||||||
|
updatedGroups[index].place
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schedulePlacesUpdate(updatedGroups)
|
||||||
|
if (++collectCount == totalBatches) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collectResults.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some centering task may need to wait for map to be ready, if they are requested before map is
|
||||||
|
* ready. So we will remember it when the map is ready
|
||||||
|
*/
|
||||||
|
private fun handleCenteringTaskIfAny() {
|
||||||
|
if (!placesLoadedOnce) {
|
||||||
|
placesLoadedOnce = true
|
||||||
|
nearbyParentFragmentView.centerMapToPlace(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWikidataEditSuccessful() {
|
||||||
|
updateMapAndList(LocationChangeType.MAP_UPDATED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLocationChangedSignificantly(latLng: LatLng) {
|
||||||
|
Timber.d("Location significantly changed")
|
||||||
|
updateMapAndList(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLocationChangedSlightly(latLng: LatLng) {
|
||||||
|
Timber.d("Location significantly changed")
|
||||||
|
updateMapAndList(LocationChangeType.LOCATION_SLIGHTLY_CHANGED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLocationChangedMedium(latLng: LatLng) {
|
||||||
|
Timber.d("Location changed medium")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun filterByMarkerType(
|
||||||
|
selectedLabels: List<Label?>?, state: Int,
|
||||||
|
filterForPlaceState: Boolean, filterForAllNoneType: Boolean
|
||||||
|
) {
|
||||||
|
if (filterForAllNoneType) { // Means we will set labels based on states
|
||||||
|
when (state) {
|
||||||
|
CheckBoxTriStates.UNKNOWN -> {}
|
||||||
|
CheckBoxTriStates.UNCHECKED -> {
|
||||||
|
//TODO
|
||||||
|
nearbyParentFragmentView.filterOutAllMarkers()
|
||||||
|
nearbyParentFragmentView.setRecyclerViewAdapterItemsGreyedOut()
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBoxTriStates.CHECKED -> {
|
||||||
|
// Despite showing all labels NearbyFilterState still should be applied
|
||||||
|
nearbyParentFragmentView.filterMarkersByLabels(
|
||||||
|
selectedLabels,
|
||||||
|
filterForPlaceState, false
|
||||||
|
)
|
||||||
|
nearbyParentFragmentView.setRecyclerViewAdapterAllSelected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nearbyParentFragmentView.filterMarkersByLabels(
|
||||||
|
selectedLabels,
|
||||||
|
filterForPlaceState, false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the supplied markerPlaceGroups to `NearbyController` and nearby list fragment,
|
||||||
|
* and tells nearby parent fragment to filter the updated values to be rendered as overlays
|
||||||
|
* on the map
|
||||||
|
*
|
||||||
|
* @param markerPlaceGroups the new/updated list of places along with their bookmarked status
|
||||||
|
*/
|
||||||
|
@MainThread
|
||||||
|
private fun updatePlaceGroupsToControllerAndRender(markerPlaceGroups: List<MarkerPlaceGroup>) {
|
||||||
|
NearbyController.markerLabelList.clear()
|
||||||
|
NearbyController.markerLabelList.addAll(markerPlaceGroups)
|
||||||
|
nearbyParentFragmentView.setFilterState()
|
||||||
|
nearbyParentFragmentView.updateListFragment(markerPlaceGroups.map { it.place })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCheckboxUnknown() {
|
||||||
|
nearbyParentFragmentView.setCheckBoxState(CheckBoxTriStates.UNKNOWN)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAdvancedQuery(query: String) {
|
||||||
|
this.customQuery = query
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchViewGainedFocus() {
|
||||||
|
if (nearbyParentFragmentView.isListBottomSheetExpanded()) {
|
||||||
|
// Back should first hide the bottom sheet if it is expanded
|
||||||
|
nearbyParentFragmentView.hideBottomSheet()
|
||||||
|
} else if (nearbyParentFragmentView.isDetailsBottomSheetVisible()) {
|
||||||
|
nearbyParentFragmentView.hideBottomDetailsSheet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates a search for places within the area. Depending on whether the search
|
||||||
|
* is close to the current location, the map and list are updated
|
||||||
|
* accordingly.
|
||||||
|
*/
|
||||||
|
fun searchInTheArea() {
|
||||||
|
if (searchCloseToCurrentLocation()) {
|
||||||
|
updateMapAndList(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)
|
||||||
|
} else {
|
||||||
|
updateMapAndList(LocationChangeType.SEARCH_CUSTOM_AREA)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if search this area button is used around our current location, so that we can
|
||||||
|
* continue following our current location again
|
||||||
|
*
|
||||||
|
* @return Returns true if search this area button is used around our current location
|
||||||
|
*/
|
||||||
|
fun searchCloseToCurrentLocation(): Boolean {
|
||||||
|
if (null == nearbyParentFragmentView.getLastMapFocus()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
//TODO
|
||||||
|
val myLocation = Location("")
|
||||||
|
val destLocation = Location("")
|
||||||
|
destLocation.latitude = nearbyParentFragmentView.getMapFocus().latitude
|
||||||
|
destLocation.longitude = nearbyParentFragmentView.getMapFocus().longitude
|
||||||
|
myLocation.latitude = nearbyParentFragmentView.getLastMapFocus().latitude
|
||||||
|
myLocation.longitude = nearbyParentFragmentView.getLastMapFocus().longitude
|
||||||
|
val distance = myLocation.distanceTo(destLocation)
|
||||||
|
|
||||||
|
return (distance <= 2000.0 * 3 / 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMapReady() {
|
||||||
|
initializeMapOperations()
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
InvocationHandler { proxy: Any?, method: Method?, args: Array<Any?>? ->
|
||||||
|
if (method!!.name == "onMyEvent") {
|
||||||
|
return@InvocationHandler null
|
||||||
|
} else if (String::class.java == method.returnType) {
|
||||||
|
return@InvocationHandler ""
|
||||||
|
} else if (Int::class.java == method.returnType) {
|
||||||
|
return@InvocationHandler 0
|
||||||
|
} else if (Int::class.javaPrimitiveType == method.returnType) {
|
||||||
|
return@InvocationHandler 0
|
||||||
|
} else if (Boolean::class.java == method.returnType) {
|
||||||
|
return@InvocationHandler java.lang.Boolean.FALSE
|
||||||
|
} else if (Boolean::class.javaPrimitiveType == method.returnType) {
|
||||||
|
return@InvocationHandler false
|
||||||
|
} else {
|
||||||
|
return@InvocationHandler null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) as NearbyParentFragmentContract.View
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -139,6 +139,7 @@ class QuizActivity : AppCompatActivity() {
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setMessage(message)
|
.setMessage(message)
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
|
.setNegativeButton(R.string.cancel){_,_ -> }
|
||||||
.setPositiveButton(R.string.continue_message) { dialog, _ ->
|
.setPositiveButton(R.string.continue_message) { dialog, _ ->
|
||||||
questionIndex++
|
questionIndex++
|
||||||
if (questionIndex == quiz.size) {
|
if (questionIndex == quiz.size) {
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ class UploadRepository @Inject constructor(
|
||||||
*
|
*
|
||||||
* @param licenseName
|
* @param licenseName
|
||||||
*/
|
*/
|
||||||
fun setSelectedLicense(licenseName: String) {
|
fun setSelectedLicense(licenseName: String?) {
|
||||||
uploadModel.selectedLicense = licenseName
|
uploadModel.selectedLicense = licenseName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,12 @@ class ReviewImageFragment : CommonsDaggerSupportFragment() {
|
||||||
R.string.review_category_explanation,
|
R.string.review_category_explanation,
|
||||||
formattedCatString
|
formattedCatString
|
||||||
)
|
)
|
||||||
return Html.fromHtml(stringToConvertHtml).toString()
|
val formattedString = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||||
|
Html.fromHtml(stringToConvertHtml, Html.FROM_HTML_MODE_LEGACY).toString()
|
||||||
|
} else {
|
||||||
|
Html.fromHtml(stringToConvertHtml).toString()
|
||||||
|
}
|
||||||
|
return formattedString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getString(R.string.review_no_category)
|
return getString(R.string.review_no_category)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
package fr.free.nrw.commons.settings
|
package fr.free.nrw.commons.settings
|
||||||
|
|
||||||
object Prefs {
|
object Prefs {
|
||||||
const val GLOBAL_PREFS = "fr.free.nrw.commons.preferences"
|
|
||||||
|
|
||||||
const val TRACKING_ENABLED = "eventLogging"
|
|
||||||
const val DEFAULT_LICENSE = "defaultLicense"
|
const val DEFAULT_LICENSE = "defaultLicense"
|
||||||
const val UPLOADS_SHOWING = "uploadsShowing"
|
|
||||||
const val MANAGED_EXIF_TAGS = "managed_exif_tags"
|
const val MANAGED_EXIF_TAGS = "managed_exif_tags"
|
||||||
const val DESCRIPTION_LANGUAGE = "languageDescription"
|
const val DESCRIPTION_LANGUAGE = "languageDescription"
|
||||||
const val APP_UI_LANGUAGE = "appUiLanguage"
|
const val APP_UI_LANGUAGE = "appUiLanguage"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import fr.free.nrw.commons.theme.BaseActivity
|
||||||
class SettingsActivity : BaseActivity() {
|
class SettingsActivity : BaseActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivitySettingsBinding
|
private lateinit var binding: ActivitySettingsBinding
|
||||||
// private var settingsDelegate: AppCompatDelegate? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* to be called when the activity starts
|
* to be called when the activity starts
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package fr.free.nrw.commons.settings
|
package fr.free.nrw.commons.settings
|
||||||
|
|
||||||
import android.Manifest.permission
|
import android.Manifest.permission
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
|
|
@ -13,6 +12,7 @@ import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
|
import android.widget.Button
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ListView
|
import android.widget.ListView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
|
@ -129,7 +129,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
inAppCameraLocationPref?.setOnPreferenceChangeListener { _, newValue ->
|
inAppCameraLocationPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
val isInAppCameraLocationTurnedOn = newValue as Boolean
|
val isInAppCameraLocationTurnedOn = newValue as Boolean
|
||||||
if (isInAppCameraLocationTurnedOn) {
|
if (isInAppCameraLocationTurnedOn) {
|
||||||
createDialogsAndHandleLocationPermissions(requireActivity())
|
createDialogsAndHandleLocationPermissions()
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -254,7 +254,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
*
|
*
|
||||||
* @param activity
|
* @param activity
|
||||||
*/
|
*/
|
||||||
private fun createDialogsAndHandleLocationPermissions(activity: Activity) {
|
private fun createDialogsAndHandleLocationPermissions() {
|
||||||
inAppCameraLocationPermissionLauncher.launch(arrayOf(permission.ACCESS_FINE_LOCATION))
|
inAppCameraLocationPermissionLauncher.launch(arrayOf(permission.ACCESS_FINE_LOCATION))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -282,7 +282,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
return object : PreferenceGroupAdapter(preferenceScreen) {
|
return object : PreferenceGroupAdapter(preferenceScreen) {
|
||||||
override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) {
|
||||||
super.onBindViewHolder(holder, position)
|
super.onBindViewHolder(holder, position)
|
||||||
val preference = getItem(position)
|
|
||||||
val iconFrame: View? = holder.itemView.findViewById(R.id.icon_frame)
|
val iconFrame: View? = holder.itemView.findViewById(R.id.icon_frame)
|
||||||
iconFrame?.visibility = View.GONE
|
iconFrame?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
@ -341,6 +340,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
val editText: EditText = dialog.findViewById(R.id.search_language)
|
val editText: EditText = dialog.findViewById(R.id.search_language)
|
||||||
val listView: ListView = dialog.findViewById(R.id.language_list)
|
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)
|
languageHistoryListView = dialog.findViewById(R.id.language_history_list)
|
||||||
recentLanguagesTextView = dialog.findViewById(R.id.recent_searches)
|
recentLanguagesTextView = dialog.findViewById(R.id.recent_searches)
|
||||||
separator = dialog.findViewById(R.id.separator)
|
separator = dialog.findViewById(R.id.separator)
|
||||||
|
|
@ -349,6 +349,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
listView.adapter = languagesAdapter
|
listView.adapter = languagesAdapter
|
||||||
|
|
||||||
|
cancelButton.setOnClickListener { dialog.dismiss() }
|
||||||
|
|
||||||
editText.addTextChangedListener(object : TextWatcher {
|
editText.addTextChangedListener(object : TextWatcher {
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, start: Int, count: Int, after: Int) {
|
override fun beforeTextChanged(charSequence: CharSequence, start: Int, count: Int, after: Int) {
|
||||||
hideRecentLanguagesSection()
|
hideRecentLanguagesSection()
|
||||||
|
|
@ -378,10 +380,12 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
if (keyListPreference == "appUiDefaultLanguagePref") {
|
if (keyListPreference == "appUiDefaultLanguagePref") {
|
||||||
appUiLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale)
|
appUiLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale)
|
||||||
setLocale(requireActivity(), lCode)
|
setLocale(requireActivity(), lCode)
|
||||||
requireActivity().recreate()
|
|
||||||
val intent = Intent(requireActivity(), MainActivity::class.java)
|
val intent = Intent(requireActivity(), MainActivity::class.java)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
|
requireActivity().finish()
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
descriptionLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale)
|
descriptionLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale)
|
||||||
}
|
}
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
|
|
|
||||||
|
|
@ -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
|
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.net.Uri
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
|
@ -13,6 +16,7 @@ import androidx.paging.PagedListAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.facebook.imagepipeline.request.ImageRequest
|
import com.facebook.imagepipeline.request.ImageRequest
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.contributions.Contribution
|
import fr.free.nrw.commons.contributions.Contribution
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -51,6 +55,24 @@ class FailedUploadsAdapter(
|
||||||
position: Int,
|
position: Int,
|
||||||
) {
|
) {
|
||||||
val item: Contribution? = getItem(position)
|
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) {
|
if (item != null) {
|
||||||
holder.titleTextView.setText(item.media.displayTitle)
|
holder.titleTextView.setText(item.media.displayTitle)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ class FileProcessor
|
||||||
*/
|
*/
|
||||||
fun getExifTagsToRedact(): Set<String> {
|
fun getExifTagsToRedact(): Set<String> {
|
||||||
val prefManageEXIFTags =
|
val prefManageEXIFTags =
|
||||||
defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS) ?: emptySet()
|
defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS)
|
||||||
val redactTags: Set<String> =
|
val redactTags: Set<String> =
|
||||||
context.resources.getStringArray(R.array.pref_exifTag_values).toSet()
|
context.resources.getStringArray(R.array.pref_exifTag_values).toSet()
|
||||||
return redactTags - prefManageEXIFTags
|
return redactTags - prefManageEXIFTags
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,10 @@ class FileUtilsWrapper @Inject constructor(private val context: Context) {
|
||||||
while ((bis.read(buffer).also { size = it }) > 0) {
|
while ((bis.read(buffer).also { size = it }) > 0) {
|
||||||
buffers.add(
|
buffers.add(
|
||||||
writeToFile(
|
writeToFile(
|
||||||
buffer.copyOf(size),
|
buffer,
|
||||||
file.name ?: "",
|
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.
|
* Create a temp file containing the passed byte data.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@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)
|
val file = File.createTempFile(fileName, fileExtension, context.cacheDir)
|
||||||
try {
|
try {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
|
|
@ -75,7 +76,7 @@ class FileUtilsWrapper @Inject constructor(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
FileOutputStream(file).use { fos ->
|
FileOutputStream(file).use { fos ->
|
||||||
fos.write(data)
|
fos.write(data, 0, size)
|
||||||
}
|
}
|
||||||
} catch (throwable: Exception) {
|
} catch (throwable: Exception) {
|
||||||
Timber.e(throwable, "Failed to create file")
|
Timber.e(throwable, "Failed to create file")
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class ImageProcessingService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.d("Checking the validity of image")
|
Timber.d("Checking the validity of image")
|
||||||
val filePath = uploadItem.mediaUri.path
|
val filePath = uploadItem.mediaUri?.path
|
||||||
|
|
||||||
return Single.zip(
|
return Single.zip(
|
||||||
checkDuplicateImage(filePath),
|
checkDuplicateImage(filePath),
|
||||||
|
|
@ -107,7 +107,7 @@ class ImageProcessingService @Inject constructor(
|
||||||
return Single.just(EMPTY_CAPTION)
|
return Single.just(EMPTY_CAPTION)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.fileName)
|
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.filename)
|
||||||
.map { doesFileExist: Boolean ->
|
.map { doesFileExist: Boolean ->
|
||||||
Timber.d("Result for valid title is %s", doesFileExist)
|
Timber.d("Result for valid title is %s", doesFileExist)
|
||||||
if (doesFileExist) FILE_NAME_EXISTS else IMAGE_OK
|
if (doesFileExist) FILE_NAME_EXISTS else IMAGE_OK
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package fr.free.nrw.commons.upload
|
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.net.Uri
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
|
@ -13,6 +16,7 @@ import androidx.paging.PagedListAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.facebook.imagepipeline.request.ImageRequest
|
import com.facebook.imagepipeline.request.ImageRequest
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.contributions.Contribution
|
import fr.free.nrw.commons.contributions.Contribution
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -99,6 +103,22 @@ class PendingUploadsAdapter(
|
||||||
|
|
||||||
fun bind(contribution: Contribution) {
|
fun bind(contribution: Contribution) {
|
||||||
titleTextView.text = contribution.media.displayTitle
|
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()
|
val imageSource: String = contribution.localUri.toString()
|
||||||
var imageRequest: ImageRequest? = null
|
var imageRequest: ImageRequest? = null
|
||||||
|
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.Window;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
|
||||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.databinding.FragmentSimilarImageDialogBinding;
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by harisanker on 14/2/18.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class SimilarImageDialogFragment extends DialogFragment {
|
|
||||||
|
|
||||||
Callback callback;//Implemented interface from shareActivity
|
|
||||||
Boolean gotResponse = false;
|
|
||||||
|
|
||||||
private FragmentSimilarImageDialogBinding binding;
|
|
||||||
|
|
||||||
public SimilarImageDialogFragment() {
|
|
||||||
}
|
|
||||||
public interface Callback {
|
|
||||||
void onPositiveResponse();
|
|
||||||
|
|
||||||
void onNegativeResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCallback(Callback callback) {
|
|
||||||
this.callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
|
||||||
binding = FragmentSimilarImageDialogBinding.inflate(inflater, container, false);
|
|
||||||
|
|
||||||
|
|
||||||
binding.orginalImage.setHierarchy(GenericDraweeHierarchyBuilder
|
|
||||||
.newInstance(getResources())
|
|
||||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
|
||||||
R.drawable.ic_image_black_24dp,getContext().getTheme()))
|
|
||||||
.setFailureImage(VectorDrawableCompat.create(getResources(),
|
|
||||||
R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
|
|
||||||
.build());
|
|
||||||
binding.possibleImage.setHierarchy(GenericDraweeHierarchyBuilder
|
|
||||||
.newInstance(getResources())
|
|
||||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
|
||||||
R.drawable.ic_image_black_24dp,getContext().getTheme()))
|
|
||||||
.setFailureImage(VectorDrawableCompat.create(getResources(),
|
|
||||||
R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
|
|
||||||
.build());
|
|
||||||
|
|
||||||
binding.orginalImage.setImageURI(Uri.fromFile(new File(getArguments().getString("originalImagePath"))));
|
|
||||||
binding.possibleImage.setImageURI(Uri.fromFile(new File(getArguments().getString("possibleImagePath"))));
|
|
||||||
|
|
||||||
binding.postiveButton.setOnClickListener(v -> onPositiveButtonClicked());
|
|
||||||
binding.negativeButton.setOnClickListener(v -> onNegativeButtonClicked());
|
|
||||||
|
|
||||||
return binding.getRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
Dialog dialog = super.onCreateDialog(savedInstanceState);
|
|
||||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDismiss(DialogInterface dialog) {
|
|
||||||
// I user dismisses dialog by pressing outside the dialog.
|
|
||||||
if (!gotResponse) {
|
|
||||||
callback.onNegativeResponse();
|
|
||||||
}
|
|
||||||
super.onDismiss(dialog);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onNegativeButtonClicked() {
|
|
||||||
callback.onNegativeResponse();
|
|
||||||
gotResponse = true;
|
|
||||||
dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onPositiveButtonClicked() {
|
|
||||||
callback.onPositiveResponse();
|
|
||||||
gotResponse = true;
|
|
||||||
dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
binding = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
package fr.free.nrw.commons.upload
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.Window
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||||
|
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentSimilarImageDialogBinding
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by harisanker on 14/2/18.
|
||||||
|
*/
|
||||||
|
class SimilarImageDialogFragment : DialogFragment() {
|
||||||
|
var callback: Callback? = null //Implemented interface from shareActivity
|
||||||
|
var gotResponse: Boolean = false
|
||||||
|
|
||||||
|
private var _binding: FragmentSimilarImageDialogBinding? = null
|
||||||
|
private val binding: FragmentSimilarImageDialogBinding get() = _binding!!
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onPositiveResponse()
|
||||||
|
|
||||||
|
fun onNegativeResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentSimilarImageDialogBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
binding.orginalImage.hierarchy =
|
||||||
|
GenericDraweeHierarchyBuilder.newInstance(resources).setPlaceholderImage(
|
||||||
|
VectorDrawableCompat.create(
|
||||||
|
resources, R.drawable.ic_image_black_24dp, requireContext().theme
|
||||||
|
)
|
||||||
|
).setFailureImage(
|
||||||
|
VectorDrawableCompat.create(
|
||||||
|
resources, R.drawable.ic_error_outline_black_24dp, requireContext().theme
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
|
||||||
|
binding.possibleImage.hierarchy =
|
||||||
|
GenericDraweeHierarchyBuilder.newInstance(resources).setPlaceholderImage(
|
||||||
|
VectorDrawableCompat.create(
|
||||||
|
resources, R.drawable.ic_image_black_24dp, requireContext().theme
|
||||||
|
)
|
||||||
|
).setFailureImage(
|
||||||
|
VectorDrawableCompat.create(
|
||||||
|
resources, R.drawable.ic_error_outline_black_24dp, requireContext().theme
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
|
||||||
|
arguments?.let {
|
||||||
|
binding.orginalImage.setImageURI(
|
||||||
|
Uri.fromFile(File(it.getString("originalImagePath")!!))
|
||||||
|
)
|
||||||
|
binding.possibleImage.setImageURI(
|
||||||
|
Uri.fromFile(File(it.getString("possibleImagePath")!!))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.postiveButton.setOnClickListener {
|
||||||
|
callback?.onPositiveResponse()
|
||||||
|
gotResponse = true
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.negativeButton.setOnClickListener {
|
||||||
|
callback?.onNegativeResponse()
|
||||||
|
gotResponse = true
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val dialog = super.onCreateDialog(savedInstanceState)
|
||||||
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
|
// I user dismisses dialog by pressing outside the dialog.
|
||||||
|
if (!gotResponse) {
|
||||||
|
callback?.onNegativeResponse()
|
||||||
|
}
|
||||||
|
super.onDismiss(dialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.drawable.GradientDrawable;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build.VERSION;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import com.facebook.drawee.view.SimpleDraweeView;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.databinding.ItemUploadThumbnailBinding;
|
|
||||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The adapter class for image thumbnails to be shown while uploading.
|
|
||||||
*/
|
|
||||||
class ThumbnailsAdapter extends RecyclerView.Adapter<ThumbnailsAdapter.ViewHolder> {
|
|
||||||
public static Context context;
|
|
||||||
List<UploadableFile> uploadableFiles;
|
|
||||||
private Callback callback;
|
|
||||||
|
|
||||||
private OnThumbnailDeletedListener listener;
|
|
||||||
|
|
||||||
private ItemUploadThumbnailBinding binding;
|
|
||||||
|
|
||||||
public ThumbnailsAdapter(Callback callback) {
|
|
||||||
this.uploadableFiles = new ArrayList<>();
|
|
||||||
this.callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the data, the media files
|
|
||||||
* @param uploadableFiles
|
|
||||||
*/
|
|
||||||
public void setUploadableFiles(
|
|
||||||
List<UploadableFile> uploadableFiles) {
|
|
||||||
this.uploadableFiles=uploadableFiles;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnThumbnailDeletedListener(OnThumbnailDeletedListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
|
||||||
binding = ItemUploadThumbnailBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false);
|
|
||||||
return new ViewHolder(binding.getRoot());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
|
|
||||||
viewHolder.bind(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return uploadableFiles.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
|
|
||||||
RelativeLayout rlContainer;
|
|
||||||
SimpleDraweeView background;
|
|
||||||
ImageView ivError;
|
|
||||||
|
|
||||||
ImageView ivCross;
|
|
||||||
|
|
||||||
public ViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
rlContainer = binding.rlContainer;
|
|
||||||
background = binding.ivThumbnail;
|
|
||||||
ivError = binding.ivError;
|
|
||||||
ivCross = binding.icCross;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a row item to the ViewHolder
|
|
||||||
* @param position
|
|
||||||
*/
|
|
||||||
public void bind(int position) {
|
|
||||||
UploadableFile uploadableFile = uploadableFiles.get(position);
|
|
||||||
Uri uri = uploadableFile.getMediaUri();
|
|
||||||
background.setImageURI(Uri.fromFile(new File(String.valueOf(uri))));
|
|
||||||
if (position == callback.getCurrentSelectedFilePosition()) {
|
|
||||||
GradientDrawable border = new GradientDrawable();
|
|
||||||
border.setShape(GradientDrawable.RECTANGLE);
|
|
||||||
border.setStroke(8, context.getResources().getColor(R.color.primaryColor));
|
|
||||||
rlContainer.setEnabled(true);
|
|
||||||
rlContainer.setClickable(true);
|
|
||||||
rlContainer.setAlpha(1.0f);
|
|
||||||
rlContainer.setBackground(border);
|
|
||||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
|
|
||||||
rlContainer.setElevation(10);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rlContainer.setEnabled(false);
|
|
||||||
rlContainer.setClickable(false);
|
|
||||||
rlContainer.setAlpha(0.7f);
|
|
||||||
rlContainer.setBackground(null);
|
|
||||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
|
|
||||||
rlContainer.setElevation(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ivCross.setOnClickListener(v -> {
|
|
||||||
if(listener != null) {
|
|
||||||
listener.onThumbnailDeleted(position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback used to get the current selected file position
|
|
||||||
*/
|
|
||||||
interface Callback {
|
|
||||||
|
|
||||||
int getCurrentSelectedFilePosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface to listen to thumbnail delete events
|
|
||||||
*/
|
|
||||||
|
|
||||||
public interface OnThumbnailDeletedListener {
|
|
||||||
void onThumbnailDeleted(int position);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
package fr.free.nrw.commons.upload
|
||||||
|
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.RelativeLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.facebook.drawee.view.SimpleDraweeView
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.databinding.ItemUploadThumbnailBinding
|
||||||
|
import fr.free.nrw.commons.filepicker.UploadableFile
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The adapter class for image thumbnails to be shown while uploading.
|
||||||
|
*/
|
||||||
|
internal class ThumbnailsAdapter(private val callback: Callback) :
|
||||||
|
RecyclerView.Adapter<ThumbnailsAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
var onThumbnailDeletedListener: OnThumbnailDeletedListener? = null
|
||||||
|
var uploadableFiles: List<UploadableFile> = emptyList()
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int) = ViewHolder(
|
||||||
|
ItemUploadThumbnailBinding.inflate(
|
||||||
|
LayoutInflater.from(viewGroup.context), viewGroup, false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) = viewHolder.bind(position)
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = uploadableFiles.size
|
||||||
|
|
||||||
|
inner class ViewHolder(val binding: ItemUploadThumbnailBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
private val rlContainer: RelativeLayout = binding.rlContainer
|
||||||
|
private val background: SimpleDraweeView = binding.ivThumbnail
|
||||||
|
private val ivError: ImageView = binding.ivError
|
||||||
|
private val ivCross: ImageView = binding.icCross
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a row item to the ViewHolder
|
||||||
|
*/
|
||||||
|
fun bind(position: Int) {
|
||||||
|
val uploadableFile = uploadableFiles[position]
|
||||||
|
val uri = uploadableFile.getMediaUri()
|
||||||
|
background.setImageURI(Uri.fromFile(File(uri.toString())))
|
||||||
|
if (position == callback.getCurrentSelectedFilePosition()) {
|
||||||
|
val border = GradientDrawable()
|
||||||
|
border.shape = GradientDrawable.RECTANGLE
|
||||||
|
border.setStroke(8, ContextCompat.getColor(itemView.context, R.color.primaryColor))
|
||||||
|
rlContainer.isEnabled = true
|
||||||
|
rlContainer.isClickable = true
|
||||||
|
rlContainer.alpha = 1.0f
|
||||||
|
rlContainer.background = border
|
||||||
|
rlContainer.elevation = 10f
|
||||||
|
} else {
|
||||||
|
rlContainer.isEnabled = false
|
||||||
|
rlContainer.isClickable = false
|
||||||
|
rlContainer.alpha = 0.7f
|
||||||
|
rlContainer.background = null
|
||||||
|
rlContainer.elevation = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
ivCross.setOnClickListener {
|
||||||
|
onThumbnailDeletedListener?.onThumbnailDeleted(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback used to get the current selected file position
|
||||||
|
*/
|
||||||
|
internal fun interface Callback {
|
||||||
|
fun getCurrentSelectedFilePosition(): Int
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to listen to thumbnail delete events
|
||||||
|
*/
|
||||||
|
fun interface OnThumbnailDeletedListener {
|
||||||
|
fun onThumbnailDeleted(position: Int)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -448,7 +448,6 @@ public class UploadActivity extends BaseActivity implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void receiveSharedItems() {
|
private void receiveSharedItems() {
|
||||||
ThumbnailsAdapter.context=this;
|
|
||||||
final Intent intent = getIntent();
|
final Intent intent = getIntent();
|
||||||
final String action = intent.getAction();
|
final String action = intent.getAction();
|
||||||
if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
||||||
|
|
@ -899,6 +898,7 @@ public class UploadActivity extends BaseActivity implements
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setTitle(getString(R.string.multiple_files_depiction_header))
|
.setTitle(getString(R.string.multiple_files_depiction_header))
|
||||||
.setMessage(getString(R.string.multiple_files_depiction))
|
.setMessage(getString(R.string.multiple_files_depiction))
|
||||||
|
.setCancelable(false)
|
||||||
.setPositiveButton("OK", (dialog, which) -> {
|
.setPositiveButton("OK", (dialog, which) -> {
|
||||||
if (checkBox.isChecked()) {
|
if (checkBox.isChecked()) {
|
||||||
// Save the user's choice to not show the dialog again
|
// Save the user's choice to not show the dialog again
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The base fragment of the fragments in upload
|
|
||||||
*/
|
|
||||||
public class UploadBaseFragment extends CommonsDaggerSupportFragment {
|
|
||||||
|
|
||||||
public Callback callback;
|
|
||||||
public static final String CALLBACK = "callback";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCallback(Callback callback) {
|
|
||||||
this.callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onBecameVisible() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Callback {
|
|
||||||
|
|
||||||
void onNextButtonClicked(int index);
|
|
||||||
|
|
||||||
void onPreviousButtonClicked(int index);
|
|
||||||
|
|
||||||
void showProgress(boolean shouldShow);
|
|
||||||
|
|
||||||
int getIndexInViewFlipper(UploadBaseFragment fragment);
|
|
||||||
|
|
||||||
int getTotalNumberOfSteps();
|
|
||||||
|
|
||||||
boolean isWLMUpload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package fr.free.nrw.commons.upload
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base fragment of the fragments in upload
|
||||||
|
*/
|
||||||
|
abstract class UploadBaseFragment : CommonsDaggerSupportFragment() {
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
|
protected open fun onBecameVisible() = Unit
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
val totalNumberOfSteps: Int
|
||||||
|
val isWLMUpload: Boolean
|
||||||
|
|
||||||
|
fun onNextButtonClicked(index: Int)
|
||||||
|
fun onPreviousButtonClicked(index: Int)
|
||||||
|
fun showProgress(shouldShow: Boolean)
|
||||||
|
fun getIndexInViewFlipper(fragment: UploadBaseFragment?): Int
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CALLBACK: String = "callback"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.AssetFileDescriptor;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import fr.free.nrw.commons.Media;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
|
||||||
import fr.free.nrw.commons.contributions.Contribution;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Date;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class UploadController {
|
|
||||||
|
|
||||||
private final SessionManager sessionManager;
|
|
||||||
private final Context context;
|
|
||||||
private final JsonKvStore store;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public UploadController(final SessionManager sessionManager,
|
|
||||||
final Context context,
|
|
||||||
final JsonKvStore store) {
|
|
||||||
this.sessionManager = sessionManager;
|
|
||||||
this.context = context;
|
|
||||||
this.store = store;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a new upload task.
|
|
||||||
*
|
|
||||||
* @param contribution the contribution object
|
|
||||||
*/
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
public void prepareMedia(final Contribution contribution) {
|
|
||||||
//Set creator, desc, and license
|
|
||||||
|
|
||||||
// If author name is enabled and set, use it
|
|
||||||
final Media media = contribution.getMedia();
|
|
||||||
if (store.getBoolean("useAuthorName", false)) {
|
|
||||||
final String authorName = store.getString("authorName", "");
|
|
||||||
media.setAuthor(authorName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(media.getAuthor())) {
|
|
||||||
final Account currentAccount = sessionManager.getCurrentAccount();
|
|
||||||
if (currentAccount == null) {
|
|
||||||
Timber.d("Current account is null");
|
|
||||||
ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in));
|
|
||||||
sessionManager.forceLogin(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
media.setAuthor(sessionManager.getUserName());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media.getFallbackDescription() == null) {
|
|
||||||
media.setFallbackDescription("");
|
|
||||||
}
|
|
||||||
|
|
||||||
final String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
|
|
||||||
media.setLicense(license);
|
|
||||||
|
|
||||||
buildUpload(contribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make the Contribution object ready to be uploaded
|
|
||||||
* @param contribution
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private void buildUpload(final Contribution contribution) {
|
|
||||||
final ContentResolver contentResolver = context.getContentResolver();
|
|
||||||
|
|
||||||
contribution.setDataLength(resolveDataLength(contentResolver, contribution));
|
|
||||||
|
|
||||||
final String mimeType = resolveMimeType(contentResolver, contribution);
|
|
||||||
|
|
||||||
if (mimeType != null) {
|
|
||||||
Timber.d("MimeType is: %s", mimeType);
|
|
||||||
contribution.setMimeType(mimeType);
|
|
||||||
if(mimeType.startsWith("image/") && contribution.getDateCreated() == null){
|
|
||||||
contribution.setDateCreated(resolveDateTakenOrNow(contentResolver, contribution));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveMimeType(final ContentResolver contentResolver, final Contribution contribution) {
|
|
||||||
final String mimeType = contribution.getMimeType();
|
|
||||||
if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
|
|
||||||
return contentResolver.getType(contribution.getLocalUri());
|
|
||||||
}
|
|
||||||
return mimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
private long resolveDataLength(final ContentResolver contentResolver, final Contribution contribution) {
|
|
||||||
try {
|
|
||||||
if (contribution.getDataLength() <= 0) {
|
|
||||||
Timber.d("UploadController/doInBackground, contribution.getLocalUri():%s", contribution.getLocalUri());
|
|
||||||
final AssetFileDescriptor assetFileDescriptor = contentResolver
|
|
||||||
.openAssetFileDescriptor(Uri.fromFile(new File(contribution.getLocalUri().getPath())), "r");
|
|
||||||
if (assetFileDescriptor != null) {
|
|
||||||
final long length = assetFileDescriptor.getLength();
|
|
||||||
return length != -1 ? length
|
|
||||||
: countBytes(contentResolver.openInputStream(contribution.getLocalUri()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (final IOException | NullPointerException | SecurityException e) {
|
|
||||||
Timber.e(e, "Exception occurred while uploading image");
|
|
||||||
}
|
|
||||||
return contribution.getDataLength();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Date resolveDateTakenOrNow(final ContentResolver contentResolver, final Contribution contribution) {
|
|
||||||
Timber.d("local uri %s", contribution.getLocalUri());
|
|
||||||
try(final Cursor cursor = dateTakenCursor(contentResolver, contribution)) {
|
|
||||||
if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) {
|
|
||||||
cursor.moveToFirst();
|
|
||||||
final Date dateCreated = new Date(cursor.getLong(0));
|
|
||||||
if (dateCreated.after(new Date(0))) {
|
|
||||||
return dateCreated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Date();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Cursor dateTakenCursor(final ContentResolver contentResolver, final Contribution contribution) {
|
|
||||||
return contentResolver.query(contribution.getLocalUri(),
|
|
||||||
new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts the number of bytes in {@code stream}.
|
|
||||||
*
|
|
||||||
* @param stream the stream
|
|
||||||
* @return the number of bytes in {@code stream}
|
|
||||||
* @throws IOException if an I/O error occurs
|
|
||||||
*/
|
|
||||||
private long countBytes(final InputStream stream) throws IOException {
|
|
||||||
long count = 0;
|
|
||||||
final BufferedInputStream bis = new BufferedInputStream(stream);
|
|
||||||
while (bis.read() != -1) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
166
app/src/main/java/fr/free/nrw/commons/upload/UploadController.kt
Normal file
166
app/src/main/java/fr/free/nrw/commons/upload/UploadController.kt
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
package fr.free.nrw.commons.upload
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.text.TextUtils
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
|
import fr.free.nrw.commons.contributions.Contribution
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import fr.free.nrw.commons.settings.Prefs
|
||||||
|
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.util.Date
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class UploadController @Inject constructor(
|
||||||
|
private val sessionManager: SessionManager,
|
||||||
|
private val context: Context,
|
||||||
|
private val store: JsonKvStore
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Starts a new upload task.
|
||||||
|
*
|
||||||
|
* @param contribution the contribution object
|
||||||
|
*/
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
fun prepareMedia(contribution: Contribution) {
|
||||||
|
//Set creator, desc, and license
|
||||||
|
|
||||||
|
// If author name is enabled and set, use it
|
||||||
|
|
||||||
|
val media = contribution.media
|
||||||
|
if (store.getBoolean("useAuthorName", false)) {
|
||||||
|
val authorName = store.getString("authorName", "")
|
||||||
|
media.author = authorName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.author.isNullOrEmpty()) {
|
||||||
|
val currentAccount = sessionManager.currentAccount
|
||||||
|
if (currentAccount == null) {
|
||||||
|
Timber.d("Current account is null")
|
||||||
|
showLongToast(context, context.getString(R.string.user_not_logged_in))
|
||||||
|
sessionManager.forceLogin(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
media.author = sessionManager.userName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.fallbackDescription == null) {
|
||||||
|
media.fallbackDescription = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3)
|
||||||
|
media.license = license
|
||||||
|
|
||||||
|
buildUpload(contribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildUpload(contribution: Contribution) {
|
||||||
|
val contentResolver = context.contentResolver
|
||||||
|
|
||||||
|
contribution.dataLength = resolveDataLength(contentResolver, contribution)
|
||||||
|
|
||||||
|
val mimeType = resolveMimeType(contentResolver, contribution)
|
||||||
|
|
||||||
|
if (mimeType != null) {
|
||||||
|
Timber.d("MimeType is: %s", mimeType)
|
||||||
|
contribution.mimeType = mimeType
|
||||||
|
if (mimeType.startsWith("image/") && contribution.dateCreated == null) {
|
||||||
|
contribution.dateCreated = resolveDateTakenOrNow(contentResolver, contribution)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveMimeType(
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
contribution: Contribution
|
||||||
|
): String? {
|
||||||
|
val mimeType: String? = contribution.mimeType
|
||||||
|
return if (mimeType.isNullOrEmpty() || mimeType.endsWith("*")) {
|
||||||
|
contentResolver.getType(contribution.localUri!!)
|
||||||
|
} else {
|
||||||
|
mimeType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveDataLength(
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
contribution: Contribution
|
||||||
|
): Long {
|
||||||
|
try {
|
||||||
|
if (contribution.dataLength <= 0) {
|
||||||
|
Timber.d(
|
||||||
|
"UploadController/doInBackground, contribution.getLocalUri():%s",
|
||||||
|
contribution.localUri
|
||||||
|
)
|
||||||
|
|
||||||
|
contentResolver.openAssetFileDescriptor(
|
||||||
|
Uri.fromFile(File(contribution.localUri!!.path!!)), "r"
|
||||||
|
)?.use {
|
||||||
|
return if (it.length != -1L) it.length
|
||||||
|
else countBytes(contentResolver.openInputStream(contribution.localUri))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e, "Exception occurred while uploading image")
|
||||||
|
} catch (e: NullPointerException) {
|
||||||
|
Timber.e(e, "Exception occurred while uploading image")
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Timber.e(e, "Exception occurred while uploading image")
|
||||||
|
}
|
||||||
|
return contribution.dataLength
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveDateTakenOrNow(
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
contribution: Contribution
|
||||||
|
): Date {
|
||||||
|
Timber.d("local uri %s", contribution.localUri)
|
||||||
|
dateTakenCursor(contentResolver, contribution).use { cursor ->
|
||||||
|
if (cursor != null && cursor.count != 0 && cursor.columnCount != 0) {
|
||||||
|
cursor.moveToFirst()
|
||||||
|
val dateCreated = Date(cursor.getLong(0))
|
||||||
|
if (dateCreated.after(Date(0))) {
|
||||||
|
return dateCreated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dateTakenCursor(
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
contribution: Contribution
|
||||||
|
): Cursor? = contentResolver.query(
|
||||||
|
contribution.localUri!!,
|
||||||
|
arrayOf(MediaStore.Images.ImageColumns.DATE_TAKEN), null, null, null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the number of bytes in `stream`.
|
||||||
|
*
|
||||||
|
* @param stream the stream
|
||||||
|
* @return the number of bytes in `stream`
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun countBytes(stream: InputStream?): Long {
|
||||||
|
var count: Long = 0
|
||||||
|
val bis = BufferedInputStream(stream)
|
||||||
|
while (bis.read() != -1) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.net.Uri;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import fr.free.nrw.commons.Utils;
|
|
||||||
import fr.free.nrw.commons.filepicker.MimeTypeMapWrapper;
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
|
||||||
import fr.free.nrw.commons.utils.ImageUtils;
|
|
||||||
import io.reactivex.subjects.BehaviorSubject;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class UploadItem {
|
|
||||||
|
|
||||||
private Uri mediaUri;
|
|
||||||
private final String mimeType;
|
|
||||||
private ImageCoordinates gpsCoords;
|
|
||||||
private List<UploadMediaDetail> uploadMediaDetails;
|
|
||||||
private Place place;
|
|
||||||
private final long createdTimestamp;
|
|
||||||
private final String createdTimestampSource;
|
|
||||||
private final BehaviorSubject<Integer> imageQuality;
|
|
||||||
private boolean hasInvalidLocation;
|
|
||||||
private boolean isWLMUpload = false;
|
|
||||||
private String countryCode;
|
|
||||||
private String fileCreatedDateString; //according to EXIF data
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uri of uploadItem
|
|
||||||
* Uri points to image location or name, eg content://media/external/images/camera/10495 (Android 10)
|
|
||||||
*/
|
|
||||||
private Uri contentUri;
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
|
||||||
UploadItem(final Uri mediaUri,
|
|
||||||
final String mimeType,
|
|
||||||
final ImageCoordinates gpsCoords,
|
|
||||||
final Place place,
|
|
||||||
final long createdTimestamp,
|
|
||||||
final String createdTimestampSource,
|
|
||||||
final Uri contentUri,
|
|
||||||
final String fileCreatedDateString) {
|
|
||||||
this.createdTimestampSource = createdTimestampSource;
|
|
||||||
uploadMediaDetails = new ArrayList<>(Collections.singletonList(new UploadMediaDetail()));
|
|
||||||
this.place = place;
|
|
||||||
this.mediaUri = mediaUri;
|
|
||||||
this.mimeType = mimeType;
|
|
||||||
this.gpsCoords = gpsCoords;
|
|
||||||
this.createdTimestamp = createdTimestamp;
|
|
||||||
this.contentUri = contentUri;
|
|
||||||
imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT);
|
|
||||||
this.fileCreatedDateString = fileCreatedDateString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCreatedTimestampSource() {
|
|
||||||
return createdTimestampSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ImageCoordinates getGpsCoords() {
|
|
||||||
return gpsCoords;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<UploadMediaDetail> getUploadMediaDetails() {
|
|
||||||
return uploadMediaDetails;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getCreatedTimestamp() {
|
|
||||||
return createdTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uri getMediaUri() {
|
|
||||||
return mediaUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getImageQuality() {
|
|
||||||
return imageQuality.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getContentUri.
|
|
||||||
* @return Uri of uploadItem
|
|
||||||
* Uri points to image location or name, eg content://media/external/images/camera/10495 (Android 10)
|
|
||||||
*/
|
|
||||||
public Uri getContentUri() { return contentUri; }
|
|
||||||
|
|
||||||
public String getFileCreatedDateString() { return fileCreatedDateString; }
|
|
||||||
|
|
||||||
public void setImageQuality(final int imageQuality) {
|
|
||||||
this.imageQuality.onNext(imageQuality);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the corresponding place to the uploadItem
|
|
||||||
*
|
|
||||||
* @param place geolocated Wikidata item
|
|
||||||
*/
|
|
||||||
public void setPlace(Place place) {
|
|
||||||
this.place = place;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Place getPlace() {
|
|
||||||
return place;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMediaDetails(final List<UploadMediaDetail> uploadMediaDetails) {
|
|
||||||
this.uploadMediaDetails = uploadMediaDetails;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWLMUpload(final boolean WLMUpload) {
|
|
||||||
isWLMUpload = WLMUpload;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isWLMUpload() {
|
|
||||||
return isWLMUpload;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(@Nullable final Object obj) {
|
|
||||||
if (!(obj instanceof UploadItem)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return mediaUri.toString().contains(((UploadItem) (obj)).mediaUri.toString());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return mediaUri.hashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Choose a filename for the media. Currently, the caption is used as a filename. If several
|
|
||||||
* languages have been entered, the first language is used.
|
|
||||||
*/
|
|
||||||
public String getFileName() {
|
|
||||||
return Utils.fixExtension(uploadMediaDetails.get(0).getCaptionText(),
|
|
||||||
MimeTypeMapWrapper.getExtensionFromMimeType(mimeType));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGpsCoords(final ImageCoordinates gpsCoords) {
|
|
||||||
this.gpsCoords = gpsCoords;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHasInvalidLocation(boolean hasInvalidLocation) {
|
|
||||||
this.hasInvalidLocation = hasInvalidLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasInvalidLocation() {
|
|
||||||
return hasInvalidLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCountryCode(final String countryCode) {
|
|
||||||
this.countryCode = countryCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getCountryCode() {
|
|
||||||
return countryCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets both the contentUri and mediaUri to the specified Uri.
|
|
||||||
* This method allows you to assign the same Uri to both the contentUri and mediaUri
|
|
||||||
* properties.
|
|
||||||
*
|
|
||||||
* @param uri The Uri to be set as both the contentUri and mediaUri.
|
|
||||||
*/
|
|
||||||
public void setContentUri(Uri uri) {
|
|
||||||
contentUri = uri;
|
|
||||||
mediaUri = uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
65
app/src/main/java/fr/free/nrw/commons/upload/UploadItem.kt
Normal file
65
app/src/main/java/fr/free/nrw/commons/upload/UploadItem.kt
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
package fr.free.nrw.commons.upload
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
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?,
|
||||||
|
val mimeType: String?,
|
||||||
|
var gpsCoords: ImageCoordinates?,
|
||||||
|
var place: Place?,
|
||||||
|
val createdTimestamp: Long?,
|
||||||
|
val createdTimestampSource: String?,
|
||||||
|
/**
|
||||||
|
* Uri of uploadItem
|
||||||
|
* Uri points to image location or name, eg content://media/external/images/camera/10495 (Android 10)
|
||||||
|
*/
|
||||||
|
var contentUri: Uri?,
|
||||||
|
//according to EXIF data
|
||||||
|
val fileCreatedDateString: String?
|
||||||
|
) {
|
||||||
|
var imageQuality: Int = ImageUtils.IMAGE_WAIT
|
||||||
|
var uploadMediaDetails: MutableList<UploadMediaDetail> = mutableListOf(UploadMediaDetail())
|
||||||
|
var hasInvalidLocation = false
|
||||||
|
var isWLMUpload = false
|
||||||
|
var countryCode: String? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choose a filename for the media. Currently, the caption is used as a filename. If several
|
||||||
|
* languages have been entered, the first language is used.
|
||||||
|
*/
|
||||||
|
val filename: String
|
||||||
|
get() = Utils.fixExtension(
|
||||||
|
uploadMediaDetails[0].captionText,
|
||||||
|
getExtensionFromMimeType(mimeType)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun hasInvalidLocation(): Boolean = hasInvalidLocation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets both the contentUri and mediaUri to the specified Uri.
|
||||||
|
* This method allows you to assign the same Uri to both the contentUri and mediaUri
|
||||||
|
* properties.
|
||||||
|
*
|
||||||
|
* @param uri The Uri to be set as both the contentUri and mediaUri.
|
||||||
|
*/
|
||||||
|
fun setContentAndMediaUri(uri: Uri) {
|
||||||
|
contentUri = uri
|
||||||
|
mediaUri = uri
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is UploadItem) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return mediaUri.toString().contains((other).mediaUri.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return mediaUri.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import android.view.View.OnClickListener;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
|
import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
@ -302,8 +303,9 @@ public class UploadMediaDetailAdapter extends
|
||||||
|
|
||||||
removeButton.setOnClickListener(v -> removeDescription(uploadMediaDetail, position));
|
removeButton.setOnClickListener(v -> removeDescription(uploadMediaDetail, position));
|
||||||
captionListener = new AbstractTextWatcher(
|
captionListener = new AbstractTextWatcher(
|
||||||
captionText -> uploadMediaDetail.setCaptionText(convertIdeographicSpaceToLatinSpace(
|
captionText -> uploadMediaDetail.setCaptionText(
|
||||||
removeLeadingAndTrailingWhitespace(captionText))));
|
convertIdeographicSpaceToLatinSpace(captionText.strip()))
|
||||||
|
);
|
||||||
descriptionListener = new AbstractTextWatcher(
|
descriptionListener = new AbstractTextWatcher(
|
||||||
descriptionText -> uploadMediaDetail.setDescriptionText(descriptionText));
|
descriptionText -> uploadMediaDetail.setDescriptionText(descriptionText));
|
||||||
captionItemEditText.addTextChangedListener(captionListener);
|
captionItemEditText.addTextChangedListener(captionListener);
|
||||||
|
|
@ -356,6 +358,7 @@ public class UploadMediaDetailAdapter extends
|
||||||
|
|
||||||
EditText editText = dialog.findViewById(R.id.search_language);
|
EditText editText = dialog.findViewById(R.id.search_language);
|
||||||
ListView listView = dialog.findViewById(R.id.language_list);
|
ListView listView = dialog.findViewById(R.id.language_list);
|
||||||
|
final Button cancelButton = dialog.findViewById(R.id.cancel_button);
|
||||||
languageHistoryListView = dialog.findViewById(R.id.language_history_list);
|
languageHistoryListView = dialog.findViewById(R.id.language_history_list);
|
||||||
recentLanguagesTextView = dialog.findViewById(R.id.recent_searches);
|
recentLanguagesTextView = dialog.findViewById(R.id.recent_searches);
|
||||||
separator = dialog.findViewById(R.id.separator);
|
separator = dialog.findViewById(R.id.separator);
|
||||||
|
|
@ -363,6 +366,8 @@ public class UploadMediaDetailAdapter extends
|
||||||
|
|
||||||
listView.setAdapter(languagesAdapter);
|
listView.setAdapter(languagesAdapter);
|
||||||
|
|
||||||
|
cancelButton.setOnClickListener(v -> dialog.dismiss());
|
||||||
|
|
||||||
editText.addTextChangedListener(new TextWatcher() {
|
editText.addTextChangedListener(new TextWatcher() {
|
||||||
@Override
|
@Override
|
||||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1,
|
public void beforeTextChanged(CharSequence charSequence, int i, int i1,
|
||||||
|
|
@ -547,39 +552,6 @@ public class UploadMediaDetailAdapter extends
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes any leading and trailing whitespace from the source text.
|
|
||||||
*
|
|
||||||
* @param source input string
|
|
||||||
* @return a string without leading and trailing whitespace
|
|
||||||
*/
|
|
||||||
public String removeLeadingAndTrailingWhitespace(String source) {
|
|
||||||
// This method can be replaced with the inbuilt String::strip when updated to JDK 11.
|
|
||||||
// Note that String::trim does not adequately remove all whitespace chars.
|
|
||||||
int firstNonWhitespaceIndex = 0;
|
|
||||||
while (firstNonWhitespaceIndex < source.length()) {
|
|
||||||
if (Character.isWhitespace(source.charAt(firstNonWhitespaceIndex))) {
|
|
||||||
firstNonWhitespaceIndex++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (firstNonWhitespaceIndex == source.length()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
int lastNonWhitespaceIndex = source.length() - 1;
|
|
||||||
while (lastNonWhitespaceIndex > firstNonWhitespaceIndex) {
|
|
||||||
if (Character.isWhitespace(source.charAt(lastNonWhitespaceIndex))) {
|
|
||||||
lastNonWhitespaceIndex--;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return source.substring(firstNonWhitespaceIndex, lastNonWhitespaceIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert Ideographic space to Latin space
|
* Convert Ideographic space to Latin space
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.text.InputFilter;
|
|
||||||
import android.text.Spanned;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link InputFilter} class that removes characters blocklisted in Wikimedia titles. The list
|
|
||||||
* of blocklisted characters is linked below.
|
|
||||||
* @see <a href="https://commons.wikimedia.org/wiki/MediaWiki:Titleblacklist"></a>wikimedia.org</a>
|
|
||||||
*/
|
|
||||||
public class UploadMediaDetailInputFilter implements InputFilter {
|
|
||||||
private final Pattern[] patterns;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the blocklisted patterns.
|
|
||||||
*/
|
|
||||||
public UploadMediaDetailInputFilter() {
|
|
||||||
patterns = new Pattern[]{
|
|
||||||
Pattern.compile("[\\x{00A0}\\x{1680}\\x{180E}\\x{2000}-\\x{200B}\\x{2028}\\x{2029}\\x{202F}\\x{205F}]"),
|
|
||||||
Pattern.compile("[\\x{202A}-\\x{202E}]"),
|
|
||||||
Pattern.compile("\\p{Cc}"),
|
|
||||||
Pattern.compile("\\x{3A}"), // Added for colon(:)
|
|
||||||
Pattern.compile("\\x{FEFF}"),
|
|
||||||
Pattern.compile("\\x{00AD}"),
|
|
||||||
Pattern.compile("[\\x{E000}-\\x{F8FF}\\x{FFF0}-\\x{FFFF}]"),
|
|
||||||
Pattern.compile("[^\\x{0000}-\\x{FFFF}\\p{sc=Han}]")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the source text contains any blocklisted characters.
|
|
||||||
* @param source input text
|
|
||||||
* @return contains a blocklisted character
|
|
||||||
*/
|
|
||||||
private Boolean checkBlocklisted(final CharSequence source) {
|
|
||||||
for (final Pattern pattern: patterns) {
|
|
||||||
if (pattern.matcher(source).find()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes any blocklisted characters from the source text.
|
|
||||||
* @param source input text
|
|
||||||
* @return a cleaned character sequence
|
|
||||||
*/
|
|
||||||
private CharSequence removeBlocklisted(CharSequence source) {
|
|
||||||
for (final Pattern pattern: patterns) {
|
|
||||||
source = pattern.matcher(source).replaceAll("");
|
|
||||||
}
|
|
||||||
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters out any blocklisted characters.
|
|
||||||
* @param source {@inheritDoc}
|
|
||||||
* @param start {@inheritDoc}
|
|
||||||
* @param end {@inheritDoc}
|
|
||||||
* @param dest {@inheritDoc}
|
|
||||||
* @param dstart {@inheritDoc}
|
|
||||||
* @param dend {@inheritDoc}
|
|
||||||
* @return {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
|
|
||||||
int dend) {
|
|
||||||
if (checkBlocklisted(source)) {
|
|
||||||
if (start == dstart) {
|
|
||||||
return dest;
|
|
||||||
}
|
|
||||||
|
|
||||||
return removeBlocklisted(source);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
package fr.free.nrw.commons.upload
|
||||||
|
|
||||||
|
import android.text.InputFilter
|
||||||
|
import android.text.Spanned
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [InputFilter] class that removes characters blocklisted in Wikimedia titles. The list
|
||||||
|
* of blocklisted characters is linked below.
|
||||||
|
* @see [](https://commons.wikimedia.org/wiki/MediaWiki:Titleblacklist)wikimedia.org
|
||||||
|
*/
|
||||||
|
class UploadMediaDetailInputFilter : InputFilter {
|
||||||
|
private val patterns = listOf(
|
||||||
|
Pattern.compile("[\\x{00A0}\\x{1680}\\x{180E}\\x{2000}-\\x{200B}\\x{2028}\\x{2029}\\x{202F}\\x{205F}]"),
|
||||||
|
Pattern.compile("[\\x{202A}-\\x{202E}]"),
|
||||||
|
Pattern.compile("\\p{Cc}"),
|
||||||
|
Pattern.compile("\\x{3A}"), // Added for colon(:)
|
||||||
|
Pattern.compile("\\x{FEFF}"),
|
||||||
|
Pattern.compile("\\x{00AD}"),
|
||||||
|
Pattern.compile("[\\x{E000}-\\x{F8FF}\\x{FFF0}-\\x{FFFF}]"),
|
||||||
|
Pattern.compile("[^\\x{0000}-\\x{FFFF}\\p{sc=Han}]")
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the source text contains any blocklisted characters.
|
||||||
|
* @param source input text
|
||||||
|
* @return contains a blocklisted character
|
||||||
|
*/
|
||||||
|
private fun checkBlocklisted(source: CharSequence): Boolean =
|
||||||
|
patterns.any { it.matcher(source).find() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes any blocklisted characters from the source text.
|
||||||
|
* @param source input text
|
||||||
|
* @return a cleaned character sequence
|
||||||
|
*/
|
||||||
|
private fun removeBlocklisted(input: CharSequence): CharSequence {
|
||||||
|
var source = input
|
||||||
|
patterns.forEach {
|
||||||
|
source = it.matcher(source).replaceAll("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out any blocklisted characters.
|
||||||
|
* @param source {@inheritDoc}
|
||||||
|
* @param start {@inheritDoc}
|
||||||
|
* @param end {@inheritDoc}
|
||||||
|
* @param dest {@inheritDoc}
|
||||||
|
* @param dstart {@inheritDoc}
|
||||||
|
* @param dend {@inheritDoc}
|
||||||
|
* @return {@inheritDoc}
|
||||||
|
*/
|
||||||
|
override fun filter(
|
||||||
|
source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int,
|
||||||
|
dend: Int
|
||||||
|
): CharSequence? {
|
||||||
|
if (checkBlocklisted(source)) {
|
||||||
|
if (start == dstart && dest.isNotEmpty()) {
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
return removeBlocklisted(source)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,206 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
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.repository.UploadRepository;
|
|
||||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract;
|
|
||||||
import io.reactivex.Observer;
|
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
|
||||||
import io.reactivex.disposables.Disposable;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import java.lang.reflect.Proxy;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The MVP pattern presenter of Upload GUI
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
public class UploadPresenter implements UploadContract.UserActionListener {
|
|
||||||
|
|
||||||
private static final UploadContract.View DUMMY = (UploadContract.View) Proxy.newProxyInstance(
|
|
||||||
UploadContract.View.class.getClassLoader(),
|
|
||||||
new Class[]{UploadContract.View.class}, (proxy, method, methodArgs) -> null);
|
|
||||||
private final UploadRepository repository;
|
|
||||||
private final JsonKvStore defaultKvStore;
|
|
||||||
private UploadContract.View view = DUMMY;
|
|
||||||
@Inject
|
|
||||||
UploadMediaDetailsContract.UserActionListener presenter;
|
|
||||||
|
|
||||||
private CompositeDisposable compositeDisposable;
|
|
||||||
public static final String COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES
|
|
||||||
= "number_of_consecutive_uploads_without_coordinates";
|
|
||||||
|
|
||||||
public static final int CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD = 10;
|
|
||||||
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
UploadPresenter(UploadRepository uploadRepository,
|
|
||||||
@Named("default_preferences") JsonKvStore defaultKvStore) {
|
|
||||||
this.repository = uploadRepository;
|
|
||||||
this.defaultKvStore = defaultKvStore;
|
|
||||||
compositeDisposable = new CompositeDisposable();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the submit button in {@link UploadActivity}
|
|
||||||
*/
|
|
||||||
@SuppressLint("CheckResult")
|
|
||||||
@Override
|
|
||||||
public void handleSubmit() {
|
|
||||||
boolean hasLocationProvidedForNewUploads = false;
|
|
||||||
for (UploadItem item : repository.getUploads()) {
|
|
||||||
if (item.getGpsCoords().getImageCoordsExists()) {
|
|
||||||
hasLocationProvidedForNewUploads = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
boolean hasManyConsecutiveUploadsWithoutLocation = defaultKvStore.getInt(
|
|
||||||
COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0) >=
|
|
||||||
CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD;
|
|
||||||
|
|
||||||
if (hasManyConsecutiveUploadsWithoutLocation && !hasLocationProvidedForNewUploads) {
|
|
||||||
defaultKvStore.putInt(COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0);
|
|
||||||
view.showAlertDialog(
|
|
||||||
R.string.location_message,
|
|
||||||
() -> {defaultKvStore.putInt(
|
|
||||||
COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES,
|
|
||||||
0);
|
|
||||||
processContributionsForSubmission();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
processContributionsForSubmission();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processContributionsForSubmission() {
|
|
||||||
if (view.isLoggedIn()) {
|
|
||||||
view.showProgress(true);
|
|
||||||
repository.buildContributions()
|
|
||||||
.observeOn(Schedulers.io())
|
|
||||||
.subscribe(new Observer<Contribution>() {
|
|
||||||
@Override
|
|
||||||
public void onSubscribe(Disposable d) {
|
|
||||||
view.showProgress(false);
|
|
||||||
if (defaultKvStore
|
|
||||||
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
|
|
||||||
false)) {
|
|
||||||
view.showMessage(R.string.uploading_queued);
|
|
||||||
} else {
|
|
||||||
view.showMessage(R.string.uploading_started);
|
|
||||||
}
|
|
||||||
|
|
||||||
compositeDisposable.add(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNext(Contribution contribution) {
|
|
||||||
if (contribution.getDecimalCoords() == null) {
|
|
||||||
final int recentCount = defaultKvStore.getInt(
|
|
||||||
COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0);
|
|
||||||
defaultKvStore.putInt(
|
|
||||||
COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, recentCount + 1);
|
|
||||||
} else {
|
|
||||||
defaultKvStore.putInt(
|
|
||||||
COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0);
|
|
||||||
}
|
|
||||||
repository.prepareMedia(contribution);
|
|
||||||
contribution.setState(Contribution.STATE_QUEUED);
|
|
||||||
repository.saveContribution(contribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(Throwable e) {
|
|
||||||
view.showMessage(R.string.upload_failed);
|
|
||||||
repository.cleanup();
|
|
||||||
view.returnToMainActivity();
|
|
||||||
compositeDisposable.clear();
|
|
||||||
Timber.e("failed to upload: " + e.getMessage());
|
|
||||||
|
|
||||||
//is submission error, not need to go to the uploadActivity
|
|
||||||
//not start the uploading progress
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onComplete() {
|
|
||||||
view.makeUploadRequest();
|
|
||||||
repository.cleanup();
|
|
||||||
view.returnToMainActivity();
|
|
||||||
compositeDisposable.clear();
|
|
||||||
|
|
||||||
//after finish the uploadActivity, if successful,
|
|
||||||
//directly go to the upload progress activity
|
|
||||||
view.goToUploadProgressActivity();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
view.askUserToLogIn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls checkImageQuality of UploadMediaPresenter to check image quality of next image
|
|
||||||
*
|
|
||||||
* @param uploadItemIndex Index of next image, whose quality is to be checked
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void checkImageQuality(int uploadItemIndex) {
|
|
||||||
UploadItem uploadItem = repository.getUploadItem(uploadItemIndex);
|
|
||||||
presenter.checkImageQuality(uploadItem, uploadItemIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deletePictureAtIndex(int index) {
|
|
||||||
List<UploadableFile> uploadableFiles = view.getUploadableFiles();
|
|
||||||
if (index == uploadableFiles.size() - 1) {
|
|
||||||
// If the next fragment to be shown is not one of the MediaDetailsFragment
|
|
||||||
// lets hide the top card so that it doesn't appear on the other fragments
|
|
||||||
view.showHideTopCard(false);
|
|
||||||
}
|
|
||||||
view.setImageCancelled(true);
|
|
||||||
repository.deletePicture(uploadableFiles.get(index).getFilePath());
|
|
||||||
if (uploadableFiles.size() == 1) {
|
|
||||||
view.showMessage(R.string.upload_cancelled);
|
|
||||||
view.finish();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
if (presenter != null) {
|
|
||||||
presenter.updateImageQualitiesJSON(uploadableFiles.size(), index);
|
|
||||||
}
|
|
||||||
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
|
|
||||||
UploadItem uploadItem = repository.getUploadItem(index);
|
|
||||||
presenter.checkImageQuality(uploadItem, index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (uploadableFiles.size() < 2) {
|
|
||||||
view.showHideTopCard(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
//In case lets update the number of uploadable media
|
|
||||||
view.updateTopCardTitle();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttachView(UploadContract.View view) {
|
|
||||||
this.view = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetachView() {
|
|
||||||
this.view = DUMMY;
|
|
||||||
compositeDisposable.clear();
|
|
||||||
repository.cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
192
app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt
Normal file
192
app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
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.JsonKvStore
|
||||||
|
import fr.free.nrw.commons.repository.UploadRepository
|
||||||
|
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract
|
||||||
|
import io.reactivex.Observer
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.disposables.Disposable
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.lang.reflect.Proxy
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The MVP pattern presenter of Upload GUI
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class UploadPresenter @Inject internal constructor(
|
||||||
|
private val repository: UploadRepository,
|
||||||
|
@param:Named("default_preferences") private val defaultKvStore: JsonKvStore
|
||||||
|
) : UploadContract.UserActionListener {
|
||||||
|
private var view = DUMMY
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var presenter: UploadMediaDetailsContract.UserActionListener
|
||||||
|
|
||||||
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the submit button in [UploadActivity]
|
||||||
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
override fun handleSubmit() {
|
||||||
|
var hasLocationProvidedForNewUploads = false
|
||||||
|
for (item in repository.getUploads()) {
|
||||||
|
if (item.gpsCoords?.imageCoordsExists == true) {
|
||||||
|
hasLocationProvidedForNewUploads = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val hasManyConsecutiveUploadsWithoutLocation = defaultKvStore.getInt(
|
||||||
|
COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0
|
||||||
|
) >=
|
||||||
|
CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD
|
||||||
|
|
||||||
|
if (hasManyConsecutiveUploadsWithoutLocation && !hasLocationProvidedForNewUploads) {
|
||||||
|
defaultKvStore.putInt(COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0)
|
||||||
|
view.showAlertDialog(
|
||||||
|
R.string.location_message
|
||||||
|
) {
|
||||||
|
defaultKvStore.putInt(
|
||||||
|
COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
processContributionsForSubmission()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
processContributionsForSubmission()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processContributionsForSubmission() {
|
||||||
|
if (view.isLoggedIn()) {
|
||||||
|
view.showProgress(true)
|
||||||
|
repository.buildContributions()
|
||||||
|
?.observeOn(Schedulers.io())
|
||||||
|
?.subscribe(object : Observer<Contribution> {
|
||||||
|
override fun onSubscribe(d: Disposable) {
|
||||||
|
view.showProgress(false)
|
||||||
|
if (defaultKvStore.getBoolean(IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
|
||||||
|
view.showMessage(R.string.uploading_queued)
|
||||||
|
} else {
|
||||||
|
view.showMessage(R.string.uploading_started)
|
||||||
|
}
|
||||||
|
compositeDisposable.add(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNext(contribution: Contribution) {
|
||||||
|
if (contribution.decimalCoords == null) {
|
||||||
|
val recentCount = defaultKvStore.getInt(
|
||||||
|
COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0
|
||||||
|
)
|
||||||
|
defaultKvStore.putInt(
|
||||||
|
COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, recentCount + 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
defaultKvStore.putInt(
|
||||||
|
COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
repository.prepareMedia(contribution)
|
||||||
|
contribution.state = Contribution.STATE_QUEUED
|
||||||
|
repository.saveContribution(contribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Throwable) {
|
||||||
|
view.showMessage(R.string.upload_failed)
|
||||||
|
repository.cleanup()
|
||||||
|
view.returnToMainActivity()
|
||||||
|
compositeDisposable.clear()
|
||||||
|
Timber.e(e, "failed to upload")
|
||||||
|
|
||||||
|
//is submission error, not need to go to the uploadActivity
|
||||||
|
//not start the uploading progress
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onComplete() {
|
||||||
|
view.makeUploadRequest()
|
||||||
|
repository.cleanup()
|
||||||
|
view.returnToMainActivity()
|
||||||
|
compositeDisposable.clear()
|
||||||
|
|
||||||
|
//after finish the uploadActivity, if successful,
|
||||||
|
//directly go to the upload progress activity
|
||||||
|
view.goToUploadProgressActivity()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
view.askUserToLogIn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deletePictureAtIndex(index: Int) {
|
||||||
|
val uploadableFiles = view.getUploadableFiles()
|
||||||
|
if (index == uploadableFiles!!.size - 1) {
|
||||||
|
// If the next fragment to be shown is not one of the MediaDetailsFragment
|
||||||
|
// lets hide the top card so that it doesn't appear on the other fragments
|
||||||
|
view.showHideTopCard(false)
|
||||||
|
}
|
||||||
|
view.setImageCancelled(true)
|
||||||
|
repository.deletePicture(uploadableFiles[index].getFilePath())
|
||||||
|
if (uploadableFiles.size == 1) {
|
||||||
|
view.showMessage(R.string.upload_cancelled)
|
||||||
|
view.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
presenter.updateImageQualitiesJSON(uploadableFiles.size, index)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadableFiles.size < 2) {
|
||||||
|
view.showHideTopCard(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
//In case lets update the number of uploadable media
|
||||||
|
view.updateTopCardTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachView(view: UploadContract.View) {
|
||||||
|
this.view = view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachView() {
|
||||||
|
view = DUMMY
|
||||||
|
compositeDisposable.clear()
|
||||||
|
repository.cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DUMMY = Proxy.newProxyInstance(
|
||||||
|
UploadContract.View::class.java.classLoader,
|
||||||
|
arrayOf<Class<*>>(UploadContract.View::class.java)
|
||||||
|
) { _: Any?, _: Method?, _: Array<Any?>? -> null } as UploadContract.View
|
||||||
|
|
||||||
|
const val COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES: String =
|
||||||
|
"number_of_consecutive_uploads_without_coordinates"
|
||||||
|
|
||||||
|
const val CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD: Int = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ data class UploadResult(
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
parcel.readString() ?: "",
|
parcel.readString() ?: "",
|
||||||
parcel.readString() ?: "",
|
parcel.readString() ?: "",
|
||||||
parcel.readInt() ?: 0,
|
parcel.readInt(),
|
||||||
parcel.readString() ?: "",
|
parcel.readString() ?: "",
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,425 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload.categories;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import com.jakewharton.rxbinding2.view.RxView;
|
|
||||||
import com.jakewharton.rxbinding2.widget.RxTextView;
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.Media;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
|
||||||
import fr.free.nrw.commons.category.CategoryItem;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionsFragment;
|
|
||||||
import fr.free.nrw.commons.databinding.UploadCategoriesFragmentBinding;
|
|
||||||
import fr.free.nrw.commons.media.MediaDetailFragment;
|
|
||||||
import fr.free.nrw.commons.upload.UploadActivity;
|
|
||||||
import fr.free.nrw.commons.upload.UploadBaseFragment;
|
|
||||||
import fr.free.nrw.commons.utils.DialogUtil;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.disposables.Disposable;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import kotlin.Unit;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class UploadCategoriesFragment extends UploadBaseFragment implements CategoriesContract.View {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
CategoriesContract.UserActionListener presenter;
|
|
||||||
@Inject
|
|
||||||
SessionManager sessionManager;
|
|
||||||
private UploadCategoryAdapter adapter;
|
|
||||||
private Disposable subscribe;
|
|
||||||
/**
|
|
||||||
* Current media
|
|
||||||
*/
|
|
||||||
private Media media;
|
|
||||||
/**
|
|
||||||
* Progress Dialog for showing background process
|
|
||||||
*/
|
|
||||||
private ProgressDialog progressDialog;
|
|
||||||
/**
|
|
||||||
* WikiText from the server
|
|
||||||
*/
|
|
||||||
private String wikiText;
|
|
||||||
private String nearbyPlaceCategory;
|
|
||||||
|
|
||||||
private UploadCategoriesFragmentBinding binding;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
|
|
||||||
@Nullable final Bundle savedInstanceState) {
|
|
||||||
binding = UploadCategoriesFragmentBinding.inflate(inflater, container, false);
|
|
||||||
return binding.getRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
final Bundle bundle = getArguments();
|
|
||||||
if (bundle != null) {
|
|
||||||
media = bundle.getParcelable("Existing_Categories");
|
|
||||||
wikiText = bundle.getString("WikiText");
|
|
||||||
nearbyPlaceCategory = bundle.getString(SELECTED_NEARBY_PLACE_CATEGORY);
|
|
||||||
}
|
|
||||||
init();
|
|
||||||
presenter.getCategories().observe(getViewLifecycleOwner(), this::setCategories);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init() {
|
|
||||||
if (binding == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (media == null) {
|
|
||||||
if (callback != null) {
|
|
||||||
binding.tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
|
|
||||||
callback.getTotalNumberOfSteps(), getString(R.string.categories_activity_title)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.tvTitle.setText(R.string.edit_categories);
|
|
||||||
binding.tvSubtitle.setVisibility(View.GONE);
|
|
||||||
binding.btnNext.setText(R.string.menu_save_categories);
|
|
||||||
binding.btnPrevious.setText(R.string.menu_cancel_upload);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTvSubTitle();
|
|
||||||
binding.tooltip.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(final View v) {
|
|
||||||
DialogUtil.showAlertDialog(requireActivity(),
|
|
||||||
getString(R.string.categories_activity_title),
|
|
||||||
getString(R.string.categories_tooltip),
|
|
||||||
getString(android.R.string.ok),
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (media == null) {
|
|
||||||
presenter.onAttachView(this);
|
|
||||||
} else {
|
|
||||||
presenter.onAttachViewWithMedia(this, media);
|
|
||||||
}
|
|
||||||
binding.btnNext.setOnClickListener(v -> onNextButtonClicked());
|
|
||||||
binding.btnPrevious.setOnClickListener(v -> onPreviousButtonClicked());
|
|
||||||
|
|
||||||
initRecyclerView();
|
|
||||||
addTextChangeListenerToEtSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addTextChangeListenerToEtSearch() {
|
|
||||||
if (binding == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
subscribe = RxTextView.textChanges(binding.etSearch)
|
|
||||||
.doOnEach(v -> binding.tilContainerSearch.setError(null))
|
|
||||||
.takeUntil(RxView.detaches(binding.etSearch))
|
|
||||||
.debounce(500, TimeUnit.MILLISECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(filter -> searchForCategory(filter.toString()), Timber::e);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the tv subtitle If the activity is the instance of [UploadActivity] and
|
|
||||||
* if multiple files aren't selected.
|
|
||||||
*/
|
|
||||||
private void setTvSubTitle() {
|
|
||||||
final Activity activity = getActivity();
|
|
||||||
if (activity instanceof UploadActivity) {
|
|
||||||
final boolean isMultipleFileSelected = ((UploadActivity) activity).getIsMultipleFilesSelected();
|
|
||||||
if (!isMultipleFileSelected) {
|
|
||||||
binding.tvSubtitle.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void searchForCategory(final String query) {
|
|
||||||
presenter.searchForCategories(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initRecyclerView() {
|
|
||||||
adapter = new UploadCategoryAdapter(categoryItem -> {
|
|
||||||
presenter.onCategoryItemClicked(categoryItem);
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
}, nearbyPlaceCategory);
|
|
||||||
|
|
||||||
if (binding!=null) {
|
|
||||||
binding.rvCategories.setLayoutManager(new LinearLayoutManager(getContext()));
|
|
||||||
binding.rvCategories.setAdapter(adapter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
super.onDestroyView();
|
|
||||||
presenter.onDetachView();
|
|
||||||
subscribe.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showProgress(final boolean shouldShow) {
|
|
||||||
if (binding != null) {
|
|
||||||
binding.pbCategories.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showError(final String error) {
|
|
||||||
if (binding != null) {
|
|
||||||
binding.tilContainerSearch.setError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showError(final int stringResourceId) {
|
|
||||||
if (binding != null) {
|
|
||||||
binding.tilContainerSearch.setError(getString(stringResourceId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setCategories(final List<CategoryItem> categories) {
|
|
||||||
if (categories == null) {
|
|
||||||
adapter.clear();
|
|
||||||
} else {
|
|
||||||
adapter.setItems(categories);
|
|
||||||
}
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
|
|
||||||
|
|
||||||
if (binding == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Nested waiting for search result data to load into the category
|
|
||||||
// list and smoothly scroll to the top of the search result list.
|
|
||||||
binding.rvCategories.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
binding.rvCategories.smoothScrollToPosition(0);
|
|
||||||
binding.rvCategories.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
binding.rvCategories.smoothScrollToPosition(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void goToNextScreen() {
|
|
||||||
if (callback != null){
|
|
||||||
callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showNoCategorySelected() {
|
|
||||||
if (media == null) {
|
|
||||||
DialogUtil.showAlertDialog(requireActivity(),
|
|
||||||
getString(R.string.no_categories_selected),
|
|
||||||
getString(R.string.no_categories_selected_warning_desc),
|
|
||||||
getString(R.string.continue_message),
|
|
||||||
getString(R.string.cancel),
|
|
||||||
this::goToNextScreen,
|
|
||||||
null);
|
|
||||||
} else {
|
|
||||||
Toast.makeText(requireContext(), getString(R.string.no_categories_selected),
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
presenter.clearPreviousSelection();
|
|
||||||
goBackToPreviousScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets existing categories from media
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public List<String> getExistingCategories() {
|
|
||||||
return (media == null) ? null : media.getCategories();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns required context
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Context getFragmentContext() {
|
|
||||||
return requireContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns to previous fragment
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void goBackToPreviousScreen() {
|
|
||||||
getFragmentManager().popBackStack();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the progress dialog
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void showProgressDialog() {
|
|
||||||
progressDialog = new ProgressDialog(requireContext());
|
|
||||||
progressDialog.setMessage(getString(R.string.please_wait));
|
|
||||||
progressDialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the progress dialog
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void dismissProgressDialog() {
|
|
||||||
if (progressDialog != null) {
|
|
||||||
progressDialog.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes the categories
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void refreshCategories() {
|
|
||||||
final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
|
|
||||||
assert mediaDetailFragment != null;
|
|
||||||
mediaDetailFragment.updateCategories();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void navigateToLoginScreen() {
|
|
||||||
final String username = sessionManager.getUserName();
|
|
||||||
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
|
|
||||||
requireActivity(),
|
|
||||||
requireActivity().getString(R.string.invalid_login_message),
|
|
||||||
username
|
|
||||||
);
|
|
||||||
|
|
||||||
CommonsApplication.getInstance().clearApplicationData(
|
|
||||||
requireActivity(), logoutListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onNextButtonClicked() {
|
|
||||||
if (media != null) {
|
|
||||||
presenter.updateCategories(media, wikiText);
|
|
||||||
} else {
|
|
||||||
presenter.verifyCategories();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onPreviousButtonClicked() {
|
|
||||||
if (media != null) {
|
|
||||||
presenter.clearPreviousSelection();
|
|
||||||
adapter.setItems(null);
|
|
||||||
final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
|
|
||||||
assert mediaDetailFragment != null;
|
|
||||||
mediaDetailFragment.onResume();
|
|
||||||
goBackToPreviousScreen();
|
|
||||||
} else {
|
|
||||||
if (callback != null) {
|
|
||||||
callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onBecameVisible() {
|
|
||||||
super.onBecameVisible();
|
|
||||||
if (binding == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
presenter.selectCategories();
|
|
||||||
final Editable text = binding.etSearch.getText();
|
|
||||||
if (text != null) {
|
|
||||||
presenter.searchForCategories(text.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the action bar while opening editing fragment
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
if (media != null) {
|
|
||||||
binding.etSearch.setOnKeyListener((v, keyCode, event) -> {
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
|
||||||
binding.etSearch.clearFocus();
|
|
||||||
presenter.clearPreviousSelection();
|
|
||||||
final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
|
|
||||||
assert mediaDetailFragment != null;
|
|
||||||
mediaDetailFragment.onResume();
|
|
||||||
goBackToPreviousScreen();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
requireView().setFocusableInTouchMode(true);
|
|
||||||
getView().requestFocus();
|
|
||||||
getView().setOnKeyListener((v, keyCode, event) -> {
|
|
||||||
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
|
||||||
presenter.clearPreviousSelection();
|
|
||||||
final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
|
|
||||||
assert mediaDetailFragment != null;
|
|
||||||
mediaDetailFragment.onResume();
|
|
||||||
goBackToPreviousScreen();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
Objects.requireNonNull(
|
|
||||||
((AppCompatActivity) requireActivity()).getSupportActionBar())
|
|
||||||
.hide();
|
|
||||||
|
|
||||||
if (getParentFragment().getParentFragment().getParentFragment()
|
|
||||||
instanceof ContributionsFragment) {
|
|
||||||
((ContributionsFragment) (getParentFragment()
|
|
||||||
.getParentFragment().getParentFragment())).binding.cardViewNearby
|
|
||||||
.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the action bar while closing editing fragment
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
if (media != null) {
|
|
||||||
Objects.requireNonNull(
|
|
||||||
((AppCompatActivity) requireActivity()).getSupportActionBar())
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
binding = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,397 @@
|
||||||
|
package fr.free.nrw.commons.upload.categories
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.ProgressDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.jakewharton.rxbinding2.view.RxView
|
||||||
|
import com.jakewharton.rxbinding2.widget.RxTextView
|
||||||
|
import fr.free.nrw.commons.CommonsApplication
|
||||||
|
import fr.free.nrw.commons.CommonsApplication.Companion.instance
|
||||||
|
import fr.free.nrw.commons.Media
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
|
import fr.free.nrw.commons.category.CategoryItem
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionsFragment
|
||||||
|
import fr.free.nrw.commons.databinding.UploadCategoriesFragmentBinding
|
||||||
|
import fr.free.nrw.commons.media.MediaDetailFragment
|
||||||
|
import fr.free.nrw.commons.upload.UploadActivity
|
||||||
|
import fr.free.nrw.commons.upload.UploadBaseFragment
|
||||||
|
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY
|
||||||
|
import io.reactivex.Notification
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.disposables.Disposable
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.Objects
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View {
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var presenter: CategoriesContract.UserActionListener? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var sessionManager: SessionManager? = null
|
||||||
|
private var adapter: UploadCategoryAdapter? = null
|
||||||
|
private var subscribe: Disposable? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current media
|
||||||
|
*/
|
||||||
|
private var media: Media? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Progress Dialog for showing background process
|
||||||
|
*/
|
||||||
|
private var progressDialog: ProgressDialog? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WikiText from the server
|
||||||
|
*/
|
||||||
|
private var wikiText: String? = null
|
||||||
|
private var nearbyPlaceCategory: String? = null
|
||||||
|
|
||||||
|
private var binding: UploadCategoriesFragmentBinding? = null
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
binding = UploadCategoriesFragmentBinding.inflate(inflater, container, false)
|
||||||
|
return binding!!.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
val bundle = arguments
|
||||||
|
if (bundle != null) {
|
||||||
|
media = bundle.getParcelable("Existing_Categories")
|
||||||
|
wikiText = bundle.getString("WikiText")
|
||||||
|
nearbyPlaceCategory = bundle.getString(SELECTED_NEARBY_PLACE_CATEGORY)
|
||||||
|
}
|
||||||
|
init()
|
||||||
|
presenter!!.getCategories().observe(
|
||||||
|
viewLifecycleOwner
|
||||||
|
) { categories: List<CategoryItem>? ->
|
||||||
|
this.setCategories(
|
||||||
|
categories
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init() {
|
||||||
|
if (binding == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding!!.tvTitle.setText(R.string.edit_categories)
|
||||||
|
binding!!.tvSubtitle.visibility = View.GONE
|
||||||
|
binding!!.btnNext.setText(R.string.menu_save_categories)
|
||||||
|
binding!!.btnPrevious.setText(R.string.menu_cancel_upload)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTvSubTitle()
|
||||||
|
binding!!.tooltip.setOnClickListener {
|
||||||
|
showAlertDialog(
|
||||||
|
requireActivity(),
|
||||||
|
getString(R.string.categories_activity_title),
|
||||||
|
getString(R.string.categories_tooltip),
|
||||||
|
getString(android.R.string.ok),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (media == null) {
|
||||||
|
presenter!!.onAttachView(this)
|
||||||
|
} else {
|
||||||
|
presenter!!.onAttachViewWithMedia(this, media!!)
|
||||||
|
}
|
||||||
|
binding!!.btnNext.setOnClickListener { v: View? -> onNextButtonClicked() }
|
||||||
|
binding!!.btnPrevious.setOnClickListener { v: View? -> onPreviousButtonClicked() }
|
||||||
|
|
||||||
|
initRecyclerView()
|
||||||
|
addTextChangeListenerToEtSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addTextChangeListenerToEtSearch() {
|
||||||
|
if (binding == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
subscribe = RxTextView.textChanges(binding!!.etSearch)
|
||||||
|
.doOnEach { v: Notification<CharSequence?>? ->
|
||||||
|
binding!!.tilContainerSearch.error =
|
||||||
|
null
|
||||||
|
}
|
||||||
|
.takeUntil(RxView.detaches(binding!!.etSearch))
|
||||||
|
.debounce(500, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ filter: CharSequence -> searchForCategory(filter.toString()) },
|
||||||
|
{ t: Throwable? -> Timber.e(t) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the tv subtitle If the activity is the instance of [UploadActivity] and
|
||||||
|
* if multiple files aren't selected.
|
||||||
|
*/
|
||||||
|
private fun setTvSubTitle() {
|
||||||
|
val activity: Activity? = activity
|
||||||
|
if (activity is UploadActivity) {
|
||||||
|
val isMultipleFileSelected = activity.isMultipleFilesSelected
|
||||||
|
if (!isMultipleFileSelected) {
|
||||||
|
binding!!.tvSubtitle.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchForCategory(query: String) {
|
||||||
|
presenter!!.searchForCategories(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initRecyclerView() {
|
||||||
|
adapter = UploadCategoryAdapter({ categoryItem: CategoryItem? ->
|
||||||
|
presenter!!.onCategoryItemClicked(categoryItem!!)
|
||||||
|
Unit
|
||||||
|
}, nearbyPlaceCategory)
|
||||||
|
|
||||||
|
if (binding != null) {
|
||||||
|
binding!!.rvCategories.layoutManager = LinearLayoutManager(context)
|
||||||
|
binding!!.rvCategories.adapter = adapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
presenter!!.onDetachView()
|
||||||
|
subscribe!!.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showProgress(shouldShow: Boolean) {
|
||||||
|
binding?.pbCategories?.setVisibility(if (shouldShow) View.VISIBLE else View.GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showError(error: String?) {
|
||||||
|
binding?.tilContainerSearch?.error = error
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showError(stringResourceId: Int) {
|
||||||
|
binding?.tilContainerSearch?.error = getString(stringResourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCategories(categories: List<CategoryItem>?) {
|
||||||
|
if (categories == null) {
|
||||||
|
adapter!!.clear()
|
||||||
|
} else {
|
||||||
|
adapter!!.items = categories
|
||||||
|
}
|
||||||
|
adapter!!.notifyDataSetChanged()
|
||||||
|
|
||||||
|
if (binding == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Nested waiting for search result data to load into the category
|
||||||
|
// list and smoothly scroll to the top of the search result list.
|
||||||
|
binding!!.rvCategories.post {
|
||||||
|
binding!!.rvCategories.smoothScrollToPosition(0)
|
||||||
|
binding!!.rvCategories.post {
|
||||||
|
binding!!.rvCategories.smoothScrollToPosition(
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun goToNextScreen() {
|
||||||
|
callback!!.onNextButtonClicked(callback!!.getIndexInViewFlipper(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showNoCategorySelected() {
|
||||||
|
if (media == null) {
|
||||||
|
showAlertDialog(
|
||||||
|
requireActivity(),
|
||||||
|
getString(R.string.no_categories_selected),
|
||||||
|
getString(R.string.no_categories_selected_warning_desc),
|
||||||
|
getString(R.string.continue_message),
|
||||||
|
getString(R.string.cancel),
|
||||||
|
{ this.goToNextScreen() },
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(), getString(R.string.no_categories_selected),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
presenter!!.clearPreviousSelection()
|
||||||
|
goBackToPreviousScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets existing categories from media
|
||||||
|
*/
|
||||||
|
override fun getExistingCategories(): List<String>? {
|
||||||
|
return media?.categories
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns required context
|
||||||
|
*/
|
||||||
|
override fun getFragmentContext(): Context {
|
||||||
|
return requireContext()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns to previous fragment
|
||||||
|
*/
|
||||||
|
override fun goBackToPreviousScreen() {
|
||||||
|
fragmentManager?.popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the progress dialog
|
||||||
|
*/
|
||||||
|
override fun showProgressDialog() {
|
||||||
|
progressDialog = ProgressDialog(requireContext()).apply {
|
||||||
|
setMessage(getString(R.string.please_wait))
|
||||||
|
}.also {
|
||||||
|
it.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the progress dialog
|
||||||
|
*/
|
||||||
|
override fun dismissProgressDialog() {
|
||||||
|
progressDialog?.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the categories
|
||||||
|
*/
|
||||||
|
override fun refreshCategories() {
|
||||||
|
(parentFragment as MediaDetailFragment?)?.updateCategories()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
override fun navigateToLoginScreen() {
|
||||||
|
val username = sessionManager!!.userName
|
||||||
|
val logoutListener = CommonsApplication.BaseLogoutListener(
|
||||||
|
requireActivity(),
|
||||||
|
requireActivity().getString(R.string.invalid_login_message),
|
||||||
|
username
|
||||||
|
)
|
||||||
|
|
||||||
|
instance.clearApplicationData(
|
||||||
|
requireActivity(), logoutListener
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNextButtonClicked() {
|
||||||
|
if (media != null) {
|
||||||
|
presenter!!.updateCategories(media!!, wikiText!!)
|
||||||
|
} else {
|
||||||
|
presenter!!.verifyCategories()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPreviousButtonClicked() {
|
||||||
|
if (media != null) {
|
||||||
|
presenter!!.clearPreviousSelection()
|
||||||
|
adapter!!.items = null
|
||||||
|
val mediaDetailFragment = checkNotNull(parentFragment as MediaDetailFragment?)
|
||||||
|
mediaDetailFragment.onResume()
|
||||||
|
goBackToPreviousScreen()
|
||||||
|
} else {
|
||||||
|
callback!!.onPreviousButtonClicked(callback!!.getIndexInViewFlipper(this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBecameVisible() {
|
||||||
|
super.onBecameVisible()
|
||||||
|
if (binding == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
presenter!!.selectCategories()
|
||||||
|
val text = binding!!.etSearch.text
|
||||||
|
if (text != null) {
|
||||||
|
presenter!!.searchForCategories(text.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the action bar while opening editing fragment
|
||||||
|
*/
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
if (media != null) {
|
||||||
|
binding!!.etSearch.setOnKeyListener { v: View?, keyCode: Int, event: KeyEvent? ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
binding!!.etSearch.clearFocus()
|
||||||
|
presenter!!.clearPreviousSelection()
|
||||||
|
val mediaDetailFragment =
|
||||||
|
checkNotNull(parentFragment as MediaDetailFragment?)
|
||||||
|
mediaDetailFragment.onResume()
|
||||||
|
goBackToPreviousScreen()
|
||||||
|
return@setOnKeyListener true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
requireView().isFocusableInTouchMode = true
|
||||||
|
requireView().requestFocus()
|
||||||
|
requireView().setOnKeyListener { v: View?, keyCode: Int, event: KeyEvent ->
|
||||||
|
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
presenter!!.clearPreviousSelection()
|
||||||
|
val mediaDetailFragment =
|
||||||
|
checkNotNull(parentFragment as MediaDetailFragment?)
|
||||||
|
mediaDetailFragment.onResume()
|
||||||
|
goBackToPreviousScreen()
|
||||||
|
return@setOnKeyListener true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
(requireActivity() as AppCompatActivity).supportActionBar?.hide()
|
||||||
|
|
||||||
|
if (parentFragment?.parentFragment?.parentFragment is ContributionsFragment) {
|
||||||
|
((parentFragment?.parentFragment?.parentFragment) as ContributionsFragment).binding.cardViewNearby.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the action bar while closing editing fragment
|
||||||
|
*/
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
if (media != null) {
|
||||||
|
(requireActivity() as AppCompatActivity).supportActionBar?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,444 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload.depicts;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import com.jakewharton.rxbinding2.view.RxView;
|
|
||||||
import com.jakewharton.rxbinding2.widget.RxTextView;
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.Media;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionsFragment;
|
|
||||||
import fr.free.nrw.commons.databinding.UploadDepictsFragmentBinding;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import fr.free.nrw.commons.media.MediaDetailFragment;
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
|
||||||
import fr.free.nrw.commons.upload.UploadActivity;
|
|
||||||
import fr.free.nrw.commons.upload.UploadBaseFragment;
|
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
|
||||||
import fr.free.nrw.commons.utils.DialogUtil;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.disposables.Disposable;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import kotlin.Unit;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment for showing depicted items list in Upload activity after media details
|
|
||||||
*/
|
|
||||||
public class DepictsFragment extends UploadBaseFragment implements DepictsContract.View {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@Named("default_preferences")
|
|
||||||
public
|
|
||||||
JsonKvStore applicationKvStore;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
DepictsContract.UserActionListener presenter;
|
|
||||||
private UploadDepictsAdapter adapter;
|
|
||||||
private Disposable subscribe;
|
|
||||||
private Media media;
|
|
||||||
private ProgressDialog progressDialog;
|
|
||||||
/**
|
|
||||||
* Determines each encounter of edit depicts
|
|
||||||
*/
|
|
||||||
private int count;
|
|
||||||
private Place nearbyPlace;
|
|
||||||
|
|
||||||
private UploadDepictsFragmentBinding binding;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
SessionManager sessionManager;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public android.view.View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
|
||||||
@Nullable Bundle savedInstanceState) {
|
|
||||||
binding = UploadDepictsFragmentBinding.inflate(inflater, container, false);
|
|
||||||
return binding.getRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(@NonNull android.view.View view, @Nullable Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
|
|
||||||
Bundle bundle = getArguments();
|
|
||||||
if (bundle != null) {
|
|
||||||
media = bundle.getParcelable("Existing_Depicts");
|
|
||||||
nearbyPlace = bundle.getParcelable(SELECTED_NEARBY_PLACE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(callback!=null || media!=null){
|
|
||||||
init();
|
|
||||||
presenter.getDepictedItems().observe(getViewLifecycleOwner(), this::setDepictsList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize presenter and views
|
|
||||||
*/
|
|
||||||
private void init() {
|
|
||||||
|
|
||||||
if (binding == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media == null) {
|
|
||||||
binding.depictsTitle.setText(String.format(getString(R.string.step_count), callback.getIndexInViewFlipper(this) + 1,
|
|
||||||
callback.getTotalNumberOfSteps(), getString(R.string.depicts_step_title)));
|
|
||||||
} else {
|
|
||||||
binding.depictsTitle.setText(R.string.edit_depictions);
|
|
||||||
binding.depictsSubtitle.setVisibility(View.GONE);
|
|
||||||
binding.depictsNext.setText(R.string.menu_save_categories);
|
|
||||||
binding.depictsPrevious.setText(R.string.menu_cancel_upload);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDepictsSubTitle();
|
|
||||||
binding.tooltip.setOnClickListener(v -> DialogUtil
|
|
||||||
.showAlertDialog(getActivity(), getString(R.string.depicts_step_title),
|
|
||||||
getString(R.string.depicts_tooltip), getString(android.R.string.ok), null));
|
|
||||||
if (media == null) {
|
|
||||||
presenter.onAttachView(this);
|
|
||||||
} else {
|
|
||||||
presenter.onAttachViewWithMedia(this, media);
|
|
||||||
}
|
|
||||||
initRecyclerView();
|
|
||||||
addTextChangeListenerToSearchBox();
|
|
||||||
|
|
||||||
binding.depictsNext.setOnClickListener(v->onNextButtonClicked());
|
|
||||||
binding.depictsPrevious.setOnClickListener(v->onPreviousButtonClicked());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the depicts subtitle If the activity is the instance of [UploadActivity] and
|
|
||||||
* if multiple files aren't selected.
|
|
||||||
*/
|
|
||||||
private void setDepictsSubTitle() {
|
|
||||||
final Activity activity = getActivity();
|
|
||||||
if (activity instanceof UploadActivity) {
|
|
||||||
final boolean isMultipleFileSelected = ((UploadActivity) activity).getIsMultipleFilesSelected();
|
|
||||||
if (!isMultipleFileSelected) {
|
|
||||||
binding.depictsSubtitle.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise recyclerView and set adapter
|
|
||||||
*/
|
|
||||||
private void initRecyclerView() {
|
|
||||||
if (media == null) {
|
|
||||||
adapter = new UploadDepictsAdapter(categoryItem -> {
|
|
||||||
presenter.onDepictItemClicked(categoryItem);
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
}, nearbyPlace);
|
|
||||||
} else {
|
|
||||||
adapter = new UploadDepictsAdapter(item -> {
|
|
||||||
presenter.onDepictItemClicked(item);
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
}, nearbyPlace);
|
|
||||||
}
|
|
||||||
if (binding == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
binding.depictsRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
|
||||||
binding.depictsRecyclerView.setAdapter(adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onBecameVisible() {
|
|
||||||
super.onBecameVisible();
|
|
||||||
// Select Place depiction as the fragment becomes visible to ensure that the most up to date
|
|
||||||
// Place is used (i.e. if the user accepts a nearby place dialog)
|
|
||||||
presenter.selectPlaceDepictions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void goToNextScreen() {
|
|
||||||
callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void goToPreviousScreen() {
|
|
||||||
callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void noDepictionSelected() {
|
|
||||||
if (media == null) {
|
|
||||||
DialogUtil.showAlertDialog(getActivity(),
|
|
||||||
getString(R.string.no_depictions_selected),
|
|
||||||
getString(R.string.no_depictions_selected_warning_desc),
|
|
||||||
getString(R.string.continue_message),
|
|
||||||
getString(R.string.cancel),
|
|
||||||
this::goToNextScreen,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Toast.makeText(requireContext(), getString(R.string.no_depictions_selected),
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
presenter.clearPreviousSelection();
|
|
||||||
updateDepicts();
|
|
||||||
goBackToPreviousScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
super.onDestroyView();
|
|
||||||
media = null;
|
|
||||||
presenter.onDetachView();
|
|
||||||
subscribe.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showProgress(boolean shouldShow) {
|
|
||||||
if (binding == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
binding.depictsSearchInProgress.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showError(boolean value) {
|
|
||||||
if (binding == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (value) {
|
|
||||||
binding.depictsSearchContainer.setError(getString(R.string.no_depiction_found));
|
|
||||||
} else {
|
|
||||||
binding.depictsSearchContainer.setErrorEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setDepictsList(List<DepictedItem> depictedItemList) {
|
|
||||||
|
|
||||||
if (applicationKvStore.getBoolean("first_edit_depict")) {
|
|
||||||
count = 1;
|
|
||||||
applicationKvStore.putBoolean("first_edit_depict", false);
|
|
||||||
adapter.setItems(depictedItemList);
|
|
||||||
} else {
|
|
||||||
if ((count == 0) && (!depictedItemList.isEmpty())) {
|
|
||||||
adapter.setItems(null);
|
|
||||||
count = 1;
|
|
||||||
} else {
|
|
||||||
adapter.setItems(depictedItemList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binding == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Nested waiting for search result data to load into the depicted item
|
|
||||||
// list and smoothly scroll to the top of the search result list.
|
|
||||||
binding.depictsRecyclerView.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
binding.depictsRecyclerView.smoothScrollToPosition(0);
|
|
||||||
binding.depictsRecyclerView.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
binding.depictsRecyclerView.smoothScrollToPosition(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns required context
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Context getFragmentContext(){
|
|
||||||
return requireContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns to previous fragment
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void goBackToPreviousScreen() {
|
|
||||||
getFragmentManager().popBackStack();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets existing depictions IDs from media
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public List<String> getExistingDepictions(){
|
|
||||||
return (media == null) ? null : media.getDepictionIds();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the progress dialog
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void showProgressDialog() {
|
|
||||||
progressDialog = new ProgressDialog(requireContext());
|
|
||||||
progressDialog.setMessage(getString(R.string.please_wait));
|
|
||||||
progressDialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the progress dialog
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void dismissProgressDialog() {
|
|
||||||
progressDialog.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the depicts
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void updateDepicts() {
|
|
||||||
final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
|
|
||||||
assert mediaDetailFragment != null;
|
|
||||||
mediaDetailFragment.onResume();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigates to the login Activity
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void navigateToLoginScreen() {
|
|
||||||
final String username = sessionManager.getUserName();
|
|
||||||
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
|
|
||||||
getActivity(),
|
|
||||||
requireActivity().getString(R.string.invalid_login_message),
|
|
||||||
username
|
|
||||||
);
|
|
||||||
|
|
||||||
CommonsApplication.getInstance().clearApplicationData(
|
|
||||||
requireActivity(), logoutListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the calling fragment by media nullability and act accordingly
|
|
||||||
*/
|
|
||||||
public void onNextButtonClicked() {
|
|
||||||
if(media != null){
|
|
||||||
presenter.updateDepictions(media);
|
|
||||||
} else {
|
|
||||||
presenter.verifyDepictions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the calling fragment by media nullability and act accordingly
|
|
||||||
*/
|
|
||||||
public void onPreviousButtonClicked() {
|
|
||||||
if(media != null){
|
|
||||||
presenter.clearPreviousSelection();
|
|
||||||
updateDepicts();
|
|
||||||
goBackToPreviousScreen();
|
|
||||||
} else {
|
|
||||||
callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Text change listener for the edit text view of depicts
|
|
||||||
*/
|
|
||||||
private void addTextChangeListenerToSearchBox() {
|
|
||||||
subscribe = RxTextView.textChanges(binding.depictsSearch)
|
|
||||||
.doOnEach(v -> binding.depictsSearchContainer.setError(null))
|
|
||||||
.takeUntil(RxView.detaches(binding.depictsSearch))
|
|
||||||
.debounce(500, TimeUnit.MILLISECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(filter -> searchForDepictions(filter.toString()), Timber::e);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for depictions for the following query
|
|
||||||
*
|
|
||||||
* @param query query string
|
|
||||||
*/
|
|
||||||
private void searchForDepictions(final String query) {
|
|
||||||
presenter.searchForDepictions(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the action bar while opening editing fragment
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
if (media != null) {
|
|
||||||
binding.depictsSearch.setOnKeyListener((v, keyCode, event) -> {
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
|
||||||
binding.depictsSearch.clearFocus();
|
|
||||||
presenter.clearPreviousSelection();
|
|
||||||
updateDepicts();
|
|
||||||
goBackToPreviousScreen();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
requireView().setFocusableInTouchMode(true);
|
|
||||||
getView().requestFocus();
|
|
||||||
getView().setOnKeyListener((v, keyCode, event) -> {
|
|
||||||
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
|
||||||
presenter.clearPreviousSelection();
|
|
||||||
updateDepicts();
|
|
||||||
goBackToPreviousScreen();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
Objects.requireNonNull(
|
|
||||||
((AppCompatActivity) requireActivity()).getSupportActionBar())
|
|
||||||
.hide();
|
|
||||||
|
|
||||||
if (getParentFragment().getParentFragment().getParentFragment()
|
|
||||||
instanceof ContributionsFragment) {
|
|
||||||
((ContributionsFragment) (getParentFragment()
|
|
||||||
.getParentFragment().getParentFragment())).binding.cardViewNearby
|
|
||||||
.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the action bar while closing editing fragment
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
if (media != null) {
|
|
||||||
Objects.requireNonNull(
|
|
||||||
((AppCompatActivity) requireActivity()).getSupportActionBar())
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
binding = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,425 @@
|
||||||
|
package fr.free.nrw.commons.upload.depicts
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.ProgressDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.jakewharton.rxbinding2.view.RxView
|
||||||
|
import com.jakewharton.rxbinding2.widget.RxTextView
|
||||||
|
import fr.free.nrw.commons.CommonsApplication
|
||||||
|
import fr.free.nrw.commons.CommonsApplication.Companion.instance
|
||||||
|
import fr.free.nrw.commons.Media
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionsFragment
|
||||||
|
import fr.free.nrw.commons.databinding.UploadDepictsFragmentBinding
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import fr.free.nrw.commons.media.MediaDetailFragment
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
import fr.free.nrw.commons.upload.UploadActivity
|
||||||
|
import fr.free.nrw.commons.upload.UploadBaseFragment
|
||||||
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE
|
||||||
|
import io.reactivex.Notification
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.disposables.Disposable
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment for showing depicted items list in Upload activity after media details
|
||||||
|
*/
|
||||||
|
class DepictsFragment : UploadBaseFragment(), DepictsContract.View {
|
||||||
|
@Inject
|
||||||
|
@field:Named("default_preferences")
|
||||||
|
lateinit var applicationKvStore: JsonKvStore
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var presenter: DepictsContract.UserActionListener
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
|
private var adapter: UploadDepictsAdapter? = null
|
||||||
|
private var subscribe: Disposable? = null
|
||||||
|
private var media: Media? = null
|
||||||
|
private var progressDialog: ProgressDialog? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines each encounter of edit depicts
|
||||||
|
*/
|
||||||
|
private var count = 0
|
||||||
|
private var nearbyPlace: Place? = null
|
||||||
|
|
||||||
|
private var _binding: UploadDepictsFragmentBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = UploadDepictsFragmentBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
arguments?.let {
|
||||||
|
media = it.getParcelable("Existing_Depicts")
|
||||||
|
nearbyPlace = it.getParcelable(SELECTED_NEARBY_PLACE)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback != null || media != null) {
|
||||||
|
init()
|
||||||
|
presenter.getDepictedItems().observe(viewLifecycleOwner, ::setDepictsList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize presenter and views
|
||||||
|
*/
|
||||||
|
private fun init() {
|
||||||
|
if (_binding == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
binding.depictsTitle.setText(R.string.edit_depictions)
|
||||||
|
binding.depictsSubtitle.visibility = View.GONE
|
||||||
|
binding.depictsNext.setText(R.string.menu_save_categories)
|
||||||
|
binding.depictsPrevious.setText(R.string.menu_cancel_upload)
|
||||||
|
}
|
||||||
|
|
||||||
|
setDepictsSubTitle()
|
||||||
|
binding.tooltip.setOnClickListener { v: View? ->
|
||||||
|
showAlertDialog(
|
||||||
|
requireActivity(),
|
||||||
|
getString(R.string.depicts_step_title),
|
||||||
|
getString(R.string.depicts_tooltip),
|
||||||
|
getString(android.R.string.ok),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (media == null) {
|
||||||
|
presenter.onAttachView(this)
|
||||||
|
} else {
|
||||||
|
presenter.onAttachViewWithMedia(this, media!!)
|
||||||
|
}
|
||||||
|
initRecyclerView()
|
||||||
|
addTextChangeListenerToSearchBox()
|
||||||
|
|
||||||
|
binding.depictsNext.setOnClickListener { v: View? -> onNextButtonClicked() }
|
||||||
|
binding.depictsPrevious.setOnClickListener { v: View? -> onPreviousButtonClicked() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the depicts subtitle If the activity is the instance of [UploadActivity] and
|
||||||
|
* if multiple files aren't selected.
|
||||||
|
*/
|
||||||
|
private fun setDepictsSubTitle() {
|
||||||
|
val activity: Activity? = activity
|
||||||
|
if (activity is UploadActivity) {
|
||||||
|
val isMultipleFileSelected = activity.isMultipleFilesSelected
|
||||||
|
if (!isMultipleFileSelected) {
|
||||||
|
binding.depictsSubtitle.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise recyclerView and set adapter
|
||||||
|
*/
|
||||||
|
private fun initRecyclerView() {
|
||||||
|
adapter = if (media == null) {
|
||||||
|
UploadDepictsAdapter({ categoryItem: DepictedItem? ->
|
||||||
|
presenter.onDepictItemClicked(categoryItem!!)
|
||||||
|
}, nearbyPlace)
|
||||||
|
} else {
|
||||||
|
UploadDepictsAdapter({ item: DepictedItem? ->
|
||||||
|
presenter.onDepictItemClicked(item!!)
|
||||||
|
}, nearbyPlace)
|
||||||
|
}
|
||||||
|
if (_binding == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
binding.depictsRecyclerView.layoutManager = LinearLayoutManager(context)
|
||||||
|
binding.depictsRecyclerView.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBecameVisible() {
|
||||||
|
super.onBecameVisible()
|
||||||
|
// Select Place depiction as the fragment becomes visible to ensure that the most up to date
|
||||||
|
// Place is used (i.e. if the user accepts a nearby place dialog)
|
||||||
|
presenter.selectPlaceDepictions()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun goToNextScreen() {
|
||||||
|
callback!!.onNextButtonClicked(callback!!.getIndexInViewFlipper(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun goToPreviousScreen() {
|
||||||
|
callback!!.onPreviousButtonClicked(callback!!.getIndexInViewFlipper(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun noDepictionSelected() {
|
||||||
|
if (media == null) {
|
||||||
|
showAlertDialog(
|
||||||
|
requireActivity(),
|
||||||
|
getString(R.string.no_depictions_selected),
|
||||||
|
getString(R.string.no_depictions_selected_warning_desc),
|
||||||
|
getString(R.string.continue_message),
|
||||||
|
getString(R.string.cancel),
|
||||||
|
{ goToNextScreen() },
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(), getString(R.string.no_depictions_selected),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
presenter.clearPreviousSelection()
|
||||||
|
updateDepicts()
|
||||||
|
goBackToPreviousScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
media = null
|
||||||
|
presenter.onDetachView()
|
||||||
|
subscribe!!.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showProgress(shouldShow: Boolean) {
|
||||||
|
if (_binding == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
binding.depictsSearchInProgress.visibility =
|
||||||
|
if (shouldShow) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showError(value: Boolean) {
|
||||||
|
if (_binding == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
binding.depictsSearchContainer.error =
|
||||||
|
getString(R.string.no_depiction_found)
|
||||||
|
} else {
|
||||||
|
binding.depictsSearchContainer.isErrorEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDepictsList(depictedItemList: List<DepictedItem>) {
|
||||||
|
if (applicationKvStore.getBoolean("first_edit_depict")) {
|
||||||
|
count = 1
|
||||||
|
applicationKvStore.putBoolean("first_edit_depict", false)
|
||||||
|
adapter!!.items = depictedItemList
|
||||||
|
} else {
|
||||||
|
if ((count == 0) && (!depictedItemList.isEmpty())) {
|
||||||
|
adapter!!.items = null
|
||||||
|
count = 1
|
||||||
|
} else {
|
||||||
|
adapter!!.items = depictedItemList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_binding == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Nested waiting for search result data to load into the depicted item
|
||||||
|
// list and smoothly scroll to the top of the search result list.
|
||||||
|
binding.depictsRecyclerView.post {
|
||||||
|
binding.depictsRecyclerView.smoothScrollToPosition(0)
|
||||||
|
binding.depictsRecyclerView.post {
|
||||||
|
binding.depictsRecyclerView.smoothScrollToPosition(
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns required context
|
||||||
|
*/
|
||||||
|
override fun getFragmentContext(): Context {
|
||||||
|
return requireContext()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns to previous fragment
|
||||||
|
*/
|
||||||
|
override fun goBackToPreviousScreen() {
|
||||||
|
fragmentManager?.popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets existing depictions IDs from media
|
||||||
|
*/
|
||||||
|
override fun getExistingDepictions(): List<String>? {
|
||||||
|
return if ((media == null)) null else media!!.depictionIds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the progress dialog
|
||||||
|
*/
|
||||||
|
override fun showProgressDialog() {
|
||||||
|
progressDialog = ProgressDialog(requireContext())
|
||||||
|
progressDialog!!.setMessage(getString(R.string.please_wait))
|
||||||
|
progressDialog!!.setCancelable(false)
|
||||||
|
progressDialog!!.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the progress dialog
|
||||||
|
*/
|
||||||
|
override fun dismissProgressDialog() {
|
||||||
|
progressDialog?.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the depicts
|
||||||
|
*/
|
||||||
|
override fun updateDepicts() {
|
||||||
|
(parentFragment as MediaDetailFragment?)?.onResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to the login Activity
|
||||||
|
*/
|
||||||
|
override fun navigateToLoginScreen() {
|
||||||
|
val username = sessionManager.userName
|
||||||
|
val logoutListener = CommonsApplication.BaseLogoutListener(
|
||||||
|
requireActivity(),
|
||||||
|
requireActivity().getString(R.string.invalid_login_message),
|
||||||
|
username
|
||||||
|
)
|
||||||
|
|
||||||
|
instance.clearApplicationData(
|
||||||
|
requireActivity(), logoutListener
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the calling fragment by media nullability and act accordingly
|
||||||
|
*/
|
||||||
|
fun onNextButtonClicked() {
|
||||||
|
if (media != null) {
|
||||||
|
presenter.updateDepictions(media!!)
|
||||||
|
} else {
|
||||||
|
presenter.verifyDepictions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the calling fragment by media nullability and act accordingly
|
||||||
|
*/
|
||||||
|
fun onPreviousButtonClicked() {
|
||||||
|
if (media != null) {
|
||||||
|
presenter.clearPreviousSelection()
|
||||||
|
updateDepicts()
|
||||||
|
goBackToPreviousScreen()
|
||||||
|
} else {
|
||||||
|
callback!!.onPreviousButtonClicked(callback!!.getIndexInViewFlipper(this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text change listener for the edit text view of depicts
|
||||||
|
*/
|
||||||
|
private fun addTextChangeListenerToSearchBox() {
|
||||||
|
subscribe = RxTextView.textChanges(binding.depictsSearch)
|
||||||
|
.doOnEach { v: Notification<CharSequence?>? ->
|
||||||
|
binding.depictsSearchContainer.error =
|
||||||
|
null
|
||||||
|
}
|
||||||
|
.takeUntil(RxView.detaches(binding.depictsSearch))
|
||||||
|
.debounce(500, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ filter: CharSequence -> searchForDepictions(filter.toString()) },
|
||||||
|
{ t: Throwable? -> Timber.e(t) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for depictions for the following query
|
||||||
|
*
|
||||||
|
* @param query query string
|
||||||
|
*/
|
||||||
|
private fun searchForDepictions(query: String) {
|
||||||
|
presenter.searchForDepictions(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the action bar while opening editing fragment
|
||||||
|
*/
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
if (media != null) {
|
||||||
|
binding.depictsSearch.setOnKeyListener { v: View?, keyCode: Int, event: KeyEvent? ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
binding.depictsSearch.clearFocus()
|
||||||
|
presenter.clearPreviousSelection()
|
||||||
|
updateDepicts()
|
||||||
|
goBackToPreviousScreen()
|
||||||
|
return@setOnKeyListener true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
requireView().isFocusableInTouchMode = true
|
||||||
|
requireView().requestFocus()
|
||||||
|
requireView().setOnKeyListener { v: View?, keyCode: Int, event: KeyEvent ->
|
||||||
|
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
presenter.clearPreviousSelection()
|
||||||
|
updateDepicts()
|
||||||
|
goBackToPreviousScreen()
|
||||||
|
return@setOnKeyListener true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
(requireActivity() as AppCompatActivity).supportActionBar?.hide()
|
||||||
|
|
||||||
|
if (parentFragment?.parentFragment?.parentFragment is ContributionsFragment) {
|
||||||
|
((parentFragment?.parentFragment?.parentFragment) as ContributionsFragment?)?.binding?.cardViewNearby?.setVisibility(View.GONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the action bar while closing editing fragment
|
||||||
|
*/
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
if (media != null) {
|
||||||
|
(requireActivity() as AppCompatActivity).supportActionBar?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,13 +11,13 @@ interface MediaLicenseContract {
|
||||||
|
|
||||||
fun setSelectedLicense(license: String?)
|
fun setSelectedLicense(license: String?)
|
||||||
|
|
||||||
fun updateLicenseSummary(selectedLicense: String?, numberOfItems: Int?)
|
fun updateLicenseSummary(selectedLicense: String?, numberOfItems: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActionListener : BasePresenter<View> {
|
interface UserActionListener : BasePresenter<View> {
|
||||||
fun getLicenses()
|
fun getLicenses()
|
||||||
|
|
||||||
fun selectLicense(licenseName: String)
|
fun selectLicense(licenseName: String?)
|
||||||
|
|
||||||
fun isWLMSupportedForThisPlace(): Boolean
|
fun isWLMSupportedForThisPlace(): Boolean
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload.license;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.Html;
|
|
||||||
import android.text.SpannableStringBuilder;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.text.style.ClickableSpan;
|
|
||||||
import android.text.style.URLSpan;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.AdapterView.OnItemSelectedListener;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.databinding.FragmentMediaLicenseBinding;
|
|
||||||
import fr.free.nrw.commons.upload.UploadActivity;
|
|
||||||
import fr.free.nrw.commons.utils.DialogUtil;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.Utils;
|
|
||||||
import fr.free.nrw.commons.upload.UploadBaseFragment;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class MediaLicenseFragment extends UploadBaseFragment implements MediaLicenseContract.View {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
MediaLicenseContract.UserActionListener presenter;
|
|
||||||
|
|
||||||
private FragmentMediaLicenseBinding binding;
|
|
||||||
private ArrayAdapter<String> adapter;
|
|
||||||
private List<String> licenses;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
|
||||||
@Nullable Bundle savedInstanceState) {
|
|
||||||
binding = FragmentMediaLicenseBinding.inflate(inflater, container, false);
|
|
||||||
return binding.getRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
|
|
||||||
binding.tvTitle.setText(getString(R.string.step_count,
|
|
||||||
callback.getIndexInViewFlipper(this) + 1,
|
|
||||||
callback.getTotalNumberOfSteps(),
|
|
||||||
getString(R.string.license_step_title))
|
|
||||||
);
|
|
||||||
setTvSubTitle();
|
|
||||||
binding.btnPrevious.setOnClickListener(v ->
|
|
||||||
callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this))
|
|
||||||
);
|
|
||||||
|
|
||||||
binding.btnSubmit.setOnClickListener(v ->
|
|
||||||
callback.onNextButtonClicked(callback.getIndexInViewFlipper(this))
|
|
||||||
);
|
|
||||||
|
|
||||||
binding.tooltip.setOnClickListener(v ->
|
|
||||||
DialogUtil.showAlertDialog(requireActivity(),
|
|
||||||
getString(R.string.license_step_title),
|
|
||||||
getString(R.string.license_tooltip),
|
|
||||||
getString(android.R.string.ok),
|
|
||||||
null)
|
|
||||||
);
|
|
||||||
|
|
||||||
initPresenter();
|
|
||||||
initLicenseSpinner();
|
|
||||||
presenter.getLicenses();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the tv Subtitle If the activity is the instance of [UploadActivity] and
|
|
||||||
* if multiple files aren't selected.
|
|
||||||
*/
|
|
||||||
private void setTvSubTitle() {
|
|
||||||
final Activity activity = getActivity();
|
|
||||||
if (activity instanceof UploadActivity) {
|
|
||||||
final boolean isMultipleFileSelected = ((UploadActivity) activity).getIsMultipleFilesSelected();
|
|
||||||
if (!isMultipleFileSelected) {
|
|
||||||
binding.tvSubtitle.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initPresenter() {
|
|
||||||
presenter.onAttachView(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise the license spinner
|
|
||||||
*/
|
|
||||||
private void initLicenseSpinner() {
|
|
||||||
if (getActivity() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
adapter = new ArrayAdapter<>(getActivity().getApplicationContext(), android.R.layout.simple_spinner_dropdown_item);
|
|
||||||
binding.spinnerLicenseList.setAdapter(adapter);
|
|
||||||
binding.spinnerLicenseList.setOnItemSelectedListener(new OnItemSelectedListener() {
|
|
||||||
@Override
|
|
||||||
public void onItemSelected(AdapterView<?> adapterView, View view, int position,
|
|
||||||
long l) {
|
|
||||||
String licenseName = adapterView.getItemAtPosition(position).toString();
|
|
||||||
presenter.selectLicense(licenseName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNothingSelected(AdapterView<?> adapterView) {
|
|
||||||
presenter.selectLicense(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setLicenses(List<String> licenses) {
|
|
||||||
adapter.clear();
|
|
||||||
this.licenses = licenses;
|
|
||||||
adapter.addAll(this.licenses);
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSelectedLicense(String license) {
|
|
||||||
int position = licenses.indexOf(getString(Utils.licenseNameFor(license)));
|
|
||||||
// Check if position is valid
|
|
||||||
if (position < 0) {
|
|
||||||
Timber.d("Invalid position: %d. Using default licenses", position);
|
|
||||||
position = licenses.size() - 1;
|
|
||||||
} else {
|
|
||||||
Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license)));
|
|
||||||
}
|
|
||||||
binding.spinnerLicenseList.setSelection(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateLicenseSummary(String licenseSummary, Integer numberOfItems) {
|
|
||||||
String licenseHyperLink = "<a href='" + Utils.licenseUrlFor(licenseSummary) + "'>" +
|
|
||||||
getString(Utils.licenseNameFor(licenseSummary)) + "</a><br>";
|
|
||||||
|
|
||||||
setTextViewHTML(binding.tvShareLicenseSummary, getResources()
|
|
||||||
.getQuantityString(R.plurals.share_license_summary, numberOfItems,
|
|
||||||
licenseHyperLink));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setTextViewHTML(TextView textView, String text) {
|
|
||||||
CharSequence sequence = Html.fromHtml(text);
|
|
||||||
SpannableStringBuilder strBuilder = new SpannableStringBuilder(sequence);
|
|
||||||
URLSpan[] urls = strBuilder.getSpans(0, sequence.length(), URLSpan.class);
|
|
||||||
for (URLSpan span : urls) {
|
|
||||||
makeLinkClickable(strBuilder, span);
|
|
||||||
}
|
|
||||||
textView.setText(strBuilder);
|
|
||||||
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void makeLinkClickable(SpannableStringBuilder strBuilder, final URLSpan span) {
|
|
||||||
int start = strBuilder.getSpanStart(span);
|
|
||||||
int end = strBuilder.getSpanEnd(span);
|
|
||||||
int flags = strBuilder.getSpanFlags(span);
|
|
||||||
ClickableSpan clickable = new ClickableSpan() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
// Handle hyperlink click
|
|
||||||
String hyperLink = span.getURL();
|
|
||||||
launchBrowser(hyperLink);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
strBuilder.setSpan(clickable, start, end, flags);
|
|
||||||
strBuilder.removeSpan(span);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void launchBrowser(String hyperLink) {
|
|
||||||
Utils.handleWebUrl(getContext(), Uri.parse(hyperLink));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
presenter.onDetachView();
|
|
||||||
//Free the adapter to avoid memory leaks
|
|
||||||
adapter = null;
|
|
||||||
binding = null;
|
|
||||||
super.onDestroyView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onBecameVisible() {
|
|
||||||
super.onBecameVisible();
|
|
||||||
/**
|
|
||||||
* Show the wlm info message if the upload is a WLM upload
|
|
||||||
*/
|
|
||||||
if(callback.isWLMUpload() && presenter.isWLMSupportedForThisPlace()){
|
|
||||||
binding.llInfoMonumentUpload.setVisibility(View.VISIBLE);
|
|
||||||
}else{
|
|
||||||
binding.llInfoMonumentUpload.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
package fr.free.nrw.commons.upload.license
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
|
import android.text.style.URLSpan
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.TextView
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.Utils
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentMediaLicenseBinding
|
||||||
|
import fr.free.nrw.commons.upload.UploadActivity
|
||||||
|
import fr.free.nrw.commons.upload.UploadBaseFragment
|
||||||
|
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class MediaLicenseFragment : UploadBaseFragment(), MediaLicenseContract.View {
|
||||||
|
@Inject
|
||||||
|
lateinit var presenter: MediaLicenseContract.UserActionListener
|
||||||
|
|
||||||
|
private var _binding: FragmentMediaLicenseBinding? = null
|
||||||
|
private val binding: FragmentMediaLicenseBinding get() = _binding!!
|
||||||
|
|
||||||
|
private var adapter: ArrayAdapter<String>? = null
|
||||||
|
private var licenses: List<String>? = null
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentMediaLicenseBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.tvTitle.text = getString(
|
||||||
|
R.string.step_count,
|
||||||
|
callback!!.getIndexInViewFlipper(this) + 1,
|
||||||
|
callback!!.totalNumberOfSteps,
|
||||||
|
getString(R.string.license_step_title)
|
||||||
|
)
|
||||||
|
setTvSubTitle()
|
||||||
|
binding.btnPrevious.setOnClickListener {
|
||||||
|
callback!!.onPreviousButtonClicked(
|
||||||
|
callback!!.getIndexInViewFlipper(this)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnSubmit.setOnClickListener {
|
||||||
|
callback!!.onNextButtonClicked(
|
||||||
|
callback!!.getIndexInViewFlipper(this)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.tooltip.setOnClickListener {
|
||||||
|
showAlertDialog(
|
||||||
|
requireActivity(),
|
||||||
|
getString(R.string.license_step_title),
|
||||||
|
getString(R.string.license_tooltip),
|
||||||
|
getString(android.R.string.ok),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
initPresenter()
|
||||||
|
initLicenseSpinner()
|
||||||
|
presenter.getLicenses()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the tv Subtitle If the activity is the instance of [UploadActivity] and
|
||||||
|
* if multiple files aren't selected.
|
||||||
|
*/
|
||||||
|
private fun setTvSubTitle() {
|
||||||
|
val activity: Activity? = activity
|
||||||
|
if (activity is UploadActivity) {
|
||||||
|
if (!activity.isMultipleFilesSelected) {
|
||||||
|
binding.tvSubtitle.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initPresenter() = presenter.onAttachView(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise the license spinner
|
||||||
|
*/
|
||||||
|
private fun initLicenseSpinner() {
|
||||||
|
if (activity == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
adapter = ArrayAdapter(
|
||||||
|
requireActivity().applicationContext,
|
||||||
|
android.R.layout.simple_spinner_dropdown_item
|
||||||
|
)
|
||||||
|
binding.spinnerLicenseList.adapter = adapter
|
||||||
|
binding.spinnerLicenseList.onItemSelectedListener =
|
||||||
|
object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(adapterView: AdapterView<*>, view: View, position: Int, l: Long) {
|
||||||
|
val licenseName = adapterView.getItemAtPosition(position).toString()
|
||||||
|
presenter.selectLicense(licenseName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(adapterView: AdapterView<*>?) {
|
||||||
|
presenter.selectLicense(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setLicenses(licenses: List<String>?) {
|
||||||
|
adapter!!.clear()
|
||||||
|
this.licenses = licenses
|
||||||
|
adapter!!.addAll(this.licenses!!)
|
||||||
|
adapter!!.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSelectedLicense(license: String?) {
|
||||||
|
var position = licenses!!.indexOf(getString(Utils.licenseNameFor(license)))
|
||||||
|
// Check if position is valid
|
||||||
|
if (position < 0) {
|
||||||
|
Timber.d("Invalid position: %d. Using default licenses", position)
|
||||||
|
position = licenses!!.size - 1
|
||||||
|
} else {
|
||||||
|
Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license)))
|
||||||
|
}
|
||||||
|
binding.spinnerLicenseList.setSelection(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateLicenseSummary(selectedLicense: String?, numberOfItems: Int) {
|
||||||
|
val licenseHyperLink = "<a href='" + Utils.licenseUrlFor(selectedLicense) + "'>" +
|
||||||
|
getString(Utils.licenseNameFor(selectedLicense)) + "</a><br>"
|
||||||
|
|
||||||
|
setTextViewHTML(
|
||||||
|
binding.tvShareLicenseSummary, resources
|
||||||
|
.getQuantityString(
|
||||||
|
R.plurals.share_license_summary, numberOfItems,
|
||||||
|
licenseHyperLink
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTextViewHTML(textView: TextView, text: String) {
|
||||||
|
val sequence: CharSequence = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
|
||||||
|
} else {
|
||||||
|
Html.fromHtml(text)
|
||||||
|
}
|
||||||
|
val strBuilder = SpannableStringBuilder(sequence)
|
||||||
|
val urls = strBuilder.getSpans(
|
||||||
|
0, sequence.length,
|
||||||
|
URLSpan::class.java
|
||||||
|
)
|
||||||
|
for (span in urls) {
|
||||||
|
makeLinkClickable(strBuilder, span)
|
||||||
|
}
|
||||||
|
textView.text = strBuilder
|
||||||
|
textView.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeLinkClickable(strBuilder: SpannableStringBuilder, span: URLSpan) {
|
||||||
|
val start = strBuilder.getSpanStart(span)
|
||||||
|
val end = strBuilder.getSpanEnd(span)
|
||||||
|
val flags = strBuilder.getSpanFlags(span)
|
||||||
|
val clickable: ClickableSpan = object : ClickableSpan() {
|
||||||
|
override fun onClick(view: View) {
|
||||||
|
// Handle hyperlink click
|
||||||
|
val hyperLink = span.url
|
||||||
|
launchBrowser(hyperLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strBuilder.setSpan(clickable, start, end, flags)
|
||||||
|
strBuilder.removeSpan(span)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchBrowser(hyperLink: String) =
|
||||||
|
Utils.handleWebUrl(context, Uri.parse(hyperLink))
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
presenter.onDetachView()
|
||||||
|
//Free the adapter to avoid memory leaks
|
||||||
|
adapter = null
|
||||||
|
_binding = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBecameVisible() {
|
||||||
|
super.onBecameVisible()
|
||||||
|
/**
|
||||||
|
* Show the wlm info message if the upload is a WLM upload
|
||||||
|
*/
|
||||||
|
binding.llInfoMonumentUpload.visibility =
|
||||||
|
if (callback!!.isWLMUpload && presenter.isWLMSupportedForThisPlace()) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload.license;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import fr.free.nrw.commons.Utils;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import fr.free.nrw.commons.repository.UploadRepository;
|
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
|
||||||
import fr.free.nrw.commons.upload.license.MediaLicenseContract.View;
|
|
||||||
import java.lang.reflect.Proxy;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Added JavaDocs for MediaLicensePresenter
|
|
||||||
*/
|
|
||||||
public class MediaLicensePresenter implements MediaLicenseContract.UserActionListener {
|
|
||||||
|
|
||||||
private static final MediaLicenseContract.View DUMMY = (MediaLicenseContract.View) Proxy
|
|
||||||
.newProxyInstance(
|
|
||||||
MediaLicenseContract.View.class.getClassLoader(),
|
|
||||||
new Class[]{MediaLicenseContract.View.class},
|
|
||||||
(proxy, method, methodArgs) -> null);
|
|
||||||
|
|
||||||
private final UploadRepository repository;
|
|
||||||
private final JsonKvStore defaultKVStore;
|
|
||||||
private MediaLicenseContract.View view = DUMMY;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public MediaLicensePresenter(final UploadRepository uploadRepository,
|
|
||||||
@Named("default_preferences") final JsonKvStore defaultKVStore) {
|
|
||||||
this.repository = uploadRepository;
|
|
||||||
this.defaultKVStore = defaultKVStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttachView(@NonNull final View view) {
|
|
||||||
this.view = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetachView() {
|
|
||||||
this.view = DUMMY;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* asks the repository for the available licenses, and informs the view on the same
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void getLicenses() {
|
|
||||||
final List<String> licenses = repository.getLicenses();
|
|
||||||
view.setLicenses(licenses);
|
|
||||||
|
|
||||||
String selectedLicense = defaultKVStore.getString(Prefs.DEFAULT_LICENSE,
|
|
||||||
Prefs.Licenses.CC_BY_SA_4);//CC_BY_SA_4 is the default one used by the commons web app
|
|
||||||
try {//I have to make sure that the stored default license was not one of the deprecated one's
|
|
||||||
Utils.licenseNameFor(selectedLicense);
|
|
||||||
} catch (final IllegalStateException exception) {
|
|
||||||
Timber.e(exception);
|
|
||||||
selectedLicense = Prefs.Licenses.CC_BY_SA_4;
|
|
||||||
defaultKVStore.putString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_4);
|
|
||||||
}
|
|
||||||
view.setSelectedLicense(selectedLicense);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ask the repository to select a license for the current upload
|
|
||||||
*
|
|
||||||
* @param licenseName
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void selectLicense(final String licenseName) {
|
|
||||||
repository.setSelectedLicense(licenseName);
|
|
||||||
view.updateLicenseSummary(repository.getSelectedLicense(), repository.getCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isWLMSupportedForThisPlace() {
|
|
||||||
return repository.isWMLSupportedForThisPlace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package fr.free.nrw.commons.upload.license
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.Utils
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import fr.free.nrw.commons.repository.UploadRepository
|
||||||
|
import fr.free.nrw.commons.settings.Prefs
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.lang.reflect.Proxy
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Added JavaDocs for MediaLicensePresenter
|
||||||
|
*/
|
||||||
|
class MediaLicensePresenter @Inject constructor(
|
||||||
|
private val repository: UploadRepository,
|
||||||
|
@param:Named("default_preferences") private val defaultKVStore: JsonKvStore
|
||||||
|
) : MediaLicenseContract.UserActionListener {
|
||||||
|
private var view = DUMMY
|
||||||
|
|
||||||
|
override fun onAttachView(view: MediaLicenseContract.View) {
|
||||||
|
this.view = view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachView() {
|
||||||
|
view = DUMMY
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* asks the repository for the available licenses, and informs the view on the same
|
||||||
|
*/
|
||||||
|
override fun getLicenses() {
|
||||||
|
val licenses = repository.getLicenses()
|
||||||
|
view.setLicenses(licenses)
|
||||||
|
|
||||||
|
var selectedLicense = defaultKVStore.getString(
|
||||||
|
Prefs.DEFAULT_LICENSE,
|
||||||
|
Prefs.Licenses.CC_BY_SA_4
|
||||||
|
) //CC_BY_SA_4 is the default one used by the commons web app
|
||||||
|
try { //I have to make sure that the stored default license was not one of the deprecated one's
|
||||||
|
Utils.licenseNameFor(selectedLicense)
|
||||||
|
} catch (exception: IllegalStateException) {
|
||||||
|
Timber.e(exception)
|
||||||
|
selectedLicense = Prefs.Licenses.CC_BY_SA_4
|
||||||
|
defaultKVStore.putString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_4)
|
||||||
|
}
|
||||||
|
view.setSelectedLicense(selectedLicense)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ask the repository to select a license for the current upload
|
||||||
|
*/
|
||||||
|
override fun selectLicense(licenseName: String?) {
|
||||||
|
repository.setSelectedLicense(licenseName)
|
||||||
|
view.updateLicenseSummary(repository.getSelectedLicense(), repository.getCount())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isWLMSupportedForThisPlace(): Boolean =
|
||||||
|
repository.isWMLSupportedForThisPlace()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DUMMY = Proxy.newProxyInstance(
|
||||||
|
MediaLicenseContract.View::class.java.classLoader,
|
||||||
|
arrayOf<Class<*>>(MediaLicenseContract.View::class.java)
|
||||||
|
) { _: Any?, _: Method?, _: Array<Any?>? -> null } as MediaLicenseContract.View
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -398,7 +398,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace);
|
final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace);
|
||||||
if (response) {
|
if (response) {
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace);
|
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -445,7 +445,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
() -> {
|
() -> {
|
||||||
// Execute when user confirms the upload is of the specified place
|
// Execute when user confirms the upload is of the specified place
|
||||||
UploadActivity.nearbyPopupAnswers.put(place, true);
|
UploadActivity.nearbyPopupAnswers.put(place, true);
|
||||||
presenter.onUserConfirmedUploadIsOfPlace(place);
|
presenter.onUserConfirmedUploadIsOfPlace(place, indexOfFragment);
|
||||||
},
|
},
|
||||||
() -> {
|
() -> {
|
||||||
// Execute when user cancels the upload of the specified place
|
// Execute when user cancels the upload of the specified place
|
||||||
|
|
@ -486,7 +486,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) {
|
if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) {
|
||||||
final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace);
|
final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace);
|
||||||
if (response) {
|
if (response) {
|
||||||
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace);
|
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showNearbyPlaceFound(nearbyPlace);
|
showNearbyPlaceFound(nearbyPlace);
|
||||||
|
|
@ -521,7 +521,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
getString(R.string.duplicate_file_name),
|
getString(R.string.duplicate_file_name),
|
||||||
String.format(Locale.getDefault(),
|
String.format(Locale.getDefault(),
|
||||||
uploadTitleFormat,
|
uploadTitleFormat,
|
||||||
uploadItem.getFileName()),
|
uploadItem.getFilename()),
|
||||||
getString(R.string.upload),
|
getString(R.string.upload),
|
||||||
getString(R.string.cancel),
|
getString(R.string.cancel),
|
||||||
() -> {
|
() -> {
|
||||||
|
|
@ -714,7 +714,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
if (binding != null){
|
if (binding != null){
|
||||||
binding.backgroundImage.setImageURI(Uri.fromFile(new File(path)));
|
binding.backgroundImage.setImageURI(Uri.fromFile(new File(path)));
|
||||||
}
|
}
|
||||||
editableUploadItem.setContentUri(Uri.fromFile(new File(path)));
|
editableUploadItem.setContentAndMediaUri(Uri.fromFile(new File(path)));
|
||||||
callback.changeThumbnail(indexOfFragment,
|
callback.changeThumbnail(indexOfFragment,
|
||||||
path);
|
path);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,6 @@ interface UploadMediaDetailsContract {
|
||||||
|
|
||||||
fun onEditButtonClicked(indexInViewFlipper: Int)
|
fun onEditButtonClicked(indexInViewFlipper: Int)
|
||||||
|
|
||||||
fun onUserConfirmedUploadIsOfPlace(place: Place?)
|
fun onUserConfirmedUploadIsOfPlace(place: Place?, uploadItemIndex: Int)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void setUploadMediaDetails(final List<UploadMediaDetail> uploadMediaDetails, final int uploadItemIndex) {
|
public void setUploadMediaDetails(final List<UploadMediaDetail> uploadMediaDetails, final int uploadItemIndex) {
|
||||||
repository.getUploads().get(uploadItemIndex).setMediaDetails(uploadMediaDetails);
|
repository.getUploads().get(uploadItemIndex).setUploadMediaDetails(uploadMediaDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -284,7 +284,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
public void copyTitleAndDescriptionToSubsequentMedia(final int indexInViewFlipper) {
|
public void copyTitleAndDescriptionToSubsequentMedia(final int indexInViewFlipper) {
|
||||||
for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){
|
for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){
|
||||||
final UploadItem subsequentUploadItem = repository.getUploads().get(i);
|
final UploadItem subsequentUploadItem = repository.getUploads().get(i);
|
||||||
subsequentUploadItem.setMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails()));
|
subsequentUploadItem.setUploadMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,23 +322,24 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the information regarding the specified place for uploads
|
* Updates the information regarding the specified place for the specified upload item
|
||||||
* when the user confirms the suggested nearby place.
|
* when the user confirms the suggested nearby place.
|
||||||
*
|
*
|
||||||
* @param place The place to be associated with the uploads.
|
* @param place The place to be associated with the uploads.
|
||||||
|
* @param uploadItemIndex Index of the uploadItem whose detected place has been confirmed
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onUserConfirmedUploadIsOfPlace(final Place place) {
|
public void onUserConfirmedUploadIsOfPlace(final Place place, final int uploadItemIndex) {
|
||||||
final List<UploadItem> uploads = repository.getUploads();
|
final UploadItem uploadItem = repository.getUploads().get(uploadItemIndex);
|
||||||
for (final UploadItem uploadItem : uploads) {
|
|
||||||
uploadItem.setPlace(place);
|
uploadItem.setPlace(place);
|
||||||
final List<UploadMediaDetail> uploadMediaDetails = uploadItem.getUploadMediaDetails();
|
final List<UploadMediaDetail> uploadMediaDetails = uploadItem.getUploadMediaDetails();
|
||||||
// Update UploadMediaDetail object for this UploadItem
|
// Update UploadMediaDetail object for this UploadItem
|
||||||
uploadMediaDetails.set(0, new UploadMediaDetail(place));
|
uploadMediaDetails.set(0, new UploadMediaDetail(place));
|
||||||
}
|
|
||||||
// Now that all UploadItems and their associated UploadMediaDetail objects have been updated,
|
// 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
|
// update the view with the modified media details of the first upload item
|
||||||
view.updateMediaDetails(uploads.get(0).getUploadMediaDetails());
|
view.updateMediaDetails(uploadMediaDetails);
|
||||||
UploadActivity.setUploadIsOfAPlace(true);
|
UploadActivity.setUploadIsOfAPlace(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,11 @@ class DepictModel
|
||||||
place.wikiDataEntityId?.let { qids.add(it) }
|
place.wikiDataEntityId?.let { qids.add(it) }
|
||||||
}
|
}
|
||||||
repository.getUploads().forEach { item ->
|
repository.getUploads().forEach { item ->
|
||||||
if (item.gpsCoords != null && item.gpsCoords.imageCoordsExists) {
|
if (item.gpsCoords != null && item.gpsCoords?.imageCoordsExists == true) {
|
||||||
Coordinates2Country
|
Coordinates2Country
|
||||||
.countryQID(
|
.countryQID(
|
||||||
item.gpsCoords.decLatitude,
|
item.gpsCoords!!.decLatitude,
|
||||||
item.gpsCoords.decLongitude,
|
item.gpsCoords!!.decLongitude,
|
||||||
)?.let { qids.add("Q$it") }
|
)?.let { qids.add("Q$it") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ class UploadWorker(
|
||||||
private var notificationFinishingTitle: String?,
|
private var notificationFinishingTitle: String?,
|
||||||
var contribution: Contribution?,
|
var contribution: Contribution?,
|
||||||
) {
|
) {
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
fun onProgress(
|
fun onProgress(
|
||||||
transferred: Long,
|
transferred: Long,
|
||||||
total: Long,
|
total: Long,
|
||||||
|
|
@ -175,6 +176,7 @@ class UploadWorker(
|
||||||
.setProgress(100, 0, true)
|
.setProgress(100, 0, true)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
try {
|
try {
|
||||||
var totalUploadsStarted = 0
|
var totalUploadsStarted = 0
|
||||||
|
|
@ -298,7 +300,7 @@ class UploadWorker(
|
||||||
* Upload the contribution
|
* Upload the contribution
|
||||||
* @param contribution
|
* @param contribution
|
||||||
*/
|
*/
|
||||||
@SuppressLint("StringFormatInvalid", "CheckResult")
|
@SuppressLint("StringFormatInvalid", "CheckResult", "MissingPermission")
|
||||||
private suspend fun uploadContribution(contribution: Contribution) {
|
private suspend fun uploadContribution(contribution: Contribution) {
|
||||||
if (contribution.localUri == null || contribution.localUri.path == null) {
|
if (contribution.localUri == null || contribution.localUri.path == null) {
|
||||||
Timber.e("""upload: ${contribution.media.filename} failed, file path is null""")
|
Timber.e("""upload: ${contribution.media.filename} failed, file path is null""")
|
||||||
|
|
@ -439,7 +441,7 @@ class UploadWorker(
|
||||||
username,
|
username,
|
||||||
)
|
)
|
||||||
CommonsApplication
|
CommonsApplication
|
||||||
.instance!!
|
.instance
|
||||||
.clearApplicationData(appContext, logoutListener)
|
.clearApplicationData(appContext, logoutListener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -479,8 +481,8 @@ class UploadWorker(
|
||||||
)
|
)
|
||||||
if (null != revisionID) {
|
if (null != revisionID) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val place = placesRepository.fetchPlace(wikiDataPlace.id);
|
val place = placesRepository.fetchPlace(wikiDataPlace.id)
|
||||||
place.name = wikiDataPlace.name;
|
place.name = wikiDataPlace.name
|
||||||
place.pic = HOME_URL + uploadResult.createCanonicalFileName()
|
place.pic = HOME_URL + uploadResult.createCanonicalFileName()
|
||||||
placesRepository
|
placesRepository
|
||||||
.save(place)
|
.save(place)
|
||||||
|
|
@ -566,12 +568,18 @@ class UploadWorker(
|
||||||
|
|
||||||
sequenceFileName =
|
sequenceFileName =
|
||||||
if (fileName.indexOf('.') == -1) {
|
if (fileName.indexOf('.') == -1) {
|
||||||
"$fileName #$randomHash"
|
// Append the random hash in parentheses if no file extension is present
|
||||||
|
"$fileName ($randomHash)"
|
||||||
} else {
|
} else {
|
||||||
val regex =
|
val regex =
|
||||||
Pattern.compile("^(.*)(\\..+?)$")
|
Pattern.compile("^(.*)(\\..+?)$")
|
||||||
val regexMatcher = regex.matcher(fileName)
|
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!!
|
return sequenceFileName!!
|
||||||
|
|
@ -581,7 +589,7 @@ class UploadWorker(
|
||||||
* Notify that the current upload has succeeded
|
* Notify that the current upload has succeeded
|
||||||
* @param contribution
|
* @param contribution
|
||||||
*/
|
*/
|
||||||
@SuppressLint("StringFormatInvalid")
|
@SuppressLint("StringFormatInvalid", "MissingPermission")
|
||||||
private fun showSuccessNotification(contribution: Contribution) {
|
private fun showSuccessNotification(contribution: Contribution) {
|
||||||
val displayTitle = contribution.media.displayTitle
|
val displayTitle = contribution.media.displayTitle
|
||||||
contribution.state = Contribution.STATE_COMPLETED
|
contribution.state = Contribution.STATE_COMPLETED
|
||||||
|
|
@ -606,7 +614,7 @@ class UploadWorker(
|
||||||
* Notify that the current upload has failed
|
* Notify that the current upload has failed
|
||||||
* @param contribution
|
* @param contribution
|
||||||
*/
|
*/
|
||||||
@SuppressLint("StringFormatInvalid")
|
@SuppressLint("StringFormatInvalid", "MissingPermission")
|
||||||
private fun showFailedNotification(contribution: Contribution) {
|
private fun showFailedNotification(contribution: Contribution) {
|
||||||
val displayTitle = contribution.media.displayTitle
|
val displayTitle = contribution.media.displayTitle
|
||||||
currentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
|
currentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
|
||||||
|
|
@ -626,7 +634,7 @@ class UploadWorker(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StringFormatInvalid")
|
@SuppressLint("StringFormatInvalid", "MissingPermission")
|
||||||
private fun showInvalidLoginNotification(contribution: Contribution) {
|
private fun showInvalidLoginNotification(contribution: Contribution) {
|
||||||
val displayTitle = contribution.media.displayTitle
|
val displayTitle = contribution.media.displayTitle
|
||||||
currentNotification
|
currentNotification
|
||||||
|
|
@ -648,7 +656,7 @@ class UploadWorker(
|
||||||
/**
|
/**
|
||||||
* Shows a notification for a failed contribution upload.
|
* Shows a notification for a failed contribution upload.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("StringFormatInvalid")
|
@SuppressLint("StringFormatInvalid", "MissingPermission")
|
||||||
private fun showErrorNotification(contribution: Contribution) {
|
private fun showErrorNotification(contribution: Contribution) {
|
||||||
val displayTitle = contribution.media.displayTitle
|
val displayTitle = contribution.media.displayTitle
|
||||||
currentNotification
|
currentNotification
|
||||||
|
|
@ -671,6 +679,7 @@ class UploadWorker(
|
||||||
* Notify that the current upload is paused
|
* Notify that the current upload is paused
|
||||||
* @param contribution
|
* @param contribution
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
private fun showPausedNotification(contribution: Contribution) {
|
private fun showPausedNotification(contribution: Contribution) {
|
||||||
val displayTitle = contribution.media.displayTitle
|
val displayTitle = contribution.media.displayTitle
|
||||||
|
|
||||||
|
|
@ -695,6 +704,7 @@ class UploadWorker(
|
||||||
* Notify that the current upload is cancelled
|
* Notify that the current upload is cancelled
|
||||||
* @param contribution
|
* @param contribution
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
private fun showCancelledNotification(contribution: Contribution) {
|
private fun showCancelledNotification(contribution: Contribution) {
|
||||||
val displayTitle = contribution.media.displayTitle
|
val displayTitle = contribution.media.displayTitle
|
||||||
currentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
|
currentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ class CustomSelectorUtils {
|
||||||
val uploadableFile = PickedFiles.pickedExistingPicture(context, image.uri)
|
val uploadableFile = PickedFiles.pickedExistingPicture(context, image.uri)
|
||||||
val exifInterface: ExifInterface? =
|
val exifInterface: ExifInterface? =
|
||||||
try {
|
try {
|
||||||
ExifInterface(uploadableFile.file!!)
|
ExifInterface(uploadableFile.file)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
null
|
null
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,6 @@ object DownloadUtils {
|
||||||
) {
|
) {
|
||||||
val systemService =
|
val systemService =
|
||||||
activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
systemService?.enqueue(req)
|
systemService.enqueue(req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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