mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 14:53:59 +01:00 
			
		
		
		
	Merge branch 'main' into fix-status-bar-color-in-dark-mode
This commit is contained in:
		
						commit
						8eab2afbb5
					
				
					 55 changed files with 3626 additions and 3842 deletions
				
			
		
							
								
								
									
										84
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										84
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -1,5 +1,89 @@ | ||||||
| # Wikimedia Commons for Android | # Wikimedia Commons for Android | ||||||
| 
 | 
 | ||||||
|  | ## v5.1.2 | ||||||
|  | 
 | ||||||
|  | ### What's changed | ||||||
|  | 
 | ||||||
|  | * Fix the broken category search in the explore screen | ||||||
|  | 
 | ||||||
|  | ## v5.1.1 | ||||||
|  | 
 | ||||||
|  | ### What's changed | ||||||
|  | 
 | ||||||
|  | * Use Android's new EXIF interface to mitigate security issues in old | ||||||
|  |   EXIF interface. | ||||||
|  | * Make the icon that helps view the upload queue always visible as it ensures | ||||||
|  |   that the queue accessible at all times. | ||||||
|  | 
 | ||||||
|  | ## v5.1.0 | ||||||
|  | 
 | ||||||
|  | ### What's Changed | ||||||
|  | 
 | ||||||
|  | * Enhanced **upload queue management** in the Commons app for smoother, sequential | ||||||
|  |   processing, clearer progress tracking, prevention of stuck or duplicate | ||||||
|  |   uploads. As part of this improvement, the "Limited Connection mode" has been | ||||||
|  |   removed. | ||||||
|  | * Added an option in "Nearby" feature enabling users to **provide feedback on | ||||||
|  |   Wikidata items**. Users can report if an item doesn’t exist, is at a different | ||||||
|  |   location, or has other issues, with submissions tagged for easy tracking and | ||||||
|  |   updates. | ||||||
|  | * Improved the "Nearby" feature by splitting the query into two parts for faster | ||||||
|  |   loading and **better performance, especially in areas with dense amount of | ||||||
|  |   places**. This update also resolves issues with pins overlapping place names. | ||||||
|  | * Upgraded AGP and **target/compile SDK to 34** and make necessary adjustments to | ||||||
|  |   the app such as adding **"Partial Access" support**. Also includes some minor | ||||||
|  |   refactoring, and replacement of deprecated circular progress bars. | ||||||
|  | * Fixed an **UI issue where the 'Subcategories' and 'Parent Categories' tabs | ||||||
|  |   appeared blank** in the Category Details screen. Resolved by optimizing view | ||||||
|  |   binding handling in the parent fragments. | ||||||
|  | * Fixed an issue where editing depictions removed all other structured data from | ||||||
|  |   images. Now, **only depictions are updated, preserving other associated data**. | ||||||
|  | * Fixed **map centering** in the image upload flow to **use GPS EXIF tag location** | ||||||
|  |   from pictures and ensured "Show in map app" accurately reflects this location. | ||||||
|  | * Fixed navigation **after uploading via Nearby by directing users to the Uploads | ||||||
|  |   activity** instead of returning to Nearby, preventing confusion about needing to | ||||||
|  |   upload again. | ||||||
|  | 
 | ||||||
|  | ### Bug fixes and various changes | ||||||
|  | 
 | ||||||
|  | * Improved the "Nearby" feature to fetch labels based on the user's preferred | ||||||
|  |   language instead of defaulting to English. | ||||||
|  | * Added a legend to the "Nearby" feature indicating pin statuses: red for items | ||||||
|  |   without pictures, green for those with pictures, and grey for items being | ||||||
|  |   checked. A floating action button now allows users to toggle the legend's | ||||||
|  |   visibility. | ||||||
|  | * Fixed an issue where the "Nominate for deletion" option is shown to logged out | ||||||
|  |   users, preventing app errors and crashes. | ||||||
|  | * Updated the regex pattern that filters categories with an year in it to also | ||||||
|  |   filter the 2020s. | ||||||
|  | * Fix an issue where past depictions were not shown as suggestions, despite | ||||||
|  |   being saved correctly. | ||||||
|  | * Fixed an issue in custom image picker where exiting the media preview showed | ||||||
|  |   only the first image and cleared selections. Now, previously selected images | ||||||
|  |   are restored correctly after exiting the preview. This was contributed. | ||||||
|  | * Fixed an issue in custom image picker where scrolling behavior did not | ||||||
|  |   maintain position after exiting fullscreen preview, ensuring users remain at | ||||||
|  |   the same point in their image roll unless actioned images are filtered. This | ||||||
|  |   was contributed. | ||||||
|  | * Fixed Nearby map not showing new pins on map move by removing the 2000m scroll | ||||||
|  |   threshold and adding an 800ms debounce for smoother pin updates when the map | ||||||
|  |   is moved. Queued searches are now canceled on fragment destruction. | ||||||
|  | * Revised author information retrieval to emphasize the custom author name from | ||||||
|  |   the metadata instead of the default registered username. | ||||||
|  | * Enhanced notification classification to properly identify "email" type | ||||||
|  |   notifications and prompting users to check their e-mail inbox when such | ||||||
|  |   notifications are clicked. | ||||||
|  | * Resolved a bug in the language chooser that incorrectly greyed-out previously | ||||||
|  |   selected languages, ensuring only the current language is non-selectable during | ||||||
|  |   image upload. | ||||||
|  | * Resolved pin color update issue in "Nearby" feature where the pin colour | ||||||
|  |   failed to be updated after a successful image upload. | ||||||
|  | 
 | ||||||
|  | What's listed here is only a subset of all the changes. Check the full-list of | ||||||
|  | the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v5.0.2...v5.1.0). | ||||||
|  | Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.1.0) | ||||||
|  | for an exhaustive list of changes and the various contributors who contributed the same. | ||||||
|  | 
 | ||||||
| ## v5.0.2 | ## v5.0.2 | ||||||
| 
 | 
 | ||||||
| - Enhanced multi-upload functionality with user prompts to clarify that all images would share the | - Enhanced multi-upload functionality with user prompts to clarify that all images would share the | ||||||
|  |  | ||||||
|  | @ -212,8 +212,8 @@ android { | ||||||
|     defaultConfig { |     defaultConfig { | ||||||
|         //applicationId 'fr.free.nrw.commons' |         //applicationId 'fr.free.nrw.commons' | ||||||
| 
 | 
 | ||||||
|         versionCode 1040 |         versionCode 1043 | ||||||
|         versionName '5.0.2' |         versionName '5.1.2' | ||||||
|         setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) |         setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) | ||||||
| 
 | 
 | ||||||
|         minSdkVersion 21 |         minSdkVersion 21 | ||||||
|  |  | ||||||
|  | @ -112,7 +112,7 @@ data class Contribution constructor( | ||||||
|          */ |          */ | ||||||
|         fun formatDescriptions(descriptions: List<UploadMediaDetail>) = |         fun formatDescriptions(descriptions: List<UploadMediaDetail>) = | ||||||
|             descriptions |             descriptions | ||||||
|                 .filter { it.descriptionText.isNotEmpty() } |                 .filter { !it.descriptionText.isNullOrEmpty() } | ||||||
|                 .joinToString(separator = "") { "{{${it.languageCode}|1=${it.descriptionText}}}" } |                 .joinToString(separator = "") { "{{${it.languageCode}|1=${it.descriptionText}}}" } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| package fr.free.nrw.commons.description | package fr.free.nrw.commons.description | ||||||
| 
 | 
 | ||||||
| import android.app.ProgressDialog | import android.app.ProgressDialog | ||||||
| import android.content.Intent |  | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| import android.os.Parcelable | import android.os.Parcelable | ||||||
| import android.speech.RecognizerIntent | import android.speech.RecognizerIntent | ||||||
|  | @ -72,7 +71,7 @@ class DescriptionEditActivity : | ||||||
| 
 | 
 | ||||||
|     private lateinit var binding: ActivityDescriptionEditBinding |     private lateinit var binding: ActivityDescriptionEditBinding | ||||||
| 
 | 
 | ||||||
|     private var descriptionAndCaptions: ArrayList<UploadMediaDetail>? = null |     private var descriptionAndCaptions: MutableList<UploadMediaDetail>? = null | ||||||
| 
 | 
 | ||||||
|     private val voiceInputResultLauncher = registerForActivityResult( |     private val voiceInputResultLauncher = registerForActivityResult( | ||||||
|         ActivityResultContracts.StartActivityForResult() |         ActivityResultContracts.StartActivityForResult() | ||||||
|  | @ -114,22 +113,18 @@ class DescriptionEditActivity : | ||||||
|      * Initializes the RecyclerView |      * Initializes the RecyclerView | ||||||
|      * @param descriptionAndCaptions list of description and caption |      * @param descriptionAndCaptions list of description and caption | ||||||
|      */ |      */ | ||||||
|     private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) { |     private fun initRecyclerView(descriptionAndCaptions: MutableList<UploadMediaDetail>?) { | ||||||
|         uploadMediaDetailAdapter = |         uploadMediaDetailAdapter = | ||||||
|             UploadMediaDetailAdapter( |             UploadMediaDetailAdapter( | ||||||
|                 this, |                 this, | ||||||
|                 savedLanguageValue, |                 savedLanguageValue, | ||||||
|                 descriptionAndCaptions, |                 descriptionAndCaptions ?: mutableListOf(), | ||||||
|                 recentLanguagesDao, |                 recentLanguagesDao, | ||||||
|                 voiceInputResultLauncher |                 voiceInputResultLauncher | ||||||
|             ) |             ) | ||||||
|         uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int -> | 
 | ||||||
|             showInfoAlert( |         uploadMediaDetailAdapter.callback = UploadMediaDetailAdapter.Callback(::showInfoAlert) | ||||||
|                 titleStringID, |         uploadMediaDetailAdapter.eventListener = this | ||||||
|                 messageStringId, |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|         uploadMediaDetailAdapter.setEventListener(this) |  | ||||||
|         rvDescriptions = binding.rvDescriptionsCaptions |         rvDescriptions = binding.rvDescriptionsCaptions | ||||||
|         rvDescriptions!!.layoutManager = LinearLayoutManager(this) |         rvDescriptions!!.layoutManager = LinearLayoutManager(this) | ||||||
|         rvDescriptions!!.adapter = uploadMediaDetailAdapter |         rvDescriptions!!.adapter = uploadMediaDetailAdapter | ||||||
|  |  | ||||||
|  | @ -41,8 +41,8 @@ import fr.free.nrw.commons.location.LocationPermissionsHelper | ||||||
| import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback | import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback | ||||||
| import fr.free.nrw.commons.location.LocationServiceManager | import fr.free.nrw.commons.location.LocationServiceManager | ||||||
| import fr.free.nrw.commons.theme.BaseActivity | import fr.free.nrw.commons.theme.BaseActivity | ||||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION | import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Companion.LAST_LOCATION | ||||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM | import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Companion.LAST_ZOOM | ||||||
| import fr.free.nrw.commons.utils.DialogUtil | import fr.free.nrw.commons.utils.DialogUtil | ||||||
| import fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL | import fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers | import io.reactivex.android.schedulers.AndroidSchedulers | ||||||
|  |  | ||||||
|  | @ -467,18 +467,35 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Retrieves the ContributionsFragment that is potentially the parent, grandparent, etc | ||||||
|  |      * fragment of this fragment. | ||||||
|  |      * | ||||||
|  |      * @return The ContributionsFragment instance. If the ContributionsFragment instance could not | ||||||
|  |      * be found, null is returned. | ||||||
|  |      */ | ||||||
|  |     private fun getContributionsFragmentParent(): ContributionsFragment? { | ||||||
|  |         var fragment: Fragment? = this | ||||||
|  | 
 | ||||||
|  |         while (fragment != null && fragment !is ContributionsFragment) { | ||||||
|  |             fragment = fragment.parentFragment | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (fragment == null) { | ||||||
|  |             return null | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return fragment as ContributionsFragment | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     override fun onResume() { |     override fun onResume() { | ||||||
|         super.onResume() |         super.onResume() | ||||||
|         if (parentFragment != null && requireParentFragment().parentFragment != null) { | 
 | ||||||
|             // Added a check because, not necessarily, the parent fragment |         val contributionsFragment: ContributionsFragment? = this.getContributionsFragmentParent() | ||||||
|             // will have a parent fragment, say in the case when MediaDetailPagerFragment |         if (contributionsFragment?.binding != null) { | ||||||
|             // is directly started by the CategoryImagesActivity |             contributionsFragment.binding.cardViewNearby.visibility = View.GONE | ||||||
|             if (parentFragment is ContributionsFragment) { |  | ||||||
|                 (((parentFragment as ContributionsFragment) |  | ||||||
|                     .parentFragment) as ContributionsFragment).binding.cardViewNearby.visibility = |  | ||||||
|                     View.GONE |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         // detail provider is null when fragment is shown in review activity |         // detail provider is null when fragment is shown in review activity | ||||||
|         media = if (detailProvider != null) { |         media = if (detailProvider != null) { | ||||||
|             detailProvider!!.getMediaAtPosition(index) |             detailProvider!!.getMediaAtPosition(index) | ||||||
|  | @ -1737,10 +1754,11 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         ProfileActivity.startYourself( |         ProfileActivity.startYourself( | ||||||
|             activity, |             requireActivity(),  // Ensure this is a non-null Activity context | ||||||
|             media!!.user, |             media?.user ?: "",  // Provide a fallback value if media?.user is null | ||||||
|             sessionManager.userName != media!!.user |             sessionManager.userName != media?.user  // This can remain as is, null check will apply | ||||||
|         ) |         ) | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -1,265 +0,0 @@ | ||||||
| package fr.free.nrw.commons.profile; |  | ||||||
| 
 |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.graphics.Bitmap; |  | ||||||
| import android.net.Uri; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.Menu; |  | ||||||
| import android.view.MenuInflater; |  | ||||||
| import android.view.MenuItem; |  | ||||||
| import android.view.View; |  | ||||||
| import android.widget.ImageView; |  | ||||||
| import android.widget.TextView; |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.core.content.FileProvider; |  | ||||||
| import androidx.fragment.app.Fragment; |  | ||||||
| import androidx.fragment.app.FragmentManager; |  | ||||||
| import fr.free.nrw.commons.R; |  | ||||||
| import fr.free.nrw.commons.Utils; |  | ||||||
| import fr.free.nrw.commons.ViewPagerAdapter; |  | ||||||
| import fr.free.nrw.commons.auth.SessionManager; |  | ||||||
| import fr.free.nrw.commons.contributions.ContributionsFragment; |  | ||||||
| import fr.free.nrw.commons.databinding.ActivityProfileBinding; |  | ||||||
| import fr.free.nrw.commons.profile.achievements.AchievementsFragment; |  | ||||||
| import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment; |  | ||||||
| import fr.free.nrw.commons.theme.BaseActivity; |  | ||||||
| import fr.free.nrw.commons.utils.DialogUtil; |  | ||||||
| import java.io.File; |  | ||||||
| import java.io.FileOutputStream; |  | ||||||
| import java.io.IOException; |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Locale; |  | ||||||
| import javax.inject.Inject; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * This activity will set two tabs, achievements and |  | ||||||
|  * each tab will have their own fragments |  | ||||||
|  */ |  | ||||||
| public class ProfileActivity extends BaseActivity { |  | ||||||
| 
 |  | ||||||
|     private FragmentManager supportFragmentManager; |  | ||||||
| 
 |  | ||||||
|     public ActivityProfileBinding binding; |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     SessionManager sessionManager; |  | ||||||
| 
 |  | ||||||
|     private ViewPagerAdapter viewPagerAdapter; |  | ||||||
|     private AchievementsFragment achievementsFragment; |  | ||||||
|     private LeaderboardFragment leaderboardFragment; |  | ||||||
| 
 |  | ||||||
|     public static final String KEY_USERNAME ="username"; |  | ||||||
|     public static final String KEY_SHOULD_SHOW_CONTRIBUTIONS ="shouldShowContributions"; |  | ||||||
| 
 |  | ||||||
|     String userName; |  | ||||||
|     private boolean  shouldShowContributions; |  | ||||||
| 
 |  | ||||||
|     ContributionsFragment contributionsFragment; |  | ||||||
| 
 |  | ||||||
|     public void setScroll(boolean canScroll){ |  | ||||||
|         binding.viewPager.setCanScroll(canScroll); |  | ||||||
|     } |  | ||||||
|     @Override |  | ||||||
|     protected void onRestoreInstanceState(final Bundle savedInstanceState) { |  | ||||||
|         super.onRestoreInstanceState(savedInstanceState); |  | ||||||
|         if (savedInstanceState != null) { |  | ||||||
|             userName = savedInstanceState.getString(KEY_USERNAME); |  | ||||||
|             shouldShowContributions = savedInstanceState.getBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
| 
 |  | ||||||
|         binding = ActivityProfileBinding.inflate(getLayoutInflater()); |  | ||||||
|         setContentView(binding.getRoot()); |  | ||||||
|         setSupportActionBar(binding.toolbarBinding.toolbar); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         binding.toolbarBinding.toolbar.setNavigationOnClickListener(view -> { |  | ||||||
|             onSupportNavigateUp(); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         userName = getIntent().getStringExtra(KEY_USERNAME); |  | ||||||
|         setTitle(userName); |  | ||||||
|         shouldShowContributions = getIntent().getBooleanExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, false); |  | ||||||
| 
 |  | ||||||
|         supportFragmentManager = getSupportFragmentManager(); |  | ||||||
|         viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); |  | ||||||
|         binding.viewPager.setAdapter(viewPagerAdapter); |  | ||||||
|         binding.tabLayout.setupWithViewPager(binding.viewPager); |  | ||||||
|         setTabs(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Navigate up event |  | ||||||
|      * @return boolean |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public boolean onSupportNavigateUp() { |  | ||||||
|         onBackPressed(); |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Creates a way to change current activity to AchievementActivity |  | ||||||
|      * |  | ||||||
|      * @param context |  | ||||||
|      */ |  | ||||||
|     public static void startYourself(final Context context, final String userName, |  | ||||||
|         final boolean shouldShowContributions) { |  | ||||||
|         Intent intent = new Intent(context, ProfileActivity.class); |  | ||||||
|         intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); |  | ||||||
|         intent.putExtra(KEY_USERNAME, userName); |  | ||||||
|         intent.putExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions); |  | ||||||
|         context.startActivity(intent); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Set the tabs for the fragments |  | ||||||
|      */ |  | ||||||
|     private void setTabs() { |  | ||||||
|         List<Fragment> fragmentList = new ArrayList<>(); |  | ||||||
|         List<String> titleList = new ArrayList<>(); |  | ||||||
|         achievementsFragment = new AchievementsFragment(); |  | ||||||
|         Bundle achievementsBundle = new Bundle(); |  | ||||||
|         achievementsBundle.putString(KEY_USERNAME, userName); |  | ||||||
|         achievementsFragment.setArguments(achievementsBundle); |  | ||||||
|         fragmentList.add(achievementsFragment); |  | ||||||
| 
 |  | ||||||
|         titleList.add(getResources().getString(R.string.achievements_tab_title).toUpperCase()); |  | ||||||
|         leaderboardFragment = new LeaderboardFragment(); |  | ||||||
|         Bundle leaderBoardBundle = new Bundle(); |  | ||||||
|         leaderBoardBundle.putString(KEY_USERNAME, userName); |  | ||||||
|         leaderboardFragment.setArguments(leaderBoardBundle); |  | ||||||
| 
 |  | ||||||
|         fragmentList.add(leaderboardFragment); |  | ||||||
|         titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase(Locale.ROOT)); |  | ||||||
| 
 |  | ||||||
|         contributionsFragment = new ContributionsFragment(); |  | ||||||
|         Bundle contributionsListBundle = new Bundle(); |  | ||||||
|         contributionsListBundle.putString(KEY_USERNAME, userName); |  | ||||||
|         contributionsFragment.setArguments(contributionsListBundle); |  | ||||||
|         fragmentList.add(contributionsFragment); |  | ||||||
|         titleList.add(getString(R.string.contributions_fragment).toUpperCase(Locale.ROOT)); |  | ||||||
| 
 |  | ||||||
|         viewPagerAdapter.setTabData(fragmentList, titleList); |  | ||||||
|         viewPagerAdapter.notifyDataSetChanged(); |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onDestroy() { |  | ||||||
|         super.onDestroy(); |  | ||||||
|         getCompositeDisposable().clear(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * To inflate menu |  | ||||||
|      * @param menu Menu |  | ||||||
|      * @return boolean |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public boolean onCreateOptionsMenu(final Menu menu) { |  | ||||||
|         final MenuInflater menuInflater = getMenuInflater(); |  | ||||||
|         menuInflater.inflate(R.menu.menu_about, menu); |  | ||||||
|         return super.onCreateOptionsMenu(menu); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * To receive the id of selected item and handle further logic for that selected item |  | ||||||
|      * @param item MenuItem |  | ||||||
|      * @return boolean |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public boolean onOptionsItemSelected(final MenuItem item) { |  | ||||||
|         // take screenshot in form of bitmap and show it in Alert Dialog |  | ||||||
|         if (item.getItemId() == R.id.share_app_icon) { |  | ||||||
|             final View rootView = getWindow().getDecorView().findViewById(android.R.id.content); |  | ||||||
|             final Bitmap screenShot = Utils.getScreenShot(rootView); |  | ||||||
|             showAlert(screenShot); |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|         return super.onOptionsItemSelected(item); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * It displays the alertDialog with Image of screenshot |  | ||||||
|      * @param screenshot screenshot of the present screen |  | ||||||
|      */ |  | ||||||
|     public void showAlert(final Bitmap screenshot) { |  | ||||||
|         final LayoutInflater factory = LayoutInflater.from(this); |  | ||||||
|         final View view = factory.inflate(R.layout.image_alert_layout, null); |  | ||||||
|         final ImageView screenShotImage = view.findViewById(R.id.alert_image); |  | ||||||
|         screenShotImage.setImageBitmap(screenshot); |  | ||||||
|         final TextView shareMessage = view.findViewById(R.id.alert_text); |  | ||||||
|         shareMessage.setText(R.string.achievements_share_message); |  | ||||||
|         DialogUtil.showAlertDialog(this, |  | ||||||
|             null, |  | ||||||
|             null, |  | ||||||
|             getString(R.string.about_translate_proceed), |  | ||||||
|             getString(R.string.cancel), |  | ||||||
|             () -> shareScreen(screenshot), |  | ||||||
|             () -> {}, |  | ||||||
|             view |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * To take bitmap and store it temporary storage and share it |  | ||||||
|      * @param bitmap bitmap of screenshot |  | ||||||
|      */ |  | ||||||
|     void shareScreen(final Bitmap bitmap) { |  | ||||||
|         try { |  | ||||||
|             final File file = new File(getExternalCacheDir(), "screen.png"); |  | ||||||
|             final FileOutputStream fileOutputStream = new FileOutputStream(file); |  | ||||||
|             bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream); |  | ||||||
|             fileOutputStream.flush(); |  | ||||||
|             fileOutputStream.close(); |  | ||||||
|             file.setReadable(true, false); |  | ||||||
| 
 |  | ||||||
|             final Uri fileUri = FileProvider |  | ||||||
|                 .getUriForFile(getApplicationContext(), |  | ||||||
|                     getPackageName() + ".provider", file); |  | ||||||
|             grantUriPermission(getPackageName(), fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); |  | ||||||
|             final Intent intent = new Intent(android.content.Intent.ACTION_SEND); |  | ||||||
|             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |  | ||||||
|             intent.putExtra(Intent.EXTRA_STREAM, fileUri); |  | ||||||
|             intent.setType("image/png"); |  | ||||||
|             startActivity(Intent.createChooser(intent, getString(R.string.share_image_via))); |  | ||||||
|         } catch (final IOException e) { |  | ||||||
|             e.printStackTrace(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     protected void onSaveInstanceState(@NonNull final Bundle outState) { |  | ||||||
|         outState.putString(KEY_USERNAME, userName); |  | ||||||
|         outState.putBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions); |  | ||||||
|         super.onSaveInstanceState(outState); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onBackPressed() { |  | ||||||
|         // Checking if MediaDetailPagerFragment is visible, If visible then show ContributionListFragment else close the ProfileActivity |  | ||||||
|         if(contributionsFragment != null && contributionsFragment.getMediaDetailPagerFragment() != null && contributionsFragment.getMediaDetailPagerFragment().isVisible()) { |  | ||||||
|             contributionsFragment.backButtonClicked(); |  | ||||||
|             binding.tabLayout.setVisibility(View.VISIBLE); |  | ||||||
|         }else { |  | ||||||
|             super.onBackPressed(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * To set the visibility of tab layout |  | ||||||
|      * @param isVisible boolean |  | ||||||
|      */ |  | ||||||
|     public void setTabLayoutVisibility(boolean isVisible) { |  | ||||||
|         binding.tabLayout.setVisibility(isVisible ? View.VISIBLE : View.GONE); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										229
									
								
								app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,229 @@ | ||||||
|  | package fr.free.nrw.commons.profile | ||||||
|  | 
 | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.graphics.Bitmap | ||||||
|  | import android.net.Uri | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.util.Log | ||||||
|  | import android.view.* | ||||||
|  | import android.widget.ImageView | ||||||
|  | import android.widget.TextView | ||||||
|  | import androidx.core.content.FileProvider | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import fr.free.nrw.commons.R | ||||||
|  | import fr.free.nrw.commons.Utils | ||||||
|  | import fr.free.nrw.commons.ViewPagerAdapter | ||||||
|  | import fr.free.nrw.commons.auth.SessionManager | ||||||
|  | import fr.free.nrw.commons.contributions.ContributionsFragment | ||||||
|  | import fr.free.nrw.commons.databinding.ActivityProfileBinding | ||||||
|  | import fr.free.nrw.commons.profile.achievements.AchievementsFragment | ||||||
|  | import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment | ||||||
|  | import fr.free.nrw.commons.theme.BaseActivity | ||||||
|  | import fr.free.nrw.commons.utils.DialogUtil | ||||||
|  | import java.io.File | ||||||
|  | import java.io.FileOutputStream | ||||||
|  | import java.util.* | ||||||
|  | import javax.inject.Inject | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This activity will set two tabs, achievements and | ||||||
|  |  * each tab will have their own fragments | ||||||
|  |  */ | ||||||
|  | class ProfileActivity : BaseActivity() { | ||||||
|  | 
 | ||||||
|  |     lateinit var binding: ActivityProfileBinding | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     lateinit var sessionManager: SessionManager | ||||||
|  | 
 | ||||||
|  |     private lateinit var viewPagerAdapter: ViewPagerAdapter | ||||||
|  |     private lateinit var achievementsFragment: AchievementsFragment | ||||||
|  |     private lateinit var leaderboardFragment: LeaderboardFragment | ||||||
|  |     private lateinit var userName: String | ||||||
|  |     private var shouldShowContributions: Boolean = false | ||||||
|  |     private var contributionsFragment: ContributionsFragment? = null | ||||||
|  | 
 | ||||||
|  |     fun setScroll(canScroll: Boolean) { | ||||||
|  |         binding.viewPager.setCanScroll(canScroll) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onRestoreInstanceState(savedInstanceState: Bundle) { | ||||||
|  |         super.onRestoreInstanceState(savedInstanceState) | ||||||
|  |         savedInstanceState.let { | ||||||
|  |             userName = it.getString(KEY_USERNAME, "") | ||||||
|  |             shouldShowContributions = it.getBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  | 
 | ||||||
|  |         binding = ActivityProfileBinding.inflate(layoutInflater) | ||||||
|  |         setContentView(binding.root) | ||||||
|  |         setSupportActionBar(binding.toolbarBinding.toolbar) | ||||||
|  | 
 | ||||||
|  |         binding.toolbarBinding.toolbar.setNavigationOnClickListener { | ||||||
|  |             onSupportNavigateUp() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         userName = intent.getStringExtra(KEY_USERNAME) ?: "" | ||||||
|  |         title = userName | ||||||
|  |         shouldShowContributions = intent.getBooleanExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, false) | ||||||
|  | 
 | ||||||
|  |         viewPagerAdapter = ViewPagerAdapter(supportFragmentManager) | ||||||
|  |         binding.viewPager.adapter = viewPagerAdapter | ||||||
|  |         binding.tabLayout.setupWithViewPager(binding.viewPager) | ||||||
|  |         setTabs() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onSupportNavigateUp(): Boolean { | ||||||
|  |         onBackPressed() | ||||||
|  |         return true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun setTabs() { | ||||||
|  |         val fragmentList = mutableListOf<Fragment>() | ||||||
|  |         val titleList = mutableListOf<String>() | ||||||
|  | 
 | ||||||
|  |         // Add Achievements tab | ||||||
|  |         achievementsFragment = AchievementsFragment().apply { | ||||||
|  |             arguments = Bundle().apply { | ||||||
|  |                 putString(KEY_USERNAME, userName) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         fragmentList.add(achievementsFragment) | ||||||
|  |         titleList.add(resources.getString(R.string.achievements_tab_title).uppercase()) | ||||||
|  | 
 | ||||||
|  |         // Add Leaderboard tab | ||||||
|  |         leaderboardFragment = LeaderboardFragment().apply { | ||||||
|  |             arguments = Bundle().apply { | ||||||
|  |                 putString(KEY_USERNAME, userName) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         fragmentList.add(leaderboardFragment) | ||||||
|  |         titleList.add(resources.getString(R.string.leaderboard_tab_title).uppercase(Locale.ROOT)) | ||||||
|  | 
 | ||||||
|  |         // Add Contributions tab | ||||||
|  |         contributionsFragment = ContributionsFragment().apply { | ||||||
|  |             arguments = Bundle().apply { | ||||||
|  |                 putString(KEY_USERNAME, userName) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         contributionsFragment?.let { | ||||||
|  |             fragmentList.add(it) | ||||||
|  |             titleList.add(getString(R.string.contributions_fragment).uppercase(Locale.ROOT)) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         viewPagerAdapter.setTabData(fragmentList, titleList) | ||||||
|  |         viewPagerAdapter.notifyDataSetChanged() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public override fun onDestroy() { | ||||||
|  |         super.onDestroy() | ||||||
|  |         compositeDisposable.clear() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onCreateOptionsMenu(menu: Menu): Boolean { | ||||||
|  |         menuInflater.inflate(R.menu.menu_about, menu) | ||||||
|  |         return super.onCreateOptionsMenu(menu) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||||
|  |         return when (item.itemId) { | ||||||
|  |             R.id.share_app_icon -> { | ||||||
|  |                 val rootView = window.decorView.findViewById<View>(android.R.id.content) | ||||||
|  |                 val screenShot = Utils.getScreenShot(rootView) | ||||||
|  |                 if (screenShot == null) { | ||||||
|  |                     Log.e("ERROR", "ScreenShot is null") | ||||||
|  |                     return false | ||||||
|  |                 } | ||||||
|  |                 showAlert(screenShot) | ||||||
|  |                 true | ||||||
|  |             } | ||||||
|  |             else -> super.onOptionsItemSelected(item) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun showAlert(screenshot: Bitmap) { | ||||||
|  |         val view = layoutInflater.inflate(R.layout.image_alert_layout, null) | ||||||
|  |         val screenShotImage = view.findViewById<ImageView>(R.id.alert_image) | ||||||
|  |         val shareMessage = view.findViewById<TextView>(R.id.alert_text) | ||||||
|  | 
 | ||||||
|  |         screenShotImage.setImageBitmap(screenshot) | ||||||
|  |         shareMessage.setText(R.string.achievements_share_message) | ||||||
|  | 
 | ||||||
|  |         DialogUtil.showAlertDialog( | ||||||
|  |             this, | ||||||
|  |             null, | ||||||
|  |             null, | ||||||
|  |             getString(R.string.about_translate_proceed), | ||||||
|  |             getString(R.string.cancel), | ||||||
|  |             { shareScreen(screenshot) }, | ||||||
|  |             {}, | ||||||
|  |             view | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun shareScreen(bitmap: Bitmap) { | ||||||
|  |         try { | ||||||
|  |             val file = File(externalCacheDir, "screen.png") | ||||||
|  |             FileOutputStream(file).use { out -> | ||||||
|  |                 bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) | ||||||
|  |                 out.flush() | ||||||
|  |             } | ||||||
|  |             file.setReadable(true, false) | ||||||
|  | 
 | ||||||
|  |             val fileUri = FileProvider.getUriForFile( | ||||||
|  |                 applicationContext, | ||||||
|  |                 "$packageName.provider", | ||||||
|  |                 file | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||||||
|  | 
 | ||||||
|  |             val intent = Intent(Intent.ACTION_SEND).apply { | ||||||
|  |                 flags = Intent.FLAG_ACTIVITY_NEW_TASK | ||||||
|  |                 putExtra(Intent.EXTRA_STREAM, fileUri) | ||||||
|  |                 type = "image/png" | ||||||
|  |             } | ||||||
|  |             startActivity(Intent.createChooser(intent, getString(R.string.share_image_via))) | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             e.printStackTrace() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onSaveInstanceState(outState: Bundle) { | ||||||
|  |         super.onSaveInstanceState(outState) | ||||||
|  |         outState.putString(KEY_USERNAME, userName) | ||||||
|  |         outState.putBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onBackPressed() { | ||||||
|  |         if (contributionsFragment?.mediaDetailPagerFragment?.isVisible == true) { | ||||||
|  |             contributionsFragment?.backButtonClicked() | ||||||
|  |             binding.tabLayout.visibility = View.VISIBLE | ||||||
|  |         } else { | ||||||
|  |             super.onBackPressed() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun setTabLayoutVisibility(isVisible: Boolean) { | ||||||
|  |         binding.tabLayout.visibility = if (isVisible) View.VISIBLE else View.GONE | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         const val KEY_USERNAME = "username" | ||||||
|  |         const val KEY_SHOULD_SHOW_CONTRIBUTIONS = "shouldShowContributions" | ||||||
|  | 
 | ||||||
|  |         @JvmStatic | ||||||
|  |         fun startYourself(context: Context, userName: String, shouldShowContributions: Boolean) { | ||||||
|  |             val intent = Intent(context, ProfileActivity::class.java).apply { | ||||||
|  |                 addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) | ||||||
|  |                 putExtra(KEY_USERNAME, userName) | ||||||
|  |                 putExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions) | ||||||
|  |             } | ||||||
|  |             context.startActivity(intent) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -58,7 +58,7 @@ class LeaderboardListAdapter : PagedListAdapter<LeaderboardList, ListViewHolder> | ||||||
|             if (view.context is ProfileActivity) { |             if (view.context is ProfileActivity) { | ||||||
|                 ((view.context) as Activity).finish() |                 ((view.context) as Activity).finish() | ||||||
|             } |             } | ||||||
|             ProfileActivity.startYourself(view.context, item.username, true) |             ProfileActivity.startYourself(view.context, item.username?:"", true) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -17,7 +17,7 @@ import java.util.HashMap | ||||||
| class RecentLanguagesAdapter constructor( | class RecentLanguagesAdapter constructor( | ||||||
|     context: Context, |     context: Context, | ||||||
|     var recentLanguages: List<Language>, |     var recentLanguages: List<Language>, | ||||||
|     private val selectedLanguages: HashMap<*, String>, |     private val selectedLanguages: MutableMap<Int, String>, | ||||||
| ) : ArrayAdapter<String?>(context, R.layout.row_item_languages_spinner) { | ) : ArrayAdapter<String?>(context, R.layout.row_item_languages_spinner) { | ||||||
|     /** |     /** | ||||||
|      * Selected language code in UploadMediaDetailAdapter |      * Selected language code in UploadMediaDetailAdapter | ||||||
|  |  | ||||||
|  | @ -46,7 +46,7 @@ class UploadRepository @Inject constructor( | ||||||
|      * |      * | ||||||
|      * @return |      * @return | ||||||
|      */ |      */ | ||||||
|     fun buildContributions(): Observable<Contribution>? { |     fun buildContributions(): Observable<Contribution> { | ||||||
|         return uploadModel.buildContributions() |         return uploadModel.buildContributions() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -69,7 +69,7 @@ class UploadRepository @Inject constructor( | ||||||
|      * @return |      * @return | ||||||
|      */ |      */ | ||||||
|     fun getUploads(): List<UploadItem> { |     fun getUploads(): List<UploadItem> { | ||||||
|         return uploadModel.getUploads() |         return uploadModel.uploads | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -177,7 +177,7 @@ class UploadRepository @Inject constructor( | ||||||
|         place: Place?, |         place: Place?, | ||||||
|         similarImageInterface: SimilarImageInterface?, |         similarImageInterface: SimilarImageInterface?, | ||||||
|         inAppPictureLocation: LatLng? |         inAppPictureLocation: LatLng? | ||||||
|     ): Observable<UploadItem>? { |     ): Observable<UploadItem> { | ||||||
|         return uploadModel.preProcessImage( |         return uploadModel.preProcessImage( | ||||||
|             uploadableFile, |             uploadableFile, | ||||||
|             place, |             place, | ||||||
|  | @ -193,7 +193,7 @@ class UploadRepository @Inject constructor( | ||||||
|      * @param location Location of the image |      * @param location Location of the image | ||||||
|      * @return Quality of UploadItem |      * @return Quality of UploadItem | ||||||
|      */ |      */ | ||||||
|     fun getImageQuality(uploadItem: UploadItem, location: LatLng?): Single<Int>? { |     fun getImageQuality(uploadItem: UploadItem, location: LatLng?): Single<Int> { | ||||||
|         return uploadModel.getImageQuality(uploadItem, location) |         return uploadModel.getImageQuality(uploadItem, location) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -213,7 +213,7 @@ class UploadRepository @Inject constructor( | ||||||
|      * @param uploadItem UploadItem whose caption is to be checked |      * @param uploadItem UploadItem whose caption is to be checked | ||||||
|      * @return Quality of caption of the UploadItem |      * @return Quality of caption of the UploadItem | ||||||
|      */ |      */ | ||||||
|     fun getCaptionQuality(uploadItem: UploadItem): Single<Int>? { |     fun getCaptionQuality(uploadItem: UploadItem): Single<Int> { | ||||||
|         return uploadModel.getCaptionQuality(uploadItem) |         return uploadModel.getCaptionQuality(uploadItem) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -275,7 +275,7 @@ class UploadRepository @Inject constructor( | ||||||
|      * @param selectedExistingDepictions existing depicts |      * @param selectedExistingDepictions existing depicts | ||||||
|      */ |      */ | ||||||
|     fun setSelectedExistingDepictions(selectedExistingDepictions: List<String>) { |     fun setSelectedExistingDepictions(selectedExistingDepictions: List<String>) { | ||||||
|         uploadModel.selectedExistingDepictions = selectedExistingDepictions |         uploadModel.selectedExistingDepictions = selectedExistingDepictions.toMutableList() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -52,7 +52,7 @@ class FileProcessor | ||||||
|          * Processes filePath coordinates, either from EXIF data or user location |          * Processes filePath coordinates, either from EXIF data or user location | ||||||
|          */ |          */ | ||||||
|         fun processFileCoordinates( |         fun processFileCoordinates( | ||||||
|             similarImageInterface: SimilarImageInterface, |             similarImageInterface: SimilarImageInterface?, | ||||||
|             filePath: String?, |             filePath: String?, | ||||||
|             inAppPictureLocation: LatLng?, |             inAppPictureLocation: LatLng?, | ||||||
|         ): ImageCoordinates { |         ): ImageCoordinates { | ||||||
|  | @ -146,7 +146,7 @@ class FileProcessor | ||||||
|          */ |          */ | ||||||
|         private fun findOtherImages( |         private fun findOtherImages( | ||||||
|             fileBeingProcessed: File, |             fileBeingProcessed: File, | ||||||
|             similarImageInterface: SimilarImageInterface, |             similarImageInterface: SimilarImageInterface?, | ||||||
|         ) { |         ) { | ||||||
|             val oneHundredAndTwentySeconds = 120 * 1000L |             val oneHundredAndTwentySeconds = 120 * 1000L | ||||||
|             // Time when the original image was created |             // Time when the original image was created | ||||||
|  | @ -161,7 +161,7 @@ class FileProcessor | ||||||
|                 .map { Pair(it, readImageCoordinates(it)) } |                 .map { Pair(it, readImageCoordinates(it)) } | ||||||
|                 .firstOrNull { it.second?.decimalCoords != null } |                 .firstOrNull { it.second?.decimalCoords != null } | ||||||
|                 ?.let { fileCoordinatesPair -> |                 ?.let { fileCoordinatesPair -> | ||||||
|                     similarImageInterface.showSimilarImageFragment( |                     similarImageInterface?.showSimilarImageFragment( | ||||||
|                         fileBeingProcessed.path, |                         fileBeingProcessed.path, | ||||||
|                         fileCoordinatesPair.first.absolutePath, |                         fileCoordinatesPair.first.absolutePath, | ||||||
|                         fileCoordinatesPair.second, |                         fileCoordinatesPair.second, | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ import java.util.Locale | ||||||
|  */ |  */ | ||||||
| class LanguagesAdapter constructor( | class LanguagesAdapter constructor( | ||||||
|     context: Context, |     context: Context, | ||||||
|     private val selectedLanguages: HashMap<*, String>, |     private val selectedLanguages: MutableMap<Int, String>, | ||||||
| ) : ArrayAdapter<String?>(context, R.layout.row_item_languages_spinner) { | ) : ArrayAdapter<String?>(context, R.layout.row_item_languages_spinner) { | ||||||
|     companion object { |     companion object { | ||||||
|         /** |         /** | ||||||
|  |  | ||||||
|  | @ -1,986 +0,0 @@ | ||||||
| package fr.free.nrw.commons.upload; |  | ||||||
| 
 |  | ||||||
| import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS; |  | ||||||
| import static fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction; |  | ||||||
| import static fr.free.nrw.commons.utils.PermissionUtils.getPERMISSIONS_STORAGE; |  | ||||||
| import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; |  | ||||||
| import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE; |  | ||||||
| import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY; |  | ||||||
| 
 |  | ||||||
| import android.Manifest; |  | ||||||
| import android.annotation.SuppressLint; |  | ||||||
| import android.app.ProgressDialog; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.content.pm.PackageManager; |  | ||||||
| import android.location.Location; |  | ||||||
| import android.location.LocationManager; |  | ||||||
| import android.os.Build; |  | ||||||
| import android.os.Build.VERSION; |  | ||||||
| import android.os.Build.VERSION_CODES; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.provider.Settings; |  | ||||||
| import android.util.DisplayMetrics; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.widget.CheckBox; |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.appcompat.app.AlertDialog; |  | ||||||
| import androidx.fragment.app.Fragment; |  | ||||||
| import androidx.fragment.app.FragmentManager; |  | ||||||
| import androidx.fragment.app.FragmentStatePagerAdapter; |  | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager; |  | ||||||
| import androidx.viewpager.widget.PagerAdapter; |  | ||||||
| import androidx.viewpager.widget.ViewPager; |  | ||||||
| import androidx.work.ExistingWorkPolicy; |  | ||||||
| import fr.free.nrw.commons.R; |  | ||||||
| import fr.free.nrw.commons.auth.LoginActivity; |  | ||||||
| import fr.free.nrw.commons.auth.SessionManager; |  | ||||||
| import fr.free.nrw.commons.contributions.ContributionController; |  | ||||||
| import fr.free.nrw.commons.databinding.ActivityUploadBinding; |  | ||||||
| import fr.free.nrw.commons.filepicker.Constants.RequestCodes; |  | ||||||
| import fr.free.nrw.commons.filepicker.UploadableFile; |  | ||||||
| import fr.free.nrw.commons.kvstore.BasicKvStore; |  | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; |  | ||||||
| import fr.free.nrw.commons.location.LatLng; |  | ||||||
| import fr.free.nrw.commons.location.LocationPermissionsHelper; |  | ||||||
| import fr.free.nrw.commons.location.LocationServiceManager; |  | ||||||
| import fr.free.nrw.commons.mwapi.UserClient; |  | ||||||
| import fr.free.nrw.commons.nearby.Place; |  | ||||||
| import fr.free.nrw.commons.settings.Prefs; |  | ||||||
| import fr.free.nrw.commons.theme.BaseActivity; |  | ||||||
| import fr.free.nrw.commons.upload.UploadBaseFragment.Callback; |  | ||||||
| import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment; |  | ||||||
| import fr.free.nrw.commons.upload.depicts.DepictsFragment; |  | ||||||
| import fr.free.nrw.commons.upload.license.MediaLicenseFragment; |  | ||||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment; |  | ||||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback; |  | ||||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter; |  | ||||||
| import fr.free.nrw.commons.upload.worker.WorkRequestHelper; |  | ||||||
| import fr.free.nrw.commons.utils.DialogUtil; |  | ||||||
| import fr.free.nrw.commons.utils.PermissionUtils; |  | ||||||
| import fr.free.nrw.commons.utils.ViewUtil; |  | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers; |  | ||||||
| import io.reactivex.disposables.CompositeDisposable; |  | ||||||
| import io.reactivex.schedulers.Schedulers; |  | ||||||
| import java.io.File; |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.Collections; |  | ||||||
| import java.util.HashMap; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Set; |  | ||||||
| import javax.inject.Inject; |  | ||||||
| import javax.inject.Named; |  | ||||||
| import timber.log.Timber; |  | ||||||
| 
 |  | ||||||
| public class UploadActivity extends BaseActivity implements |  | ||||||
|     UploadContract.View, UploadBaseFragment.Callback, ThumbnailsAdapter.OnThumbnailDeletedListener { |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     ContributionController contributionController; |  | ||||||
|     @Inject |  | ||||||
|     @Named("default_preferences") |  | ||||||
|     JsonKvStore directKvStore; |  | ||||||
|     @Inject |  | ||||||
|     UploadContract.UserActionListener presenter; |  | ||||||
|     @Inject |  | ||||||
|     SessionManager sessionManager; |  | ||||||
|     @Inject |  | ||||||
|     UserClient userClient; |  | ||||||
|     @Inject |  | ||||||
|     LocationServiceManager locationManager; |  | ||||||
| 
 |  | ||||||
|     private boolean isTitleExpanded = true; |  | ||||||
| 
 |  | ||||||
|     private CompositeDisposable compositeDisposable; |  | ||||||
|     private ProgressDialog progressDialog; |  | ||||||
|     private UploadImageAdapter uploadImagesAdapter; |  | ||||||
|     private List<UploadBaseFragment> fragments; |  | ||||||
|     private UploadCategoriesFragment uploadCategoriesFragment; |  | ||||||
|     private DepictsFragment depictsFragment; |  | ||||||
|     private MediaLicenseFragment mediaLicenseFragment; |  | ||||||
|     private ThumbnailsAdapter thumbnailsAdapter; |  | ||||||
|     BasicKvStore store; |  | ||||||
|     private Place place; |  | ||||||
|     private LatLng prevLocation; |  | ||||||
|     private LatLng currLocation; |  | ||||||
|     private static boolean uploadIsOfAPlace = false; |  | ||||||
|     private boolean isInAppCameraUpload; |  | ||||||
|     private List<UploadableFile> uploadableFiles = Collections.emptyList(); |  | ||||||
|     private int currentSelectedPosition = 0; |  | ||||||
|     /* |  | ||||||
|      Checks for if multiple files selected |  | ||||||
|      */ |  | ||||||
|     private boolean isMultipleFilesSelected = false; |  | ||||||
| 
 |  | ||||||
|     public static final String EXTRA_FILES = "commons_image_exta"; |  | ||||||
|     public static final String LOCATION_BEFORE_IMAGE_CAPTURE = "user_location_before_image_capture"; |  | ||||||
|     public static final String IN_APP_CAMERA_UPLOAD = "in_app_camera_upload"; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Stores all nearby places found and related users response for |  | ||||||
|      * each place while uploading media |  | ||||||
|      */ |  | ||||||
|     public static HashMap<Place,Boolean> nearbyPopupAnswers; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * A private boolean variable to control whether a permissions dialog should be shown |  | ||||||
|      * when necessary. Initially, it is set to `true`, indicating that the permissions dialog |  | ||||||
|      * should be displayed if permissions are missing and it is first time calling |  | ||||||
|      * `checkStoragePermissions` method. |  | ||||||
|      * This variable is used in the `checkStoragePermissions` method to determine whether to |  | ||||||
|      * show a permissions dialog to the user if the required permissions are not granted. |  | ||||||
|      * If `showPermissionsDialog` is set to `true` and the necessary permissions are missing, |  | ||||||
|      * a permissions dialog will be displayed to request the required permissions. If set |  | ||||||
|      * to `false`, the dialog won't be shown. |  | ||||||
|      * |  | ||||||
|      * @see UploadActivity#checkStoragePermissions() |  | ||||||
|      */ |  | ||||||
|     private boolean showPermissionsDialog = true; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Whether fragments have been saved. |  | ||||||
|      */ |  | ||||||
|     private boolean isFragmentsSaved = false; |  | ||||||
|      |  | ||||||
|     public static final String keyForCurrentUploadImagesSize = "CurrentUploadImagesSize"; |  | ||||||
|     public static final String storeNameForCurrentUploadImagesSize = "CurrentUploadImageQualities"; |  | ||||||
| 
 |  | ||||||
|     private ActivityUploadBinding binding; |  | ||||||
| 
 |  | ||||||
|     @SuppressLint("CheckResult") |  | ||||||
|     @Override |  | ||||||
|     protected void onCreate(final Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
| 
 |  | ||||||
|         binding = ActivityUploadBinding.inflate(getLayoutInflater()); |  | ||||||
|         setContentView(binding.getRoot()); |  | ||||||
| 
 |  | ||||||
|         /* |  | ||||||
|          If Configuration of device is changed then get the new fragments |  | ||||||
|          created by the system and populate the fragments ArrayList |  | ||||||
|          */ |  | ||||||
|         if (savedInstanceState != null) { |  | ||||||
|             isFragmentsSaved = true; |  | ||||||
|             final List<Fragment> fragmentList = getSupportFragmentManager().getFragments(); |  | ||||||
|             fragments = new ArrayList<>(); |  | ||||||
|             for (final Fragment fragment : fragmentList) { |  | ||||||
|                 fragments.add((UploadBaseFragment) fragment); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         compositeDisposable = new CompositeDisposable(); |  | ||||||
|         init(); |  | ||||||
|         binding.rlContainerTitle.setOnClickListener(v -> onRlContainerTitleClicked()); |  | ||||||
|         nearbyPopupAnswers = new HashMap<>(); |  | ||||||
|         //getting the current dpi of the device and if it is less than 320dp i.e. overlapping |  | ||||||
|         //threshold, thumbnails automatically minimizes |  | ||||||
|         final DisplayMetrics metrics = getResources().getDisplayMetrics(); |  | ||||||
|         final float dpi = (metrics.widthPixels)/(metrics.density); |  | ||||||
|         if (dpi<=321) { |  | ||||||
|             onRlContainerTitleClicked(); |  | ||||||
|         } |  | ||||||
|         if (PermissionUtils.hasPermission(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) { |  | ||||||
|             locationManager.registerLocationManager(); |  | ||||||
|         } |  | ||||||
|         locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); |  | ||||||
|         locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER); |  | ||||||
|         store = new BasicKvStore(this, storeNameForCurrentUploadImagesSize); |  | ||||||
|         store.clearAll(); |  | ||||||
|         checkStoragePermissions(); |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void init() { |  | ||||||
|         initProgressDialog(); |  | ||||||
|         initViewPager(); |  | ||||||
|         initThumbnailsRecyclerView(); |  | ||||||
|         //And init other things you need to |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void initProgressDialog() { |  | ||||||
|         progressDialog = new ProgressDialog(this); |  | ||||||
|         progressDialog.setMessage(getString(R.string.please_wait)); |  | ||||||
|         progressDialog.setCancelable(false); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void initThumbnailsRecyclerView() { |  | ||||||
|         binding.rvThumbnails.setLayoutManager(new LinearLayoutManager(this, |  | ||||||
|             LinearLayoutManager.HORIZONTAL, false)); |  | ||||||
|         thumbnailsAdapter = new ThumbnailsAdapter(() -> currentSelectedPosition); |  | ||||||
|         thumbnailsAdapter.setOnThumbnailDeletedListener(this); |  | ||||||
|         binding.rvThumbnails.setAdapter(thumbnailsAdapter); |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void initViewPager() { |  | ||||||
|         uploadImagesAdapter = new UploadImageAdapter(getSupportFragmentManager()); |  | ||||||
|         binding.vpUpload.setAdapter(uploadImagesAdapter); |  | ||||||
|         binding.vpUpload.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onPageScrolled(final int position, final float positionOffset, |  | ||||||
|                 final int positionOffsetPixels) { |  | ||||||
| 
 |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             @Override |  | ||||||
|             public void onPageSelected(final int position) { |  | ||||||
|                 currentSelectedPosition = position; |  | ||||||
|                 if (position >= uploadableFiles.size()) { |  | ||||||
|                     binding.cvContainerTopCard.setVisibility(View.GONE); |  | ||||||
|                 } else { |  | ||||||
|                     thumbnailsAdapter.notifyDataSetChanged(); |  | ||||||
|                     binding.cvContainerTopCard.setVisibility(View.VISIBLE); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             @Override |  | ||||||
|             public void onPageScrollStateChanged(final int state) { |  | ||||||
| 
 |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public boolean isLoggedIn() { |  | ||||||
|         return sessionManager.isUserLoggedIn(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     protected void onResume() { |  | ||||||
|         super.onResume(); |  | ||||||
|         presenter.onAttachView(this); |  | ||||||
|         if (!isLoggedIn()) { |  | ||||||
|             askUserToLogIn(); |  | ||||||
|         } |  | ||||||
|         checkBlockStatus(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Makes API call to check if user is blocked from Commons. If the user is blocked, a snackbar |  | ||||||
|      * is created to notify the user |  | ||||||
|      */ |  | ||||||
|     protected void checkBlockStatus() { |  | ||||||
|         compositeDisposable.add(userClient.isUserBlockedFromCommons() |  | ||||||
|             .subscribeOn(Schedulers.io()) |  | ||||||
|             .observeOn(AndroidSchedulers.mainThread()) |  | ||||||
|             .filter(result -> result) |  | ||||||
|             .subscribe(result -> DialogUtil.showAlertDialog( |  | ||||||
|                 this, |  | ||||||
|                 getString(R.string.block_notification_title), |  | ||||||
|                 getString(R.string.block_notification), |  | ||||||
|                 getString(R.string.ok), |  | ||||||
|                 this::finish))); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void checkStoragePermissions() { |  | ||||||
|         // Check if all required permissions are granted |  | ||||||
|         final boolean hasAllPermissions = PermissionUtils.hasPermission(this, getPERMISSIONS_STORAGE()); |  | ||||||
|         final boolean hasPartialAccess = PermissionUtils.hasPartialAccess(this); |  | ||||||
|         if (hasAllPermissions || hasPartialAccess) { |  | ||||||
|             // All required permissions are granted, so enable UI elements and perform actions |  | ||||||
|             receiveSharedItems(); |  | ||||||
|             binding.cvContainerTopCard.setVisibility(View.VISIBLE); |  | ||||||
|         } else { |  | ||||||
|             // Permissions are missing |  | ||||||
|             binding.cvContainerTopCard.setVisibility(View.INVISIBLE); |  | ||||||
|             if(showPermissionsDialog){ |  | ||||||
|                 checkPermissionsAndPerformAction(this, |  | ||||||
|                     () -> { |  | ||||||
|                         binding.cvContainerTopCard.setVisibility(View.VISIBLE); |  | ||||||
|                         this.receiveSharedItems(); |  | ||||||
|                     },() -> { |  | ||||||
|                         this.showPermissionsDialog = true; |  | ||||||
|                         this.checkStoragePermissions(); |  | ||||||
|                         }, |  | ||||||
|                     R.string.storage_permission_title, |  | ||||||
|                     R.string.write_storage_permission_rationale_for_image_share, |  | ||||||
|                     getPERMISSIONS_STORAGE()); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         /* If all permissions are not granted and a dialog is already showing on screen |  | ||||||
|          showPermissionsDialog will set to false making it not show dialog again onResume, |  | ||||||
|          but if user Denies any permission showPermissionsDialog will be to true |  | ||||||
|          and permissions dialog will be shown again. |  | ||||||
|          */ |  | ||||||
|         this.showPermissionsDialog = hasAllPermissions ; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     protected void onStop() { |  | ||||||
|         // Resetting setImageCancelled to false |  | ||||||
|         setImageCancelled(false); |  | ||||||
|         super.onStop(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void returnToMainActivity() { |  | ||||||
|         finish(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * go to the uploadProgress activity to check the status of uploading |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void goToUploadProgressActivity() { |  | ||||||
|         startActivity(new Intent(this, UploadProgressActivity.class)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Show/Hide the progress dialog |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void showProgress(final boolean shouldShow) { |  | ||||||
|         if (shouldShow) { |  | ||||||
|             if (!progressDialog.isShowing()) { |  | ||||||
|                 progressDialog.show(); |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             if (progressDialog != null && !isFinishing()) { |  | ||||||
|                 progressDialog.dismiss(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public int getIndexInViewFlipper(final UploadBaseFragment fragment) { |  | ||||||
|         return fragments.indexOf(fragment); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public int getTotalNumberOfSteps() { |  | ||||||
|         return fragments.size(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public boolean isWLMUpload() { |  | ||||||
|         return place!=null && place.isMonument(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void showMessage(final int messageResourceId) { |  | ||||||
|         ViewUtil.showLongToast(this, messageResourceId); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public List<UploadableFile> getUploadableFiles() { |  | ||||||
|         return uploadableFiles; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void showHideTopCard(final boolean shouldShow) { |  | ||||||
|         binding.llContainerTopCard.setVisibility(shouldShow ? View.VISIBLE : View.GONE); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onUploadMediaDeleted(final int index) { |  | ||||||
|         fragments.remove(index);//Remove the corresponding fragment |  | ||||||
|         uploadableFiles.remove(index);//Remove the files from the list |  | ||||||
|         thumbnailsAdapter.notifyItemRemoved(index); //Notify the thumbnails adapter |  | ||||||
|         uploadImagesAdapter.notifyDataSetChanged(); //Notify the ViewPager |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void updateTopCardTitle() { |  | ||||||
|         binding.tvTopCardTitle.setText(getResources() |  | ||||||
|             .getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size())); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void makeUploadRequest() { |  | ||||||
|         WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(), |  | ||||||
|             ExistingWorkPolicy.APPEND_OR_REPLACE); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void askUserToLogIn() { |  | ||||||
|         Timber.d("current session is null, asking user to login"); |  | ||||||
|         ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in)); |  | ||||||
|         final Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class); |  | ||||||
|         startActivity(loginIntent); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onRequestPermissionsResult(final int requestCode, |  | ||||||
|         @NonNull final String[] permissions, |  | ||||||
|         @NonNull final int[] grantResults) { |  | ||||||
|         boolean areAllGranted = false; |  | ||||||
|         if (requestCode == RequestCodes.STORAGE) { |  | ||||||
|             if (VERSION.SDK_INT >= VERSION_CODES.M) { |  | ||||||
|                 for (int i = 0; i < grantResults.length; i++) { |  | ||||||
|                     final String permission = permissions[i]; |  | ||||||
|                     areAllGranted = grantResults[i] == PackageManager.PERMISSION_GRANTED; |  | ||||||
|                     if (grantResults[i] == PackageManager.PERMISSION_DENIED) { |  | ||||||
|                         final boolean showRationale = shouldShowRequestPermissionRationale(permission); |  | ||||||
|                         if (!showRationale) { |  | ||||||
|                             DialogUtil.showAlertDialog(this, |  | ||||||
|                                 getString(R.string.storage_permissions_denied), |  | ||||||
|                                 getString(R.string.unable_to_share_upload_item), |  | ||||||
|                                 getString(android.R.string.ok), |  | ||||||
|                                 this::finish); |  | ||||||
|                         } else { |  | ||||||
|                             DialogUtil.showAlertDialog(this, |  | ||||||
|                                 getString(R.string.storage_permission_title), |  | ||||||
|                                 getString( |  | ||||||
|                                     R.string.write_storage_permission_rationale_for_image_share), |  | ||||||
|                                 getString(android.R.string.ok), |  | ||||||
|                                 this::checkStoragePermissions); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (areAllGranted) { |  | ||||||
|                     receiveSharedItems(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         super.onRequestPermissionsResult(requestCode, permissions, grantResults); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Sets the flag indicating whether the upload is of a specific place. |  | ||||||
|      * |  | ||||||
|      * @param uploadOfAPlace a boolean value indicating whether the upload is of place. |  | ||||||
|      */ |  | ||||||
|     public static void setUploadIsOfAPlace(final boolean uploadOfAPlace) { |  | ||||||
|         uploadIsOfAPlace = uploadOfAPlace; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void receiveSharedItems() { |  | ||||||
|         final Intent intent = getIntent(); |  | ||||||
|         final String action = intent.getAction(); |  | ||||||
|         if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) { |  | ||||||
|             receiveExternalSharedItems(); |  | ||||||
|         } else if (ACTION_INTERNAL_UPLOADS.equals(action)) { |  | ||||||
|             receiveInternalSharedItems(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (uploadableFiles == null || uploadableFiles.isEmpty()) { |  | ||||||
|             handleNullMedia(); |  | ||||||
|         } else { |  | ||||||
|             //Show thumbnails |  | ||||||
|             if (uploadableFiles.size() > 1){ |  | ||||||
|                 if(!defaultKvStore.getBoolean("hasAlreadyLaunchedCategoriesDialog")){//If there is only file, no need to show the image thumbnails |  | ||||||
|                     showAlertDialogForCategories(); |  | ||||||
|                 } |  | ||||||
|                 if (uploadableFiles.size() > 3 && |  | ||||||
|                     !defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload")){ |  | ||||||
|                     showAlertForBattery(); |  | ||||||
|                 } |  | ||||||
|                 thumbnailsAdapter.setUploadableFiles(uploadableFiles); |  | ||||||
|             } else { |  | ||||||
|                 binding.llContainerTopCard.setVisibility(View.GONE); |  | ||||||
|             } |  | ||||||
|             binding.tvTopCardTitle.setText(getResources() |  | ||||||
|                 .getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size())); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             if(fragments == null){ |  | ||||||
|                 fragments = new ArrayList<>(); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             for (final UploadableFile uploadableFile : uploadableFiles) { |  | ||||||
|                 final UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment(); |  | ||||||
| 
 |  | ||||||
|                 if (!uploadIsOfAPlace) { |  | ||||||
|                     handleLocation(); |  | ||||||
|                     uploadMediaDetailFragment.setImageToBeUploaded(uploadableFile, place, currLocation); |  | ||||||
|                     locationManager.unregisterLocationManager(); |  | ||||||
|                 } else { |  | ||||||
|                     uploadMediaDetailFragment.setImageToBeUploaded(uploadableFile, place, currLocation); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 final UploadMediaDetailFragmentCallback uploadMediaDetailFragmentCallback = new UploadMediaDetailFragmentCallback() { |  | ||||||
|                     @Override |  | ||||||
|                     public void deletePictureAtIndex(final int index) { |  | ||||||
|                         store.putInt(keyForCurrentUploadImagesSize, |  | ||||||
|                             (store.getInt(keyForCurrentUploadImagesSize) - 1)); |  | ||||||
|                         presenter.deletePictureAtIndex(index); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     /** |  | ||||||
|                      * Changes the thumbnail of an UploadableFile at the specified index. |  | ||||||
|                      * This method updates the list of uploadableFiles by replacing the UploadableFile |  | ||||||
|                      * at the given index with a new UploadableFile created from the provided file path. |  | ||||||
|                      * After updating the list, it notifies the RecyclerView's adapter to refresh its data, |  | ||||||
|                      * ensuring that the thumbnail change is reflected in the UI. |  | ||||||
|                      * |  | ||||||
|                      * @param index The index of the UploadableFile to be updated. |  | ||||||
|                      * @param filepath The file path of the new thumbnail image. |  | ||||||
|                      */ |  | ||||||
|                     @Override |  | ||||||
|                     public void changeThumbnail(final int index, final String filepath) { |  | ||||||
|                         uploadableFiles.remove(index); |  | ||||||
|                         uploadableFiles.add(index, new UploadableFile(new File(filepath))); |  | ||||||
|                         binding.rvThumbnails.getAdapter().notifyDataSetChanged(); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     @Override |  | ||||||
|                     public void onNextButtonClicked(final int index) { |  | ||||||
|                         UploadActivity.this.onNextButtonClicked(index); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     @Override |  | ||||||
|                     public void onPreviousButtonClicked(final int index) { |  | ||||||
|                         UploadActivity.this.onPreviousButtonClicked(index); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     @Override |  | ||||||
|                     public void showProgress(final boolean shouldShow) { |  | ||||||
|                         UploadActivity.this.showProgress(shouldShow); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     @Override |  | ||||||
|                     public int getIndexInViewFlipper(final UploadBaseFragment fragment) { |  | ||||||
|                         return fragments.indexOf(fragment); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     @Override |  | ||||||
|                     public int getTotalNumberOfSteps() { |  | ||||||
|                         return fragments.size(); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     @Override |  | ||||||
|                     public boolean isWLMUpload() { |  | ||||||
|                         return place!=null && place.isMonument(); |  | ||||||
|                     } |  | ||||||
|                 }; |  | ||||||
| 
 |  | ||||||
|                 if(isFragmentsSaved){ |  | ||||||
|                     final UploadMediaDetailFragment fragment = (UploadMediaDetailFragment) fragments.get(0); |  | ||||||
|                     fragment.setCallback(uploadMediaDetailFragmentCallback); |  | ||||||
|                 }else{ |  | ||||||
|                     uploadMediaDetailFragment.setCallback(uploadMediaDetailFragmentCallback); |  | ||||||
|                     fragments.add(uploadMediaDetailFragment); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             //If fragments are not created, create them and add them to the fragments ArrayList |  | ||||||
|             if(!isFragmentsSaved){ |  | ||||||
|                 uploadCategoriesFragment = new UploadCategoriesFragment(); |  | ||||||
|                 if (place != null) { |  | ||||||
|                     final Bundle categoryBundle = new Bundle(); |  | ||||||
|                     categoryBundle.putString(SELECTED_NEARBY_PLACE_CATEGORY, place.getCategory()); |  | ||||||
|                     uploadCategoriesFragment.setArguments(categoryBundle); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 uploadCategoriesFragment.setCallback(this); |  | ||||||
| 
 |  | ||||||
|                 depictsFragment = new DepictsFragment(); |  | ||||||
|                 final Bundle placeBundle = new Bundle(); |  | ||||||
|                 placeBundle.putParcelable(SELECTED_NEARBY_PLACE, place); |  | ||||||
|                 depictsFragment.setArguments(placeBundle); |  | ||||||
|                 depictsFragment.setCallback(this); |  | ||||||
| 
 |  | ||||||
|                 mediaLicenseFragment = new MediaLicenseFragment(); |  | ||||||
|                 mediaLicenseFragment.setCallback(this); |  | ||||||
| 
 |  | ||||||
|                 fragments.add(depictsFragment); |  | ||||||
|                 fragments.add(uploadCategoriesFragment); |  | ||||||
|                 fragments.add(mediaLicenseFragment); |  | ||||||
| 
 |  | ||||||
|             }else{ |  | ||||||
|                 for(int i=1;i<fragments.size();i++){ |  | ||||||
|                     fragments.get(i).setCallback(new Callback() { |  | ||||||
|                         @Override |  | ||||||
|                         public void onNextButtonClicked(final int index) { |  | ||||||
|                             if (index < fragments.size() - 1) { |  | ||||||
|                                 binding.vpUpload.setCurrentItem(index + 1, false); |  | ||||||
|                                 fragments.get(index + 1).onBecameVisible(); |  | ||||||
|                                 ((LinearLayoutManager) binding.rvThumbnails.getLayoutManager()) |  | ||||||
|                                     .scrollToPositionWithOffset((index > 0) ? index-1 : 0, 0); |  | ||||||
|                             } else { |  | ||||||
|                                 presenter.handleSubmit(); |  | ||||||
|                             } |  | ||||||
| 
 |  | ||||||
|                         } |  | ||||||
|                         @Override |  | ||||||
|                         public void onPreviousButtonClicked(final int index) { |  | ||||||
|                             if (index != 0) { |  | ||||||
|                                 binding.vpUpload.setCurrentItem(index - 1, true); |  | ||||||
|                                 fragments.get(index - 1).onBecameVisible(); |  | ||||||
|                                 ((LinearLayoutManager) binding.rvThumbnails.getLayoutManager()) |  | ||||||
|                                     .scrollToPositionWithOffset((index > 3) ? index-2 : 0, 0); |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                         @Override |  | ||||||
|                         public void showProgress(final boolean shouldShow) { |  | ||||||
|                             if (shouldShow) { |  | ||||||
|                                 if (!progressDialog.isShowing()) { |  | ||||||
|                                     progressDialog.show(); |  | ||||||
|                                 } |  | ||||||
|                             } else { |  | ||||||
|                                 if (progressDialog != null && !isFinishing()) { |  | ||||||
|                                     progressDialog.dismiss(); |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                         @Override |  | ||||||
|                         public int getIndexInViewFlipper(final UploadBaseFragment fragment) { |  | ||||||
|                             return fragments.indexOf(fragment); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         @Override |  | ||||||
|                         public int getTotalNumberOfSteps() { |  | ||||||
|                             return fragments.size(); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         @Override |  | ||||||
|                         public boolean isWLMUpload() { |  | ||||||
|                             return place!=null && place.isMonument(); |  | ||||||
|                         } |  | ||||||
|                     }); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             uploadImagesAdapter.setFragments(fragments); |  | ||||||
|             binding.vpUpload.setOffscreenPageLimit(fragments.size()); |  | ||||||
| 
 |  | ||||||
|         } |  | ||||||
|         // Saving size of uploadableFiles |  | ||||||
|         store.putInt(keyForCurrentUploadImagesSize, uploadableFiles.size()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Users may uncheck Location tag from the Manage EXIF tags setting any time. |  | ||||||
|      * So, their location must not be shared in this case. |  | ||||||
|      * |  | ||||||
|      */ |  | ||||||
|     private boolean isLocationTagUncheckedInTheSettings() { |  | ||||||
|         final Set<String> prefExifTags = defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS); |  | ||||||
|         if (prefExifTags.contains(getString(R.string.exif_tag_location))) { |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Changes current image when one image upload is cancelled, to highlight next image in the top thumbnail. |  | ||||||
|      * Fixes: <a href="https://github.com/commons-app/apps-android-commons/issues/5511">Issue</a> |  | ||||||
|      * |  | ||||||
|      * @param index Index of image to be removed |  | ||||||
|      * @param maxSize Max size of the {@code uploadableFiles} |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void highlightNextImageOnCancelledImage(final int index, final int maxSize) { |  | ||||||
|         if (binding.vpUpload != null && index < (maxSize)) { |  | ||||||
|             binding.vpUpload.setCurrentItem(index + 1, false); |  | ||||||
|             binding.vpUpload.setCurrentItem(index, false); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Used to check if user has cancelled upload of any image in current upload |  | ||||||
|      * so that location compare doesn't show up again in same upload. |  | ||||||
|      * Fixes: <a href="https://github.com/commons-app/apps-android-commons/issues/5511">Issue</a> |  | ||||||
|      * |  | ||||||
|      * @param isCancelled Is true when user has cancelled upload of any image in current upload |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void setImageCancelled(final boolean isCancelled) { |  | ||||||
|         final BasicKvStore basicKvStore = new BasicKvStore(this,"IsAnyImageCancelled"); |  | ||||||
|         basicKvStore.putBoolean("IsAnyImageCancelled", isCancelled); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Calculate the difference between current location and |  | ||||||
|      * location recorded before capturing the image |  | ||||||
|      * |  | ||||||
|      */ |  | ||||||
|     private float getLocationDifference(final LatLng currLocation, final LatLng prevLocation) { |  | ||||||
|         if (prevLocation == null) { |  | ||||||
|             return 0.0f; |  | ||||||
|         } |  | ||||||
|         final float[] distance = new float[2]; |  | ||||||
|         Location.distanceBetween( |  | ||||||
|             currLocation.getLatitude(), currLocation.getLongitude(), |  | ||||||
|             prevLocation.getLatitude(), prevLocation.getLongitude(), distance); |  | ||||||
|         return distance[0]; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void receiveExternalSharedItems() { |  | ||||||
|         uploadableFiles = contributionController.handleExternalImagesPicked(this, getIntent()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void receiveInternalSharedItems() { |  | ||||||
|         final Intent intent = getIntent(); |  | ||||||
| 
 |  | ||||||
|         Timber.d("Received intent %s with action %s", intent.toString(), intent.getAction()); |  | ||||||
| 
 |  | ||||||
|         uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES); |  | ||||||
|         isMultipleFilesSelected = uploadableFiles.size() > 1; |  | ||||||
|         Timber.i("Received multiple upload %s", uploadableFiles.size()); |  | ||||||
| 
 |  | ||||||
|         place = intent.getParcelableExtra(PLACE_OBJECT); |  | ||||||
|         prevLocation = intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE); |  | ||||||
|         isInAppCameraUpload = intent.getBooleanExtra(IN_APP_CAMERA_UPLOAD, false); |  | ||||||
|         resetDirectPrefs(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Returns if multiple files selected or not. |  | ||||||
|      */ |  | ||||||
|     public boolean getIsMultipleFilesSelected() { |  | ||||||
|         return isMultipleFilesSelected; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void resetDirectPrefs() { |  | ||||||
|         directKvStore.remove(PLACE_OBJECT); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Handle null URI from the received intent. |  | ||||||
|      * Current implementation will simply show a toast and finish the upload activity. |  | ||||||
|      */ |  | ||||||
|     private void handleNullMedia() { |  | ||||||
|         ViewUtil.showLongToast(this, R.string.error_processing_image); |  | ||||||
|         finish(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void showAlertDialog(final int messageResourceId, @NonNull final Runnable onPositiveClick) { |  | ||||||
|         DialogUtil.showAlertDialog(this, |  | ||||||
|             "", |  | ||||||
|             getString(messageResourceId), |  | ||||||
|             getString(R.string.ok), |  | ||||||
|             onPositiveClick); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onNextButtonClicked(final int index) { |  | ||||||
|         if (index < fragments.size() - 1) { |  | ||||||
|             binding.vpUpload.setCurrentItem(index + 1, false); |  | ||||||
|             fragments.get(index + 1).onBecameVisible(); |  | ||||||
|             ((LinearLayoutManager) binding.rvThumbnails.getLayoutManager()) |  | ||||||
|                 .scrollToPositionWithOffset((index > 0) ? index - 1 : 0, 0); |  | ||||||
|             if (index < fragments.size() - 4) { |  | ||||||
|                 // check image quality if next image exists |  | ||||||
|                 presenter.checkImageQuality(index + 1); |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             presenter.handleSubmit(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onPreviousButtonClicked(final int index) { |  | ||||||
|         if (index != 0) { |  | ||||||
|             binding.vpUpload.setCurrentItem(index - 1, true); |  | ||||||
|             fragments.get(index - 1).onBecameVisible(); |  | ||||||
|             ((LinearLayoutManager) binding.rvThumbnails.getLayoutManager()) |  | ||||||
|                 .scrollToPositionWithOffset((index > 3) ? index-2 : 0, 0); |  | ||||||
|             if ((index != 1) && ((index - 1) < uploadableFiles.size())) { |  | ||||||
|                 // Shows the top card if it was hidden because of the last image being deleted and |  | ||||||
|                 // now the user has hit previous button to go back to the media details |  | ||||||
|                 showHideTopCard(true); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onThumbnailDeleted(final int position) { |  | ||||||
|         presenter.deletePictureAtIndex(position); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * The adapter used to show image upload intermediate fragments |  | ||||||
|      */ |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     private static class UploadImageAdapter extends FragmentStatePagerAdapter { |  | ||||||
|         List<UploadBaseFragment> fragments; |  | ||||||
| 
 |  | ||||||
|         public UploadImageAdapter(final FragmentManager fragmentManager) { |  | ||||||
|             super(fragmentManager); |  | ||||||
|             this.fragments = new ArrayList<>(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         public void setFragments(final List<UploadBaseFragment> fragments) { |  | ||||||
|             this.fragments = fragments; |  | ||||||
|             notifyDataSetChanged(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @NonNull |  | ||||||
|         @Override |  | ||||||
|         public Fragment getItem(final int position) { |  | ||||||
|             return fragments.get(position); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public int getCount() { |  | ||||||
|             return fragments.size(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public int getItemPosition(@NonNull final Object item) { |  | ||||||
|             return PagerAdapter.POSITION_NONE; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     public void onRlContainerTitleClicked() { |  | ||||||
|         binding.rvThumbnails.setVisibility(isTitleExpanded ? View.GONE : View.VISIBLE); |  | ||||||
|         isTitleExpanded = !isTitleExpanded; |  | ||||||
|         binding.ibToggleTopCard.setRotation(binding.ibToggleTopCard.getRotation() + 180); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     protected void onDestroy() { |  | ||||||
|         super.onDestroy(); |  | ||||||
|         // Resetting all values in store by clearing them |  | ||||||
|         store.clearAll(); |  | ||||||
|         presenter.onDetachView(); |  | ||||||
|         compositeDisposable.clear(); |  | ||||||
|         fragments = null; |  | ||||||
|         uploadImagesAdapter = null; |  | ||||||
|         if (mediaLicenseFragment != null) { |  | ||||||
|             mediaLicenseFragment.setCallback(null); |  | ||||||
|         } |  | ||||||
|         if (uploadCategoriesFragment != null) { |  | ||||||
|             uploadCategoriesFragment.setCallback(null); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get the value of the showPermissionDialog variable. |  | ||||||
|      * |  | ||||||
|      * @return {@code true} if Permission Dialog should be shown, {@code false} otherwise. |  | ||||||
|      */ |  | ||||||
|     public boolean isShowPermissionsDialog() { |  | ||||||
|         return showPermissionsDialog; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Set the value of the showPermissionDialog variable. |  | ||||||
|      * |  | ||||||
|      * @param showPermissionsDialog {@code true} to indicate to show |  | ||||||
|      * Permissions Dialog if permissions are missing, {@code false} otherwise. |  | ||||||
|      */ |  | ||||||
|     public void setShowPermissionsDialog(final boolean showPermissionsDialog) { |  | ||||||
|         this.showPermissionsDialog = showPermissionsDialog; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Overrides the back button to make sure the user is prepared to lose their progress |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void onBackPressed() { |  | ||||||
|         DialogUtil.showAlertDialog(this, |  | ||||||
|             getString(R.string.back_button_warning), |  | ||||||
|             getString(R.string.back_button_warning_desc), |  | ||||||
|             getString(R.string.back_button_continue), |  | ||||||
|             getString(R.string.back_button_warning), |  | ||||||
|             null, |  | ||||||
|             this::finish |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * If the user uploads more than 1 file informs that |  | ||||||
|      * depictions/categories apply to all pictures of a multi upload. |  | ||||||
|      * This method takes no arguments and does not return any value. |  | ||||||
|      * It shows the AlertDialog and continues the flow of uploads. |  | ||||||
|      */ |  | ||||||
|     private void showAlertDialogForCategories() { |  | ||||||
|         UploadMediaPresenter.isCategoriesDialogShowing = true; |  | ||||||
|         // Inflate the custom layout |  | ||||||
|         final LayoutInflater inflater = getLayoutInflater(); |  | ||||||
|         final View view = inflater.inflate(R.layout.activity_upload_categories_dialog, null); |  | ||||||
|         final CheckBox checkBox = view.findViewById(R.id.categories_checkbox); |  | ||||||
|         // Create the alert dialog |  | ||||||
|         final AlertDialog alertDialog = new AlertDialog.Builder(this) |  | ||||||
|             .setView(view) |  | ||||||
|             .setTitle(getString(R.string.multiple_files_depiction_header)) |  | ||||||
|             .setMessage(getString(R.string.multiple_files_depiction)) |  | ||||||
|             .setCancelable(false) |  | ||||||
|             .setPositiveButton("OK", (dialog, which) -> { |  | ||||||
|                 if (checkBox.isChecked()) { |  | ||||||
|                     // Save the user's choice to not show the dialog again |  | ||||||
|                     defaultKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", true); |  | ||||||
|                 } |  | ||||||
|                 presenter.checkImageQuality(0); |  | ||||||
| 
 |  | ||||||
|                 UploadMediaPresenter.isCategoriesDialogShowing = false; |  | ||||||
|             }) |  | ||||||
|             .setNegativeButton("", null) |  | ||||||
|             .create(); |  | ||||||
|         alertDialog.show(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** Suggest users to turn battery optimisation off when uploading |  | ||||||
|      * more than a few files. That's because we have noticed that |  | ||||||
|      * many-files uploads have a much higher probability of failing |  | ||||||
|      * than uploads with less files. Show the dialog for Android 6 |  | ||||||
|      * and above as the ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS |  | ||||||
|      * intent was added in API level 23 |  | ||||||
|      */ |  | ||||||
|     private void showAlertForBattery(){ |  | ||||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |  | ||||||
|                 // When battery-optimisation dialog is shown don't show the image quality dialog |  | ||||||
|                 UploadMediaPresenter.isBatteryDialogShowing = true; |  | ||||||
|                 DialogUtil.showAlertDialog( |  | ||||||
|                     this, |  | ||||||
|                     getString(R.string.unrestricted_battery_mode), |  | ||||||
|                     getString(R.string.suggest_unrestricted_mode), |  | ||||||
|                     getString(R.string.title_activity_settings), |  | ||||||
|                     getString(R.string.cancel), |  | ||||||
|                     () -> { |  | ||||||
|                         /* Since opening the right settings page might be device dependent, using |  | ||||||
|                            https://github.com/WaseemSabir/BatteryPermissionHelper |  | ||||||
|                            directly appeared like a promising idea. |  | ||||||
|                            However, this simply closed the popup and did not make |  | ||||||
|                            the settings page appear on a Pixel as well as a Xiaomi device. |  | ||||||
|                            Used the standard intent instead of using this library as |  | ||||||
|                            it shows a list of all the apps on the device and allows users to |  | ||||||
|                            turn battery optimisation off. |  | ||||||
|                          */ |  | ||||||
|                         final Intent batteryOptimisationSettingsIntent = new Intent( |  | ||||||
|                             Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); |  | ||||||
|                         startActivity(batteryOptimisationSettingsIntent); |  | ||||||
|                         // calling checkImageQuality after battery dialog is interacted with |  | ||||||
|                         // so that 2 dialogs do not pop up simultaneously |  | ||||||
| 
 |  | ||||||
|                         UploadMediaPresenter.isBatteryDialogShowing = false; |  | ||||||
|                     }, |  | ||||||
|                     () -> { |  | ||||||
|                         UploadMediaPresenter.isBatteryDialogShowing = false; |  | ||||||
|                     } |  | ||||||
|                 ); |  | ||||||
|                 defaultKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", true); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * If the permission for Location is turned on and certain |  | ||||||
|      * conditions are met, returns current location of the user. |  | ||||||
|      */ |  | ||||||
|     private void handleLocation(){ |  | ||||||
|         final LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper( |  | ||||||
|             this, locationManager, null); |  | ||||||
|         if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { |  | ||||||
|             currLocation = locationManager.getLastLocation(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (currLocation != null) { |  | ||||||
|             final float locationDifference = getLocationDifference(currLocation, prevLocation); |  | ||||||
|             final boolean isLocationTagUnchecked = isLocationTagUncheckedInTheSettings(); |  | ||||||
|                     /* Remove location if the user has unchecked the Location EXIF tag in the |  | ||||||
|                        Manage EXIF Tags setting or turned "Record location for in-app shots" off. |  | ||||||
|                        Also, location information is discarded if the difference between |  | ||||||
|                        current location and location recorded just before capturing the image |  | ||||||
|                        is greater than 100 meters */ |  | ||||||
|             if (isLocationTagUnchecked || locationDifference > 100 |  | ||||||
|                 || !defaultKvStore.getBoolean("inAppCameraLocationPref") |  | ||||||
|                 || !isInAppCameraUpload) { |  | ||||||
|                 currLocation = null; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										947
									
								
								app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										947
									
								
								app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,947 @@ | ||||||
|  | package fr.free.nrw.commons.upload | ||||||
|  | 
 | ||||||
|  | import android.Manifest | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.app.ProgressDialog | ||||||
|  | import android.content.DialogInterface | ||||||
|  | import android.content.Intent | ||||||
|  | import android.content.pm.PackageManager | ||||||
|  | import android.location.Location | ||||||
|  | import android.location.LocationManager | ||||||
|  | import android.os.Build.VERSION | ||||||
|  | import android.os.Build.VERSION_CODES | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.provider.Settings | ||||||
|  | import android.view.View | ||||||
|  | import android.widget.CheckBox | ||||||
|  | import androidx.appcompat.app.AlertDialog | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import androidx.fragment.app.FragmentManager | ||||||
|  | import androidx.fragment.app.FragmentStatePagerAdapter | ||||||
|  | import androidx.recyclerview.widget.LinearLayoutManager | ||||||
|  | import androidx.viewpager.widget.ViewPager.OnPageChangeListener | ||||||
|  | import androidx.work.ExistingWorkPolicy | ||||||
|  | import fr.free.nrw.commons.R | ||||||
|  | import fr.free.nrw.commons.auth.LoginActivity | ||||||
|  | import fr.free.nrw.commons.auth.SessionManager | ||||||
|  | import fr.free.nrw.commons.contributions.ContributionController | ||||||
|  | import fr.free.nrw.commons.databinding.ActivityUploadBinding | ||||||
|  | import fr.free.nrw.commons.filepicker.Constants.RequestCodes | ||||||
|  | import fr.free.nrw.commons.filepicker.UploadableFile | ||||||
|  | import fr.free.nrw.commons.kvstore.BasicKvStore | ||||||
|  | import fr.free.nrw.commons.kvstore.JsonKvStore | ||||||
|  | import fr.free.nrw.commons.location.LatLng | ||||||
|  | import fr.free.nrw.commons.location.LocationPermissionsHelper | ||||||
|  | import fr.free.nrw.commons.location.LocationServiceManager | ||||||
|  | import fr.free.nrw.commons.mwapi.UserClient | ||||||
|  | import fr.free.nrw.commons.nearby.Place | ||||||
|  | import fr.free.nrw.commons.settings.Prefs | ||||||
|  | import fr.free.nrw.commons.theme.BaseActivity | ||||||
|  | import fr.free.nrw.commons.upload.ThumbnailsAdapter.OnThumbnailDeletedListener | ||||||
|  | import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment | ||||||
|  | import fr.free.nrw.commons.upload.depicts.DepictsFragment | ||||||
|  | import fr.free.nrw.commons.upload.license.MediaLicenseFragment | ||||||
|  | import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment | ||||||
|  | import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback | ||||||
|  | import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter | ||||||
|  | import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest | ||||||
|  | import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | ||||||
|  | import fr.free.nrw.commons.utils.PermissionUtils.PERMISSIONS_STORAGE | ||||||
|  | import fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction | ||||||
|  | import fr.free.nrw.commons.utils.PermissionUtils.hasPartialAccess | ||||||
|  | import fr.free.nrw.commons.utils.PermissionUtils.hasPermission | ||||||
|  | import fr.free.nrw.commons.utils.ViewUtil.showLongToast | ||||||
|  | import fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT | ||||||
|  | import fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE | ||||||
|  | import fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY | ||||||
|  | import io.reactivex.android.schedulers.AndroidSchedulers | ||||||
|  | import io.reactivex.schedulers.Schedulers | ||||||
|  | import timber.log.Timber | ||||||
|  | import java.io.File | ||||||
|  | import javax.inject.Inject | ||||||
|  | import javax.inject.Named | ||||||
|  | 
 | ||||||
|  | class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.Callback, | ||||||
|  |     OnThumbnailDeletedListener { | ||||||
|  |     @JvmField | ||||||
|  |     @Inject | ||||||
|  |     var contributionController: ContributionController? = null | ||||||
|  | 
 | ||||||
|  |     @JvmField | ||||||
|  |     @Inject | ||||||
|  |     @field:Named("default_preferences") | ||||||
|  |     var directKvStore: JsonKvStore? = null | ||||||
|  | 
 | ||||||
|  |     @JvmField | ||||||
|  |     @Inject | ||||||
|  |     var presenter: UploadContract.UserActionListener? = null | ||||||
|  | 
 | ||||||
|  |     @JvmField | ||||||
|  |     @Inject | ||||||
|  |     var sessionManager: SessionManager? = null | ||||||
|  | 
 | ||||||
|  |     @JvmField | ||||||
|  |     @Inject | ||||||
|  |     var userClient: UserClient? = null | ||||||
|  | 
 | ||||||
|  |     @JvmField | ||||||
|  |     @Inject | ||||||
|  |     var locationManager: LocationServiceManager? = null | ||||||
|  | 
 | ||||||
|  |     private var isTitleExpanded = true | ||||||
|  | 
 | ||||||
|  |     private var progressDialog: ProgressDialog? = null | ||||||
|  |     private var uploadImagesAdapter: UploadImageAdapter? = null | ||||||
|  |     private var fragments: MutableList<UploadBaseFragment>? = null | ||||||
|  |     private var uploadCategoriesFragment: UploadCategoriesFragment? = null | ||||||
|  |     private var depictsFragment: DepictsFragment? = null | ||||||
|  |     private var mediaLicenseFragment: MediaLicenseFragment? = null | ||||||
|  |     private var thumbnailsAdapter: ThumbnailsAdapter? = null | ||||||
|  |     var store: BasicKvStore? = null | ||||||
|  |     private var place: Place? = null | ||||||
|  |     private var prevLocation: LatLng? = null | ||||||
|  |     private var currLocation: LatLng? = null | ||||||
|  |     private var isInAppCameraUpload = false | ||||||
|  |     private var uploadableFiles: MutableList<UploadableFile> = mutableListOf() | ||||||
|  |     private var currentSelectedPosition = 0 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns if multiple files selected or not. | ||||||
|  |      */ | ||||||
|  |     /* | ||||||
|  |          Checks for if multiple files selected | ||||||
|  |          */ | ||||||
|  |     var isMultipleFilesSelected: Boolean = false | ||||||
|  |         private set | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the value of the showPermissionDialog variable. | ||||||
|  |      * | ||||||
|  |      * @return `true` if Permission Dialog should be shown, `false` otherwise. | ||||||
|  |      */ | ||||||
|  |     /** | ||||||
|  |      * Set the value of the showPermissionDialog variable. | ||||||
|  |      * | ||||||
|  |      * @param showPermissionsDialog `true` to indicate to show | ||||||
|  |      * Permissions Dialog if permissions are missing, `false` otherwise. | ||||||
|  |      */ | ||||||
|  |     /** | ||||||
|  |      * A private boolean variable to control whether a permissions dialog should be shown | ||||||
|  |      * when necessary. Initially, it is set to `true`, indicating that the permissions dialog | ||||||
|  |      * should be displayed if permissions are missing and it is first time calling | ||||||
|  |      * `checkStoragePermissions` method. | ||||||
|  |      * This variable is used in the `checkStoragePermissions` method to determine whether to | ||||||
|  |      * show a permissions dialog to the user if the required permissions are not granted. | ||||||
|  |      * If `showPermissionsDialog` is set to `true` and the necessary permissions are missing, | ||||||
|  |      * a permissions dialog will be displayed to request the required permissions. If set | ||||||
|  |      * to `false`, the dialog won't be shown. | ||||||
|  |      * | ||||||
|  |      * @see UploadActivity.checkStoragePermissions | ||||||
|  |      */ | ||||||
|  |     var isShowPermissionsDialog: Boolean = true | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Whether fragments have been saved. | ||||||
|  |      */ | ||||||
|  |     private var isFragmentsSaved = false | ||||||
|  | 
 | ||||||
|  |     override val totalNumberOfSteps: Int | ||||||
|  |         get() = fragments!!.size | ||||||
|  | 
 | ||||||
|  |     override val isWLMUpload: Boolean | ||||||
|  |         get() = place != null && place!!.isMonument | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Users may uncheck Location tag from the Manage EXIF tags setting any time. | ||||||
|  |      * So, their location must not be shared in this case. | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     private val isLocationTagUncheckedInTheSettings: Boolean | ||||||
|  |         get() { | ||||||
|  |             val prefExifTags: Set<String> = | ||||||
|  |                 defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS) | ||||||
|  |             return !prefExifTags.contains(getString(R.string.exif_tag_location)) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     private var _binding: ActivityUploadBinding? = null | ||||||
|  |     private val binding: ActivityUploadBinding get() = _binding!! | ||||||
|  | 
 | ||||||
|  |     @SuppressLint("CheckResult") | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  | 
 | ||||||
|  |         _binding = ActivityUploadBinding.inflate(layoutInflater) | ||||||
|  |         setContentView(binding.root) | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          If Configuration of device is changed then get the new fragments | ||||||
|  |          created by the system and populate the fragments ArrayList | ||||||
|  |          */ | ||||||
|  |         if (savedInstanceState != null) { | ||||||
|  |             isFragmentsSaved = true | ||||||
|  |             fragments = mutableListOf<UploadBaseFragment>().apply { | ||||||
|  |                 supportFragmentManager.fragments.forEach { fragment -> | ||||||
|  |                     add(fragment as UploadBaseFragment) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         init() | ||||||
|  |         binding.rlContainerTitle.setOnClickListener { v: View? -> onRlContainerTitleClicked() } | ||||||
|  |         nearbyPopupAnswers = mutableMapOf() | ||||||
|  |         //getting the current dpi of the device and if it is less than 320dp i.e. overlapping | ||||||
|  |         //threshold, thumbnails automatically minimizes | ||||||
|  |         val metrics = resources.displayMetrics | ||||||
|  |         val dpi = (metrics.widthPixels) / (metrics.density) | ||||||
|  |         if (dpi <= 321) { | ||||||
|  |             onRlContainerTitleClicked() | ||||||
|  |         } | ||||||
|  |         if (hasPermission(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION))) { | ||||||
|  |             locationManager!!.registerLocationManager() | ||||||
|  |         } | ||||||
|  |         locationManager!!.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER) | ||||||
|  |         locationManager!!.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) | ||||||
|  |         store = BasicKvStore(this, storeNameForCurrentUploadImagesSize).apply { | ||||||
|  |             clearAll() | ||||||
|  |         } | ||||||
|  |         checkStoragePermissions() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun init() { | ||||||
|  |         initProgressDialog() | ||||||
|  |         initViewPager() | ||||||
|  |         initThumbnailsRecyclerView() | ||||||
|  |         //And init other things you need to | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun initProgressDialog() { | ||||||
|  |         progressDialog = ProgressDialog(this) | ||||||
|  |         progressDialog!!.setMessage(getString(R.string.please_wait)) | ||||||
|  |         progressDialog!!.setCancelable(false) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun initThumbnailsRecyclerView() { | ||||||
|  |         binding.rvThumbnails.layoutManager = LinearLayoutManager( | ||||||
|  |             this, | ||||||
|  |             LinearLayoutManager.HORIZONTAL, false | ||||||
|  |         ) | ||||||
|  |         thumbnailsAdapter = ThumbnailsAdapter { currentSelectedPosition } | ||||||
|  |         thumbnailsAdapter!!.onThumbnailDeletedListener = this | ||||||
|  |         binding.rvThumbnails.adapter = thumbnailsAdapter | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun initViewPager() { | ||||||
|  |         uploadImagesAdapter = UploadImageAdapter(supportFragmentManager) | ||||||
|  |         binding.vpUpload.adapter = uploadImagesAdapter | ||||||
|  |         binding.vpUpload.addOnPageChangeListener(object : OnPageChangeListener { | ||||||
|  |             override fun onPageScrolled( | ||||||
|  |                 position: Int, positionOffset: Float, | ||||||
|  |                 positionOffsetPixels: Int | ||||||
|  |             ) = Unit | ||||||
|  | 
 | ||||||
|  |             override fun onPageSelected(position: Int) { | ||||||
|  |                 currentSelectedPosition = position | ||||||
|  |                 if (position >= uploadableFiles!!.size) { | ||||||
|  |                     binding.cvContainerTopCard.visibility = View.GONE | ||||||
|  |                 } else { | ||||||
|  |                     thumbnailsAdapter!!.notifyDataSetChanged() | ||||||
|  |                     binding.cvContainerTopCard.visibility = View.VISIBLE | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             override fun onPageScrollStateChanged(state: Int) = Unit | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun isLoggedIn(): Boolean = sessionManager!!.isUserLoggedIn | ||||||
|  | 
 | ||||||
|  |     override fun onResume() { | ||||||
|  |         super.onResume() | ||||||
|  |         presenter!!.onAttachView(this) | ||||||
|  |         if (!isLoggedIn()) { | ||||||
|  |             askUserToLogIn() | ||||||
|  |         } | ||||||
|  |         checkBlockStatus() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Makes API call to check if user is blocked from Commons. If the user is blocked, a snackbar | ||||||
|  |      * is created to notify the user | ||||||
|  |      */ | ||||||
|  |     protected fun checkBlockStatus() { | ||||||
|  |         compositeDisposable.add( | ||||||
|  |             userClient!!.isUserBlockedFromCommons() | ||||||
|  |                 .subscribeOn(Schedulers.io()) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .filter { result: Boolean? -> result!! } | ||||||
|  |                 .subscribe { result: Boolean? -> | ||||||
|  |                     showAlertDialog( | ||||||
|  |                         this, | ||||||
|  |                         getString(R.string.block_notification_title), | ||||||
|  |                         getString(R.string.block_notification), | ||||||
|  |                         getString(R.string.ok) | ||||||
|  |                     ) { finish() } | ||||||
|  |                 }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun checkStoragePermissions() { | ||||||
|  |         // Check if all required permissions are granted | ||||||
|  |         val hasAllPermissions = hasPermission(this, PERMISSIONS_STORAGE) | ||||||
|  |         val hasPartialAccess = hasPartialAccess(this) | ||||||
|  |         if (hasAllPermissions || hasPartialAccess) { | ||||||
|  |             // All required permissions are granted, so enable UI elements and perform actions | ||||||
|  |             receiveSharedItems() | ||||||
|  |             binding.cvContainerTopCard.visibility = View.VISIBLE | ||||||
|  |         } else { | ||||||
|  |             // Permissions are missing | ||||||
|  |             binding.cvContainerTopCard.visibility = View.INVISIBLE | ||||||
|  |             if (isShowPermissionsDialog) { | ||||||
|  |                 checkPermissionsAndPerformAction( | ||||||
|  |                     this, | ||||||
|  |                     Runnable { | ||||||
|  |                         binding.cvContainerTopCard.visibility = View.VISIBLE | ||||||
|  |                         receiveSharedItems() | ||||||
|  |                     }, Runnable { | ||||||
|  |                         isShowPermissionsDialog = true | ||||||
|  |                         checkStoragePermissions() | ||||||
|  |                     }, | ||||||
|  |                     R.string.storage_permission_title, | ||||||
|  |                     R.string.write_storage_permission_rationale_for_image_share, | ||||||
|  |                     *PERMISSIONS_STORAGE | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         /* If all permissions are not granted and a dialog is already showing on screen | ||||||
|  |          showPermissionsDialog will set to false making it not show dialog again onResume, | ||||||
|  |          but if user Denies any permission showPermissionsDialog will be to true | ||||||
|  |          and permissions dialog will be shown again. | ||||||
|  |          */ | ||||||
|  |         isShowPermissionsDialog = hasAllPermissions | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onStop() { | ||||||
|  |         // Resetting setImageCancelled to false | ||||||
|  |         setImageCancelled(false) | ||||||
|  |         super.onStop() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun returnToMainActivity() = finish() | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * go to the uploadProgress activity to check the status of uploading | ||||||
|  |      */ | ||||||
|  |     override fun goToUploadProgressActivity() = | ||||||
|  |         startActivity(Intent(this, UploadProgressActivity::class.java)) | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show/Hide the progress dialog | ||||||
|  |      */ | ||||||
|  |     override fun showProgress(shouldShow: Boolean) { | ||||||
|  |         if (shouldShow) { | ||||||
|  |             if (!progressDialog!!.isShowing) { | ||||||
|  |                 progressDialog!!.show() | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             if (progressDialog != null && !isFinishing) { | ||||||
|  |                 progressDialog!!.dismiss() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getIndexInViewFlipper(fragment: UploadBaseFragment?): Int = | ||||||
|  |         fragments!!.indexOf(fragment) | ||||||
|  | 
 | ||||||
|  |     override fun showMessage(messageResourceId: Int) { | ||||||
|  |         showLongToast(this, messageResourceId) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getUploadableFiles(): List<UploadableFile>? { | ||||||
|  |         return uploadableFiles | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun showHideTopCard(shouldShow: Boolean) { | ||||||
|  |         binding.llContainerTopCard.visibility = | ||||||
|  |             if (shouldShow) View.VISIBLE else View.GONE | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onUploadMediaDeleted(index: Int) { | ||||||
|  |         fragments!!.removeAt(index) //Remove the corresponding fragment | ||||||
|  |         uploadableFiles.removeAt(index) //Remove the files from the list | ||||||
|  |         thumbnailsAdapter!!.notifyItemRemoved(index) //Notify the thumbnails adapter | ||||||
|  |         uploadImagesAdapter!!.notifyDataSetChanged() //Notify the ViewPager | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun updateTopCardTitle() { | ||||||
|  |         binding.tvTopCardTitle.text = resources | ||||||
|  |             .getQuantityString( | ||||||
|  |                 R.plurals.upload_count_title, | ||||||
|  |                 uploadableFiles!!.size, | ||||||
|  |                 uploadableFiles!!.size | ||||||
|  |             ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun makeUploadRequest() { | ||||||
|  |         makeOneTimeWorkRequest( | ||||||
|  |             applicationContext, | ||||||
|  |             ExistingWorkPolicy.APPEND_OR_REPLACE | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun askUserToLogIn() { | ||||||
|  |         Timber.d("current session is null, asking user to login") | ||||||
|  |         showLongToast(this, getString(R.string.user_not_logged_in)) | ||||||
|  |         val loginIntent = Intent(this@UploadActivity, LoginActivity::class.java) | ||||||
|  |         startActivity(loginIntent) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onRequestPermissionsResult( | ||||||
|  |         requestCode: Int, | ||||||
|  |         permissions: Array<String>, | ||||||
|  |         grantResults: IntArray | ||||||
|  |     ) { | ||||||
|  |         var areAllGranted = false | ||||||
|  |         if (requestCode == RequestCodes.STORAGE) { | ||||||
|  |             if (VERSION.SDK_INT >= VERSION_CODES.M) { | ||||||
|  |                 for (i in grantResults.indices) { | ||||||
|  |                     val permission = permissions[i] | ||||||
|  |                     areAllGranted = grantResults[i] == PackageManager.PERMISSION_GRANTED | ||||||
|  |                     if (grantResults[i] == PackageManager.PERMISSION_DENIED) { | ||||||
|  |                         val showRationale = shouldShowRequestPermissionRationale(permission) | ||||||
|  |                         if (!showRationale) { | ||||||
|  |                             showAlertDialog( | ||||||
|  |                                 this, | ||||||
|  |                                 getString(R.string.storage_permissions_denied), | ||||||
|  |                                 getString(R.string.unable_to_share_upload_item), | ||||||
|  |                                 getString(android.R.string.ok) | ||||||
|  |                             ) { finish() } | ||||||
|  |                         } else { | ||||||
|  |                             showAlertDialog( | ||||||
|  |                                 this, | ||||||
|  |                                 getString(R.string.storage_permission_title), | ||||||
|  |                                 getString( | ||||||
|  |                                     R.string.write_storage_permission_rationale_for_image_share | ||||||
|  |                                 ), | ||||||
|  |                                 getString(android.R.string.ok) | ||||||
|  |                             ) { checkStoragePermissions() } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (areAllGranted) { | ||||||
|  |                     receiveSharedItems() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         super.onRequestPermissionsResult(requestCode, permissions, grantResults) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun receiveSharedItems() { | ||||||
|  |         val intent = intent | ||||||
|  |         val action = intent.action | ||||||
|  |         if (Intent.ACTION_SEND == action || Intent.ACTION_SEND_MULTIPLE == action) { | ||||||
|  |             receiveExternalSharedItems() | ||||||
|  |         } else if (ContributionController.ACTION_INTERNAL_UPLOADS == action) { | ||||||
|  |             receiveInternalSharedItems() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (uploadableFiles == null || uploadableFiles!!.isEmpty()) { | ||||||
|  |             handleNullMedia() | ||||||
|  |         } else { | ||||||
|  |             //Show thumbnails | ||||||
|  |             if (uploadableFiles!!.size > 1) { | ||||||
|  |                 if (!defaultKvStore.getBoolean("hasAlreadyLaunchedCategoriesDialog")) { //If there is only file, no need to show the image thumbnails | ||||||
|  |                     showAlertDialogForCategories() | ||||||
|  |                 } | ||||||
|  |                 if (uploadableFiles!!.size > 3 && | ||||||
|  |                     !defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload") | ||||||
|  |                 ) { | ||||||
|  |                     showAlertForBattery() | ||||||
|  |                 } | ||||||
|  |                 thumbnailsAdapter!!.uploadableFiles = uploadableFiles | ||||||
|  |             } else { | ||||||
|  |                 binding.llContainerTopCard.visibility = View.GONE | ||||||
|  |             } | ||||||
|  |             binding.tvTopCardTitle.text = resources | ||||||
|  |                 .getQuantityString( | ||||||
|  |                     R.plurals.upload_count_title, | ||||||
|  |                     uploadableFiles!!.size, | ||||||
|  |                     uploadableFiles!!.size | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             if (fragments == null) { | ||||||
|  |                 fragments = mutableListOf() | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             for (uploadableFile in uploadableFiles!!) { | ||||||
|  |                 val uploadMediaDetailFragment = UploadMediaDetailFragment() | ||||||
|  | 
 | ||||||
|  |                 if (!uploadIsOfAPlace) { | ||||||
|  |                     handleLocation() | ||||||
|  |                     uploadMediaDetailFragment.setImageToBeUploaded( | ||||||
|  |                         uploadableFile, | ||||||
|  |                         place, | ||||||
|  |                         currLocation | ||||||
|  |                     ) | ||||||
|  |                     locationManager!!.unregisterLocationManager() | ||||||
|  |                 } else { | ||||||
|  |                     uploadMediaDetailFragment.setImageToBeUploaded( | ||||||
|  |                         uploadableFile, | ||||||
|  |                         place, | ||||||
|  |                         currLocation | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 val uploadMediaDetailFragmentCallback: UploadMediaDetailFragmentCallback = | ||||||
|  |                     object : UploadMediaDetailFragmentCallback { | ||||||
|  |                         override fun deletePictureAtIndex(index: Int) { | ||||||
|  |                             store!!.putInt( | ||||||
|  |                                 keyForCurrentUploadImagesSize, | ||||||
|  |                                 (store!!.getInt(keyForCurrentUploadImagesSize) - 1) | ||||||
|  |                             ) | ||||||
|  |                             presenter!!.deletePictureAtIndex(index) | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         /** | ||||||
|  |                          * Changes the thumbnail of an UploadableFile at the specified index. | ||||||
|  |                          * This method updates the list of uploadableFiles by replacing the UploadableFile | ||||||
|  |                          * at the given index with a new UploadableFile created from the provided file path. | ||||||
|  |                          * After updating the list, it notifies the RecyclerView's adapter to refresh its data, | ||||||
|  |                          * ensuring that the thumbnail change is reflected in the UI. | ||||||
|  |                          * | ||||||
|  |                          * @param index The index of the UploadableFile to be updated. | ||||||
|  |                          * @param uri The file path of the new thumbnail image. | ||||||
|  |                          */ | ||||||
|  |                         override fun changeThumbnail(index: Int, uri: String) { | ||||||
|  |                             uploadableFiles.removeAt(index) | ||||||
|  |                             uploadableFiles.add(index, UploadableFile(File(uri))) | ||||||
|  |                             binding.rvThumbnails.adapter!!.notifyDataSetChanged() | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         override fun onNextButtonClicked(index: Int) { | ||||||
|  |                             this@UploadActivity.onNextButtonClicked(index) | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         override fun onPreviousButtonClicked(index: Int) { | ||||||
|  |                             this@UploadActivity.onPreviousButtonClicked(index) | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         override fun showProgress(shouldShow: Boolean) { | ||||||
|  |                             this@UploadActivity.showProgress(shouldShow) | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         override fun getIndexInViewFlipper(fragment: UploadBaseFragment?): Int { | ||||||
|  |                             return fragments!!.indexOf(fragment) | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         override val totalNumberOfSteps: Int | ||||||
|  |                             get() = fragments!!.size | ||||||
|  | 
 | ||||||
|  |                         override val isWLMUpload: Boolean | ||||||
|  |                             get() = place != null && place!!.isMonument | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                 if (isFragmentsSaved) { | ||||||
|  |                     val fragment = fragments!![0] as UploadMediaDetailFragment? | ||||||
|  |                     fragment!!.fragmentCallback = uploadMediaDetailFragmentCallback | ||||||
|  |                 } else { | ||||||
|  |                     uploadMediaDetailFragment.fragmentCallback = uploadMediaDetailFragmentCallback | ||||||
|  |                     fragments!!.add(uploadMediaDetailFragment) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             //If fragments are not created, create them and add them to the fragments ArrayList | ||||||
|  |             if (!isFragmentsSaved) { | ||||||
|  |                 uploadCategoriesFragment = UploadCategoriesFragment() | ||||||
|  |                 if (place != null) { | ||||||
|  |                     val categoryBundle = Bundle() | ||||||
|  |                     categoryBundle.putString(SELECTED_NEARBY_PLACE_CATEGORY, place!!.category) | ||||||
|  |                     uploadCategoriesFragment!!.arguments = categoryBundle | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 uploadCategoriesFragment!!.callback = this | ||||||
|  | 
 | ||||||
|  |                 depictsFragment = DepictsFragment() | ||||||
|  |                 val placeBundle = Bundle() | ||||||
|  |                 placeBundle.putParcelable(SELECTED_NEARBY_PLACE, place) | ||||||
|  |                 depictsFragment!!.arguments = placeBundle | ||||||
|  |                 depictsFragment!!.callback = this | ||||||
|  | 
 | ||||||
|  |                 mediaLicenseFragment = MediaLicenseFragment() | ||||||
|  |                 mediaLicenseFragment!!.callback = this | ||||||
|  | 
 | ||||||
|  |                 fragments!!.add(depictsFragment!!) | ||||||
|  |                 fragments!!.add(uploadCategoriesFragment!!) | ||||||
|  |                 fragments!!.add(mediaLicenseFragment!!) | ||||||
|  |             } else { | ||||||
|  |                 for (i in 1 until fragments!!.size) { | ||||||
|  |                     fragments!![i]!!.callback = object : UploadBaseFragment.Callback { | ||||||
|  |                         override fun onNextButtonClicked(index: Int) { | ||||||
|  |                             if (index < fragments!!.size - 1) { | ||||||
|  |                                 binding.vpUpload.setCurrentItem(index + 1, false) | ||||||
|  |                                 fragments!![index + 1]!!.onBecameVisible() | ||||||
|  |                                 (binding.rvThumbnails.layoutManager as LinearLayoutManager) | ||||||
|  |                                     .scrollToPositionWithOffset( | ||||||
|  |                                         if ((index > 0)) index - 1 else 0, | ||||||
|  |                                         0 | ||||||
|  |                                     ) | ||||||
|  |                             } else { | ||||||
|  |                                 presenter!!.handleSubmit() | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         override fun onPreviousButtonClicked(index: Int) { | ||||||
|  |                             if (index != 0) { | ||||||
|  |                                 binding.vpUpload.setCurrentItem(index - 1, true) | ||||||
|  |                                 fragments!![index - 1]!!.onBecameVisible() | ||||||
|  |                                 (binding.rvThumbnails.layoutManager as LinearLayoutManager) | ||||||
|  |                                     .scrollToPositionWithOffset( | ||||||
|  |                                         if ((index > 3)) index - 2 else 0, | ||||||
|  |                                         0 | ||||||
|  |                                     ) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         override fun showProgress(shouldShow: Boolean) { | ||||||
|  |                             if (shouldShow) { | ||||||
|  |                                 if (!progressDialog!!.isShowing) { | ||||||
|  |                                     progressDialog!!.show() | ||||||
|  |                                 } | ||||||
|  |                             } else { | ||||||
|  |                                 if (progressDialog != null && !isFinishing) { | ||||||
|  |                                     progressDialog!!.dismiss() | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         override fun getIndexInViewFlipper(fragment: UploadBaseFragment?): Int { | ||||||
|  |                             return fragments!!.indexOf(fragment) | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         override val totalNumberOfSteps: Int | ||||||
|  |                             get() = fragments!!.size | ||||||
|  | 
 | ||||||
|  |                         override val isWLMUpload: Boolean | ||||||
|  |                             get() = place != null && place!!.isMonument | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             uploadImagesAdapter!!.fragments = fragments!! | ||||||
|  |             binding.vpUpload.offscreenPageLimit = fragments!!.size | ||||||
|  |         } | ||||||
|  |         // Saving size of uploadableFiles | ||||||
|  |         store!!.putInt(keyForCurrentUploadImagesSize, uploadableFiles!!.size) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Changes current image when one image upload is cancelled, to highlight next image in the top thumbnail. | ||||||
|  |      * Fixes: [Issue](https://github.com/commons-app/apps-android-commons/issues/5511) | ||||||
|  |      * | ||||||
|  |      * @param index Index of image to be removed | ||||||
|  |      * @param maxSize Max size of the `uploadableFiles` | ||||||
|  |      */ | ||||||
|  |     override fun highlightNextImageOnCancelledImage(index: Int, maxSize: Int) { | ||||||
|  |         if (index < maxSize) { | ||||||
|  |             binding.vpUpload.setCurrentItem(index + 1, false) | ||||||
|  |             binding.vpUpload.setCurrentItem(index, false) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Used to check if user has cancelled upload of any image in current upload | ||||||
|  |      * so that location compare doesn't show up again in same upload. | ||||||
|  |      * Fixes: [Issue](https://github.com/commons-app/apps-android-commons/issues/5511) | ||||||
|  |      * | ||||||
|  |      * @param isCancelled Is true when user has cancelled upload of any image in current upload | ||||||
|  |      */ | ||||||
|  |     override fun setImageCancelled(isCancelled: Boolean) { | ||||||
|  |         val basicKvStore = BasicKvStore(this, "IsAnyImageCancelled") | ||||||
|  |         basicKvStore.putBoolean("IsAnyImageCancelled", isCancelled) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Calculate the difference between current location and | ||||||
|  |      * location recorded before capturing the image | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     private fun getLocationDifference(currLocation: LatLng, prevLocation: LatLng?): Float { | ||||||
|  |         if (prevLocation == null) { | ||||||
|  |             return 0.0f | ||||||
|  |         } | ||||||
|  |         val distance = FloatArray(2) | ||||||
|  |         Location.distanceBetween( | ||||||
|  |             currLocation.latitude, currLocation.longitude, | ||||||
|  |             prevLocation.latitude, prevLocation.longitude, distance | ||||||
|  |         ) | ||||||
|  |         return distance[0] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun receiveExternalSharedItems() { | ||||||
|  |         uploadableFiles = contributionController!!.handleExternalImagesPicked(this, intent) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun receiveInternalSharedItems() { | ||||||
|  |         val intent = intent | ||||||
|  | 
 | ||||||
|  |         Timber.d("Received intent %s with action %s", intent.toString(), intent.action) | ||||||
|  | 
 | ||||||
|  |         uploadableFiles = mutableListOf<UploadableFile>().apply { | ||||||
|  |             addAll(intent.getParcelableArrayListExtra(EXTRA_FILES) ?: emptyList()) | ||||||
|  |         } | ||||||
|  |         isMultipleFilesSelected = uploadableFiles!!.size > 1 | ||||||
|  |         Timber.i("Received multiple upload %s", uploadableFiles!!.size) | ||||||
|  | 
 | ||||||
|  |         place = intent.getParcelableExtra<Place>(PLACE_OBJECT) | ||||||
|  |         prevLocation = intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE) | ||||||
|  |         isInAppCameraUpload = intent.getBooleanExtra(IN_APP_CAMERA_UPLOAD, false) | ||||||
|  |         resetDirectPrefs() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun resetDirectPrefs() = directKvStore!!.remove(PLACE_OBJECT) | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handle null URI from the received intent. | ||||||
|  |      * Current implementation will simply show a toast and finish the upload activity. | ||||||
|  |      */ | ||||||
|  |     private fun handleNullMedia() { | ||||||
|  |         showLongToast(this, R.string.error_processing_image) | ||||||
|  |         finish() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     override fun showAlertDialog(messageResourceId: Int, onPositiveClick: Runnable) { | ||||||
|  |         showAlertDialog( | ||||||
|  |             this, | ||||||
|  |             "", | ||||||
|  |             getString(messageResourceId), | ||||||
|  |             getString(R.string.ok), | ||||||
|  |             onPositiveClick | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onNextButtonClicked(index: Int) { | ||||||
|  |         if (index < fragments!!.size - 1) { | ||||||
|  |             binding.vpUpload.setCurrentItem(index + 1, false) | ||||||
|  |             fragments!![index + 1]!!.onBecameVisible() | ||||||
|  |             (binding.rvThumbnails.layoutManager as LinearLayoutManager) | ||||||
|  |                 .scrollToPositionWithOffset(if ((index > 0)) index - 1 else 0, 0) | ||||||
|  |             if (index < fragments!!.size - 4) { | ||||||
|  |                 // check image quality if next image exists | ||||||
|  |                 presenter!!.checkImageQuality(index + 1) | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             presenter!!.handleSubmit() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onPreviousButtonClicked(index: Int) { | ||||||
|  |         if (index != 0) { | ||||||
|  |             binding.vpUpload.setCurrentItem(index - 1, true) | ||||||
|  |             fragments!![index - 1]!!.onBecameVisible() | ||||||
|  |             (binding.rvThumbnails.layoutManager as LinearLayoutManager) | ||||||
|  |                 .scrollToPositionWithOffset(if ((index > 3)) index - 2 else 0, 0) | ||||||
|  |             if ((index != 1) && ((index - 1) < uploadableFiles!!.size)) { | ||||||
|  |                 // Shows the top card if it was hidden because of the last image being deleted and | ||||||
|  |                 // now the user has hit previous button to go back to the media details | ||||||
|  |                 showHideTopCard(true) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onThumbnailDeleted(position: Int) = presenter!!.deletePictureAtIndex(position) | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The adapter used to show image upload intermediate fragments | ||||||
|  |      */ | ||||||
|  |     private class UploadImageAdapter(fragmentManager: FragmentManager) : | ||||||
|  |         FragmentStatePagerAdapter(fragmentManager) { | ||||||
|  |         var fragments: List<UploadBaseFragment> = mutableListOf() | ||||||
|  |             set(value) { | ||||||
|  |                 field = value | ||||||
|  |                 notifyDataSetChanged() | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         override fun getItem(position: Int): Fragment { | ||||||
|  |             return fragments[position] | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         override fun getCount(): Int { | ||||||
|  |             return fragments.size | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         override fun getItemPosition(item: Any): Int { | ||||||
|  |             return POSITION_NONE | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     fun onRlContainerTitleClicked() { | ||||||
|  |         binding.rvThumbnails.visibility = | ||||||
|  |             if (isTitleExpanded) View.GONE else View.VISIBLE | ||||||
|  |         isTitleExpanded = !isTitleExpanded | ||||||
|  |         binding.ibToggleTopCard.rotation = binding.ibToggleTopCard.rotation + 180 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onDestroy() { | ||||||
|  |         super.onDestroy() | ||||||
|  |         // Resetting all values in store by clearing them | ||||||
|  |         store!!.clearAll() | ||||||
|  |         presenter!!.onDetachView() | ||||||
|  |         compositeDisposable.clear() | ||||||
|  |         fragments = null | ||||||
|  |         uploadImagesAdapter = null | ||||||
|  |         if (mediaLicenseFragment != null) { | ||||||
|  |             mediaLicenseFragment!!.callback = null | ||||||
|  |         } | ||||||
|  |         if (uploadCategoriesFragment != null) { | ||||||
|  |             uploadCategoriesFragment!!.callback = null | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Overrides the back button to make sure the user is prepared to lose their progress | ||||||
|  |      */ | ||||||
|  |     override fun onBackPressed() { | ||||||
|  |         showAlertDialog( | ||||||
|  |             this, | ||||||
|  |             getString(R.string.back_button_warning), | ||||||
|  |             getString(R.string.back_button_warning_desc), | ||||||
|  |             getString(R.string.back_button_continue), | ||||||
|  |             getString(R.string.back_button_warning), | ||||||
|  |             null | ||||||
|  |         ) { finish() } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * If the user uploads more than 1 file informs that | ||||||
|  |      * depictions/categories apply to all pictures of a multi upload. | ||||||
|  |      * This method takes no arguments and does not return any value. | ||||||
|  |      * It shows the AlertDialog and continues the flow of uploads. | ||||||
|  |      */ | ||||||
|  |     private fun showAlertDialogForCategories() { | ||||||
|  |         UploadMediaPresenter.isCategoriesDialogShowing = true | ||||||
|  |         // Inflate the custom layout | ||||||
|  |         val inflater = layoutInflater | ||||||
|  |         val view = inflater.inflate(R.layout.activity_upload_categories_dialog, null) | ||||||
|  |         val checkBox = view.findViewById<CheckBox>(R.id.categories_checkbox) | ||||||
|  |         // Create the alert dialog | ||||||
|  |         val alertDialog = AlertDialog.Builder(this) | ||||||
|  |             .setView(view) | ||||||
|  |             .setTitle(getString(R.string.multiple_files_depiction_header)) | ||||||
|  |             .setMessage(getString(R.string.multiple_files_depiction)) | ||||||
|  |             .setPositiveButton("OK") { dialog: DialogInterface?, which: Int -> | ||||||
|  |                 if (checkBox.isChecked) { | ||||||
|  |                     // Save the user's choice to not show the dialog again | ||||||
|  |                     defaultKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", true) | ||||||
|  |                 } | ||||||
|  |                 presenter!!.setupBasicKvStoreFactory { BasicKvStore(this@UploadActivity, it) } | ||||||
|  |                 presenter!!.checkImageQuality(0) | ||||||
|  |                 UploadMediaPresenter.isCategoriesDialogShowing = false | ||||||
|  |             } | ||||||
|  |             .setNegativeButton("", null) | ||||||
|  |             .create() | ||||||
|  |         alertDialog.show() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** Suggest users to turn battery optimisation off when uploading | ||||||
|  |      * more than a few files. That's because we have noticed that | ||||||
|  |      * many-files uploads have a much higher probability of failing | ||||||
|  |      * than uploads with less files. Show the dialog for Android 6 | ||||||
|  |      * and above as the ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS | ||||||
|  |      * intent was added in API level 23 | ||||||
|  |      */ | ||||||
|  |     private fun showAlertForBattery() { | ||||||
|  |         if (VERSION.SDK_INT >= VERSION_CODES.M) { | ||||||
|  |             // When battery-optimisation dialog is shown don't show the image quality dialog | ||||||
|  |             UploadMediaPresenter.isBatteryDialogShowing = true | ||||||
|  |             showAlertDialog( | ||||||
|  |                 this, | ||||||
|  |                 getString(R.string.unrestricted_battery_mode), | ||||||
|  |                 getString(R.string.suggest_unrestricted_mode), | ||||||
|  |                 getString(R.string.title_activity_settings), | ||||||
|  |                 getString(R.string.cancel), | ||||||
|  |                 { | ||||||
|  |                     /* Since opening the right settings page might be device dependent, using | ||||||
|  |                                               https://github.com/WaseemSabir/BatteryPermissionHelper | ||||||
|  |                                               directly appeared like a promising idea. | ||||||
|  |                                               However, this simply closed the popup and did not make | ||||||
|  |                                               the settings page appear on a Pixel as well as a Xiaomi device. | ||||||
|  |                                               Used the standard intent instead of using this library as | ||||||
|  |                                               it shows a list of all the apps on the device and allows users to | ||||||
|  |                                               turn battery optimisation off. | ||||||
|  |                                             */ | ||||||
|  |                     val batteryOptimisationSettingsIntent = Intent( | ||||||
|  |                         Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS | ||||||
|  |                     ) | ||||||
|  |                     startActivity(batteryOptimisationSettingsIntent) | ||||||
|  | 
 | ||||||
|  |                     // calling checkImageQuality after battery dialog is interacted with | ||||||
|  |                     // so that 2 dialogs do not pop up simultaneously | ||||||
|  |                     UploadMediaPresenter.isBatteryDialogShowing = false | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     UploadMediaPresenter.isBatteryDialogShowing = false | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |             defaultKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", true) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * If the permission for Location is turned on and certain | ||||||
|  |      * conditions are met, returns current location of the user. | ||||||
|  |      */ | ||||||
|  |     private fun handleLocation() { | ||||||
|  |         val locationPermissionsHelper = LocationPermissionsHelper( | ||||||
|  |             this, locationManager!!, null | ||||||
|  |         ) | ||||||
|  |         if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { | ||||||
|  |             currLocation = locationManager!!.getLastLocation() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (currLocation != null) { | ||||||
|  |             val locationDifference = getLocationDifference(currLocation!!, prevLocation) | ||||||
|  |             val isLocationTagUnchecked = isLocationTagUncheckedInTheSettings | ||||||
|  |             /* Remove location if the user has unchecked the Location EXIF tag in the | ||||||
|  |                        Manage EXIF Tags setting or turned "Record location for in-app shots" off. | ||||||
|  |                        Also, location information is discarded if the difference between | ||||||
|  |                        current location and location recorded just before capturing the image | ||||||
|  |                        is greater than 100 meters */ | ||||||
|  |             if (isLocationTagUnchecked || locationDifference > 100 || !defaultKvStore.getBoolean("inAppCameraLocationPref") | ||||||
|  |                 || !isInAppCameraUpload | ||||||
|  |             ) { | ||||||
|  |                 currLocation = null | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         private var uploadIsOfAPlace = false | ||||||
|  |         const val EXTRA_FILES: String = "commons_image_exta" | ||||||
|  |         const val LOCATION_BEFORE_IMAGE_CAPTURE: String = "user_location_before_image_capture" | ||||||
|  |         const val IN_APP_CAMERA_UPLOAD: String = "in_app_camera_upload" | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Stores all nearby places found and related users response for | ||||||
|  |          * each place while uploading media | ||||||
|  |          */ | ||||||
|  |         @JvmField | ||||||
|  |         var nearbyPopupAnswers: MutableMap<Place, Boolean>? = null | ||||||
|  | 
 | ||||||
|  |         const val keyForCurrentUploadImagesSize: String = "CurrentUploadImagesSize" | ||||||
|  |         const val storeNameForCurrentUploadImagesSize: String = "CurrentUploadImageQualities" | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Sets the flag indicating whether the upload is of a specific place. | ||||||
|  |          * | ||||||
|  |          * @param uploadOfAPlace a boolean value indicating whether the upload is of place. | ||||||
|  |          */ | ||||||
|  |         @JvmStatic | ||||||
|  |         fun setUploadIsOfAPlace(uploadOfAPlace: Boolean) { | ||||||
|  |             uploadIsOfAPlace = uploadOfAPlace | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -8,7 +8,7 @@ import fr.free.nrw.commons.di.CommonsDaggerSupportFragment | ||||||
| abstract class UploadBaseFragment : CommonsDaggerSupportFragment() { | abstract class UploadBaseFragment : CommonsDaggerSupportFragment() { | ||||||
|     var callback: Callback? = null |     var callback: Callback? = null | ||||||
| 
 | 
 | ||||||
|     protected open fun onBecameVisible() = Unit |     open fun onBecameVisible() = Unit | ||||||
| 
 | 
 | ||||||
|     interface Callback { |     interface Callback { | ||||||
|         val totalNumberOfSteps: Int |         val totalNumberOfSteps: Int | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ package fr.free.nrw.commons.upload | ||||||
| 
 | 
 | ||||||
| import fr.free.nrw.commons.BasePresenter | import fr.free.nrw.commons.BasePresenter | ||||||
| import fr.free.nrw.commons.filepicker.UploadableFile | import fr.free.nrw.commons.filepicker.UploadableFile | ||||||
|  | import fr.free.nrw.commons.kvstore.BasicKvStore | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The contract using which the UplaodActivity would communicate with its presenter |  * The contract using which the UplaodActivity would communicate with its presenter | ||||||
|  | @ -73,5 +74,7 @@ interface UploadContract { | ||||||
|          * @param uploadItemIndex Index of next image, whose quality is to be checked |          * @param uploadItemIndex Index of next image, whose quality is to be checked | ||||||
|          */ |          */ | ||||||
|         fun checkImageQuality(uploadItemIndex: Int) |         fun checkImageQuality(uploadItemIndex: Int) | ||||||
|  | 
 | ||||||
|  |         fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,7 +5,6 @@ import fr.free.nrw.commons.Utils | ||||||
| import fr.free.nrw.commons.filepicker.MimeTypeMapWrapper.Companion.getExtensionFromMimeType | import fr.free.nrw.commons.filepicker.MimeTypeMapWrapper.Companion.getExtensionFromMimeType | ||||||
| import fr.free.nrw.commons.nearby.Place | import fr.free.nrw.commons.nearby.Place | ||||||
| import fr.free.nrw.commons.utils.ImageUtils | import fr.free.nrw.commons.utils.ImageUtils | ||||||
| import io.reactivex.subjects.BehaviorSubject |  | ||||||
| 
 | 
 | ||||||
| class UploadItem( | class UploadItem( | ||||||
|     var mediaUri: Uri?, |     var mediaUri: Uri?, | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ import kotlinx.parcelize.Parcelize | ||||||
|  * Holds a description of an item being uploaded by [UploadActivity] |  * Holds a description of an item being uploaded by [UploadActivity] | ||||||
|  */ |  */ | ||||||
| @Parcelize | @Parcelize | ||||||
| data class UploadMediaDetail constructor( | data class UploadMediaDetail( | ||||||
|     /** |     /** | ||||||
|      * The language code ie. "en" or "fr". |      * The language code ie. "en" or "fr". | ||||||
|      * @param languageCode The language code ie. "en" or "fr". |      * @param languageCode The language code ie. "en" or "fr". | ||||||
|  | @ -18,7 +18,7 @@ data class UploadMediaDetail constructor( | ||||||
|      * The description text for the item being uploaded. |      * The description text for the item being uploaded. | ||||||
|      * @param descriptionText The description text. |      * @param descriptionText The description text. | ||||||
|      */ |      */ | ||||||
|     var descriptionText: String = "", |     var descriptionText: String? = "", | ||||||
|     /** |     /** | ||||||
|      * The caption text for the item being uploaded. |      * The caption text for the item being uploaded. | ||||||
|      * @param captionText The caption text. |      * @param captionText The caption text. | ||||||
|  | @ -27,10 +27,10 @@ data class UploadMediaDetail constructor( | ||||||
| ) : Parcelable { | ) : Parcelable { | ||||||
|     fun javaCopy() = copy() |     fun javaCopy() = copy() | ||||||
| 
 | 
 | ||||||
|     constructor(place: Place) : this( |     constructor(place: Place?) : this( | ||||||
|         place.language, |         place?.language, | ||||||
|         place.longDescription, |         place?.longDescription, | ||||||
|         place.name, |         place?.name ?: "", | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -1,633 +0,0 @@ | ||||||
| package fr.free.nrw.commons.upload; |  | ||||||
| 
 |  | ||||||
| import android.app.Activity; |  | ||||||
| import android.app.Dialog; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.speech.RecognizerIntent; |  | ||||||
| import android.text.Editable; |  | ||||||
| import android.text.InputFilter; |  | ||||||
| import android.text.TextUtils; |  | ||||||
| import android.text.TextWatcher; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.View.OnClickListener; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.AdapterView; |  | ||||||
| import android.widget.AdapterView.OnItemClickListener; |  | ||||||
| import android.widget.Button; |  | ||||||
| import android.widget.EditText; |  | ||||||
| import android.widget.FrameLayout; |  | ||||||
| import android.widget.ImageView; |  | ||||||
| import android.widget.LinearLayout; |  | ||||||
| import android.widget.ListView; |  | ||||||
| import android.widget.TextView; |  | ||||||
| import androidx.activity.result.ActivityResultLauncher; |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.constraintlayout.widget.ConstraintLayout; |  | ||||||
| import androidx.fragment.app.Fragment; |  | ||||||
| import androidx.recyclerview.widget.RecyclerView; |  | ||||||
| import com.google.android.material.textfield.TextInputLayout; |  | ||||||
| import fr.free.nrw.commons.R; |  | ||||||
| import fr.free.nrw.commons.databinding.RowItemDescriptionBinding; |  | ||||||
| import fr.free.nrw.commons.recentlanguages.Language; |  | ||||||
| import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter; |  | ||||||
| import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao; |  | ||||||
| import fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText; |  | ||||||
| import fr.free.nrw.commons.utils.AbstractTextWatcher; |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.HashMap; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Objects; |  | ||||||
| import java.util.regex.Pattern; |  | ||||||
| import timber.log.Timber; |  | ||||||
| 
 |  | ||||||
| public class UploadMediaDetailAdapter extends |  | ||||||
|     RecyclerView.Adapter<UploadMediaDetailAdapter.ViewHolder> { |  | ||||||
| 
 |  | ||||||
|     RecentLanguagesDao recentLanguagesDao; |  | ||||||
| 
 |  | ||||||
|     private List<UploadMediaDetail> uploadMediaDetails; |  | ||||||
|     private Callback callback; |  | ||||||
|     private EventListener eventListener; |  | ||||||
| 
 |  | ||||||
|     private HashMap<Integer, String> selectedLanguages; |  | ||||||
|     private final String savedLanguageValue; |  | ||||||
|     private TextView recentLanguagesTextView; |  | ||||||
|     private View separator; |  | ||||||
|     private ListView languageHistoryListView; |  | ||||||
|     private int currentPosition; |  | ||||||
|     private Fragment fragment; |  | ||||||
|     private Activity activity; |  | ||||||
|     private final ActivityResultLauncher<Intent> voiceInputResultLauncher; |  | ||||||
|     private SelectedVoiceIcon selectedVoiceIcon; |  | ||||||
| 
 |  | ||||||
|     private RowItemDescriptionBinding binding; |  | ||||||
| 
 |  | ||||||
|     public UploadMediaDetailAdapter(Fragment fragment, String savedLanguageValue, |  | ||||||
|         RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher<Intent> voiceInputResultLauncher) { |  | ||||||
|         uploadMediaDetails = new ArrayList<>(); |  | ||||||
|         selectedLanguages = new HashMap<>(); |  | ||||||
|         this.savedLanguageValue = savedLanguageValue; |  | ||||||
|         this.recentLanguagesDao = recentLanguagesDao; |  | ||||||
|         this.fragment = fragment; |  | ||||||
|         this.voiceInputResultLauncher = voiceInputResultLauncher; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public UploadMediaDetailAdapter(Activity activity, final String savedLanguageValue, |  | ||||||
|         List<UploadMediaDetail> uploadMediaDetails, RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher<Intent> voiceInputResultLauncher) { |  | ||||||
|         this.uploadMediaDetails = uploadMediaDetails; |  | ||||||
|         selectedLanguages = new HashMap<>(); |  | ||||||
|         this.savedLanguageValue = savedLanguageValue; |  | ||||||
|         this.recentLanguagesDao = recentLanguagesDao; |  | ||||||
|         this.activity = activity; |  | ||||||
|         this.voiceInputResultLauncher = voiceInputResultLauncher; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setCallback(Callback callback) { |  | ||||||
|         this.callback = callback; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setEventListener(EventListener eventListener) { |  | ||||||
|         this.eventListener = eventListener; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setItems(List<UploadMediaDetail> uploadMediaDetails) { |  | ||||||
|         this.uploadMediaDetails = uploadMediaDetails; |  | ||||||
|         selectedLanguages = new HashMap<>(); |  | ||||||
|         notifyDataSetChanged(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public List<UploadMediaDetail> getItems() { |  | ||||||
|         return uploadMediaDetails; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @NonNull |  | ||||||
|     @Override |  | ||||||
|     public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { |  | ||||||
|         LayoutInflater inflater = LayoutInflater.from(parent.getContext()); |  | ||||||
|         binding = RowItemDescriptionBinding.inflate(inflater, parent, false); |  | ||||||
|         return new ViewHolder(binding.getRoot()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This is a workaround for a known bug by android here |  | ||||||
|      * https://issuetracker.google.com/issues/37095917 makes the edit text on second and subsequent |  | ||||||
|      * fragments inside an adapter receptive to long click for copy/paste options |  | ||||||
|      * |  | ||||||
|      * @param holder the view holder |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void onViewAttachedToWindow(@NonNull final ViewHolder holder) { |  | ||||||
|         super.onViewAttachedToWindow(holder); |  | ||||||
|         holder.captionItemEditText.setEnabled(false); |  | ||||||
|         holder.captionItemEditText.setEnabled(true); |  | ||||||
|         holder.descItemEditText.setEnabled(false); |  | ||||||
|         holder.descItemEditText.setEnabled(true); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onBindViewHolder(@NonNull ViewHolder holder, int position) { |  | ||||||
|         holder.bind(position); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public int getItemCount() { |  | ||||||
|         return uploadMediaDetails.size(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void addDescription(UploadMediaDetail uploadMediaDetail) { |  | ||||||
|         selectedLanguages.put(uploadMediaDetails.size(), "en"); |  | ||||||
|         this.uploadMediaDetails.add(uploadMediaDetail); |  | ||||||
|         notifyItemInserted(uploadMediaDetails.size()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void startSpeechInput(String locale) { |  | ||||||
|         Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); |  | ||||||
|         intent.putExtra( |  | ||||||
|             RecognizerIntent.EXTRA_LANGUAGE_MODEL, |  | ||||||
|             RecognizerIntent.LANGUAGE_MODEL_FREE_FORM |  | ||||||
|         ); |  | ||||||
|         intent.putExtra( |  | ||||||
|             RecognizerIntent.EXTRA_LANGUAGE, |  | ||||||
|             locale |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         try { |  | ||||||
|             voiceInputResultLauncher.launch(intent); |  | ||||||
|         } catch (Exception e) { |  | ||||||
|             Timber.e(e.getMessage()); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Handles the result of the speech input by processing the spoken text. |  | ||||||
|      * If the spoken text is not empty, it capitalizes the first letter of the spoken text |  | ||||||
|      * and updates the appropriate field (caption or description) of the current |  | ||||||
|      * UploadMediaDetail based on the selected voice icon. |  | ||||||
|      * Finally, it notifies the adapter that the data set has changed. |  | ||||||
|      * |  | ||||||
|      * @param spokenText the text input received from speech recognition. |  | ||||||
|      */ |  | ||||||
|     public void handleSpeechResult(String spokenText) { |  | ||||||
|         if (!spokenText.isEmpty()) { |  | ||||||
|             String spokenTextCapitalized = |  | ||||||
|                 spokenText.substring(0, 1).toUpperCase() + spokenText.substring(1); |  | ||||||
|             if (currentPosition < uploadMediaDetails.size()) { |  | ||||||
|                 UploadMediaDetail uploadMediaDetail = uploadMediaDetails.get(currentPosition); |  | ||||||
|                 switch (selectedVoiceIcon) { |  | ||||||
|                     case CAPTION: |  | ||||||
|                         uploadMediaDetail.setCaptionText(spokenTextCapitalized); |  | ||||||
|                         break; |  | ||||||
|                     case DESCRIPTION: |  | ||||||
|                         uploadMediaDetail.setDescriptionText(spokenTextCapitalized); |  | ||||||
|                         break; |  | ||||||
|                 } |  | ||||||
|                 notifyDataSetChanged(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Remove description based on position from the list and notifies the RecyclerView Adapter that |  | ||||||
|      * data in adapter has been removed at that particular position. |  | ||||||
|      * |  | ||||||
|      * @param uploadMediaDetail |  | ||||||
|      * @param position |  | ||||||
|      */ |  | ||||||
|     public void removeDescription(final UploadMediaDetail uploadMediaDetail, final int position) { |  | ||||||
|         selectedLanguages.remove(position); |  | ||||||
|         this.uploadMediaDetails.remove(uploadMediaDetail); |  | ||||||
|         int i = position + 1; |  | ||||||
|         while (selectedLanguages.containsKey(i)) { |  | ||||||
|             selectedLanguages.remove(i); |  | ||||||
|             i++; |  | ||||||
|         } |  | ||||||
|         notifyItemRemoved(position); |  | ||||||
|         notifyItemRangeChanged(position, uploadMediaDetails.size() - position); |  | ||||||
|         updateAddButtonVisibility(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public class ViewHolder extends RecyclerView.ViewHolder { |  | ||||||
| 
 |  | ||||||
|         TextView descriptionLanguages ; |  | ||||||
| 
 |  | ||||||
|         PasteSensitiveTextInputEditText descItemEditText; |  | ||||||
| 
 |  | ||||||
|         TextInputLayout descInputLayout; |  | ||||||
| 
 |  | ||||||
|         PasteSensitiveTextInputEditText captionItemEditText; |  | ||||||
| 
 |  | ||||||
|         TextInputLayout captionInputLayout; |  | ||||||
| 
 |  | ||||||
|         ImageView removeButton; |  | ||||||
| 
 |  | ||||||
|         ImageView addButton; |  | ||||||
| 
 |  | ||||||
|         ConstraintLayout clParent; |  | ||||||
| 
 |  | ||||||
|         LinearLayout betterCaptionLinearLayout; |  | ||||||
| 
 |  | ||||||
|         LinearLayout betterDescriptionLinearLayout; |  | ||||||
| 
 |  | ||||||
|         private |  | ||||||
| 
 |  | ||||||
|         AbstractTextWatcher captionListener; |  | ||||||
| 
 |  | ||||||
|         AbstractTextWatcher descriptionListener; |  | ||||||
| 
 |  | ||||||
|         public ViewHolder(View itemView) { |  | ||||||
|             super(itemView); |  | ||||||
|             Timber.i("descItemEditText:" + descItemEditText); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         public void bind(int position) { |  | ||||||
|             UploadMediaDetail uploadMediaDetail = uploadMediaDetails.get(position); |  | ||||||
|             Timber.d("UploadMediaDetail is " + uploadMediaDetail); |  | ||||||
| 
 |  | ||||||
|             descriptionLanguages = binding.descriptionLanguages; |  | ||||||
|             descItemEditText = binding.descriptionItemEditText; |  | ||||||
|             descInputLayout = binding.descriptionItemEditTextInputLayout; |  | ||||||
|             captionItemEditText = binding.captionItemEditText; |  | ||||||
|             captionInputLayout = binding.captionItemEditTextInputLayout; |  | ||||||
|             removeButton = binding.btnRemove; |  | ||||||
|             addButton = binding.btnAdd; |  | ||||||
|             clParent = binding.clParent; |  | ||||||
|             betterCaptionLinearLayout = binding.llWriteBetterCaption; |  | ||||||
|             betterDescriptionLinearLayout = binding.llWriteBetterDescription; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             descriptionLanguages.setFocusable(false); |  | ||||||
|             captionItemEditText.addTextChangedListener(new AbstractTextWatcher( |  | ||||||
|                 value -> { |  | ||||||
|                     if (position == 0) { |  | ||||||
|                         eventListener.onPrimaryCaptionTextChange(value.length() != 0); |  | ||||||
|                     } |  | ||||||
|                 })); |  | ||||||
|             captionItemEditText.removeTextChangedListener(captionListener); |  | ||||||
|             descItemEditText.removeTextChangedListener(descriptionListener); |  | ||||||
|             captionItemEditText.setText(uploadMediaDetail.getCaptionText()); |  | ||||||
|             descItemEditText.setText(uploadMediaDetail.getDescriptionText()); |  | ||||||
|             captionInputLayout.setEndIconMode(TextInputLayout.END_ICON_CUSTOM); |  | ||||||
|             captionInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice); |  | ||||||
|             captionInputLayout.setEndIconOnClickListener(v -> { |  | ||||||
|                 currentPosition = position; |  | ||||||
|                 selectedVoiceIcon = SelectedVoiceIcon.CAPTION; |  | ||||||
|                 startSpeechInput(descriptionLanguages.getText().toString()); |  | ||||||
|             }); |  | ||||||
|             descInputLayout.setEndIconMode(TextInputLayout.END_ICON_CUSTOM); |  | ||||||
|             descInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice); |  | ||||||
|             descInputLayout.setEndIconOnClickListener(v -> { |  | ||||||
|                 currentPosition = position; |  | ||||||
|                 selectedVoiceIcon = SelectedVoiceIcon.DESCRIPTION; |  | ||||||
|                 startSpeechInput(descriptionLanguages.getText().toString()); |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|             if (position == 0) { |  | ||||||
|                 removeButton.setVisibility(View.GONE); |  | ||||||
|                 betterCaptionLinearLayout.setVisibility(View.VISIBLE); |  | ||||||
|                 betterCaptionLinearLayout.setOnClickListener( |  | ||||||
|                     v -> callback.showAlert(R.string.media_detail_caption, R.string.caption_info)); |  | ||||||
|                 betterDescriptionLinearLayout.setVisibility(View.VISIBLE); |  | ||||||
|                 betterDescriptionLinearLayout.setOnClickListener( |  | ||||||
|                     v -> callback.showAlert(R.string.media_detail_description, |  | ||||||
|                         R.string.description_info)); |  | ||||||
|                 Objects.requireNonNull(captionInputLayout.getEditText()) |  | ||||||
|                     .setFilters(new InputFilter[]{ |  | ||||||
|                         new UploadMediaDetailInputFilter() |  | ||||||
|                     }); |  | ||||||
|             } else { |  | ||||||
|                 removeButton.setVisibility(View.VISIBLE); |  | ||||||
|                 betterCaptionLinearLayout.setVisibility(View.GONE); |  | ||||||
|                 betterDescriptionLinearLayout.setVisibility(View.GONE); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             removeButton.setOnClickListener(v -> removeDescription(uploadMediaDetail, position)); |  | ||||||
|             captionListener = new AbstractTextWatcher( |  | ||||||
|                 captionText -> uploadMediaDetail.setCaptionText( |  | ||||||
|                     convertIdeographicSpaceToLatinSpace(captionText.strip())) |  | ||||||
|             ); |  | ||||||
|             descriptionListener = new AbstractTextWatcher( |  | ||||||
|                 descriptionText -> uploadMediaDetail.setDescriptionText(descriptionText)); |  | ||||||
|             captionItemEditText.addTextChangedListener(captionListener); |  | ||||||
|             initLanguage(position, uploadMediaDetail); |  | ||||||
| 
 |  | ||||||
|             descItemEditText.addTextChangedListener(descriptionListener); |  | ||||||
|             initLanguage(position, uploadMediaDetail); |  | ||||||
| 
 |  | ||||||
|             if (fragment != null) { |  | ||||||
|                 FrameLayout.LayoutParams newLayoutParams = (FrameLayout.LayoutParams) clParent.getLayoutParams(); |  | ||||||
|                 newLayoutParams.topMargin = 0; |  | ||||||
|                 newLayoutParams.leftMargin = 0; |  | ||||||
|                 newLayoutParams.rightMargin = 0; |  | ||||||
|                 newLayoutParams.bottomMargin = 0; |  | ||||||
|                 clParent.setLayoutParams(newLayoutParams); |  | ||||||
|             } |  | ||||||
|             updateAddButtonVisibility(); |  | ||||||
|             addButton.setOnClickListener(v -> eventListener.addLanguage()); |  | ||||||
| 
 |  | ||||||
|             //If the description was manually added by the user, it deserves focus, if not, let the user decide |  | ||||||
|             if (uploadMediaDetail.isManuallyAdded()) { |  | ||||||
|                 captionItemEditText.requestFocus(); |  | ||||||
|             } else { |  | ||||||
|                 captionItemEditText.clearFocus(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         private void initLanguage(int position, UploadMediaDetail description) { |  | ||||||
| 
 |  | ||||||
|             final List<Language> recentLanguages = recentLanguagesDao.getRecentLanguages(); |  | ||||||
| 
 |  | ||||||
|             LanguagesAdapter languagesAdapter = new LanguagesAdapter( |  | ||||||
|                 descriptionLanguages.getContext(), |  | ||||||
|                 selectedLanguages |  | ||||||
|             ); |  | ||||||
| 
 |  | ||||||
|             descriptionLanguages.setOnClickListener(new OnClickListener() { |  | ||||||
|                 @Override |  | ||||||
|                 public void onClick(View view) { |  | ||||||
|                     Dialog dialog = new Dialog(view.getContext()); |  | ||||||
|                     dialog.setContentView(R.layout.dialog_select_language); |  | ||||||
|                     dialog.setCancelable(false); |  | ||||||
|                     dialog.getWindow().setLayout( |  | ||||||
|                         (int) (view.getContext().getResources().getDisplayMetrics().widthPixels |  | ||||||
|                             * 0.90), |  | ||||||
|                         (int) (view.getContext().getResources().getDisplayMetrics().heightPixels |  | ||||||
|                             * 0.90)); |  | ||||||
|                     dialog.show(); |  | ||||||
| 
 |  | ||||||
|                     EditText editText = dialog.findViewById(R.id.search_language); |  | ||||||
|                     ListView listView = dialog.findViewById(R.id.language_list); |  | ||||||
|                     final Button cancelButton = dialog.findViewById(R.id.cancel_button); |  | ||||||
|                     languageHistoryListView = dialog.findViewById(R.id.language_history_list); |  | ||||||
|                     recentLanguagesTextView = dialog.findViewById(R.id.recent_searches); |  | ||||||
|                     separator = dialog.findViewById(R.id.separator); |  | ||||||
|                     setUpRecentLanguagesSection(recentLanguages); |  | ||||||
| 
 |  | ||||||
|                     listView.setAdapter(languagesAdapter); |  | ||||||
| 
 |  | ||||||
|                     cancelButton.setOnClickListener(v -> dialog.dismiss()); |  | ||||||
| 
 |  | ||||||
|                     editText.addTextChangedListener(new TextWatcher() { |  | ||||||
|                         @Override |  | ||||||
|                         public void beforeTextChanged(CharSequence charSequence, int i, int i1, |  | ||||||
|                             int i2) { |  | ||||||
|                             hideRecentLanguagesSection(); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         @Override |  | ||||||
|                         public void onTextChanged(CharSequence charSequence, int i, int i1, |  | ||||||
|                             int i2) { |  | ||||||
|                             languagesAdapter.getFilter().filter(charSequence); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         @Override |  | ||||||
|                         public void afterTextChanged(Editable editable) { |  | ||||||
| 
 |  | ||||||
|                         } |  | ||||||
|                     }); |  | ||||||
| 
 |  | ||||||
|                     languageHistoryListView.setOnItemClickListener( |  | ||||||
|                         (adapterView, view1, position, id) -> { |  | ||||||
|                             onRecentLanguageClicked(dialog, adapterView, position, description); |  | ||||||
|                         }); |  | ||||||
| 
 |  | ||||||
|                     listView.setOnItemClickListener(new OnItemClickListener() { |  | ||||||
|                         @Override |  | ||||||
|                         public void onItemClick(AdapterView<?> adapterView, View view, int i, |  | ||||||
|                             long l) { |  | ||||||
|                             description.setSelectedLanguageIndex(i); |  | ||||||
|                             String languageCode = ((LanguagesAdapter) adapterView.getAdapter()) |  | ||||||
|                                 .getLanguageCode(i); |  | ||||||
|                             description.setLanguageCode(languageCode); |  | ||||||
|                             final String languageName |  | ||||||
|                                 = ((LanguagesAdapter) adapterView.getAdapter()).getLanguageName(i); |  | ||||||
|                             final boolean isExists |  | ||||||
|                                 = recentLanguagesDao.findRecentLanguage(languageCode); |  | ||||||
|                             if (isExists) { |  | ||||||
|                                 recentLanguagesDao.deleteRecentLanguage(languageCode); |  | ||||||
|                             } |  | ||||||
|                             recentLanguagesDao |  | ||||||
|                                 .addRecentLanguage(new Language(languageName, languageCode)); |  | ||||||
| 
 |  | ||||||
|                             selectedLanguages.clear(); |  | ||||||
|                             selectedLanguages.put(position, languageCode); |  | ||||||
|                             ((LanguagesAdapter) adapterView |  | ||||||
|                                 .getAdapter()).setSelectedLangCode(languageCode); |  | ||||||
|                             Timber.d("Description language code is: " + languageCode); |  | ||||||
|                             descriptionLanguages.setText(languageCode); |  | ||||||
|                             dialog.dismiss(); |  | ||||||
|                         } |  | ||||||
|                     }); |  | ||||||
| 
 |  | ||||||
|                     dialog.setOnDismissListener( |  | ||||||
|                         dialogInterface -> languagesAdapter.getFilter().filter("")); |  | ||||||
| 
 |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|             if (description.getSelectedLanguageIndex() == -1) { |  | ||||||
|                 if (!TextUtils.isEmpty(savedLanguageValue)) { |  | ||||||
|                     // If user has chosen a default language from settings activity |  | ||||||
|                     // savedLanguageValue is not null |  | ||||||
|                     if (!TextUtils.isEmpty(description.getLanguageCode())) { |  | ||||||
|                         descriptionLanguages.setText(description.getLanguageCode()); |  | ||||||
|                         selectedLanguages.remove(position); |  | ||||||
|                         selectedLanguages.put(position, description.getLanguageCode()); |  | ||||||
|                     } else { |  | ||||||
|                         description.setLanguageCode(savedLanguageValue); |  | ||||||
|                         descriptionLanguages.setText(savedLanguageValue); |  | ||||||
|                         selectedLanguages.remove(position); |  | ||||||
|                         selectedLanguages.put(position, savedLanguageValue); |  | ||||||
|                     } |  | ||||||
|                 } else if (!TextUtils.isEmpty(description.getLanguageCode())) { |  | ||||||
|                     descriptionLanguages.setText(description.getLanguageCode()); |  | ||||||
|                     selectedLanguages.remove(position); |  | ||||||
|                     selectedLanguages.put(position, description.getLanguageCode()); |  | ||||||
|                 } else { |  | ||||||
|                     //Checking whether Language Code attribute is null or not. |  | ||||||
|                     if (uploadMediaDetails.get(position).getLanguageCode() != null) { |  | ||||||
|                         //If it is not null that means it is fetching details from the previous |  | ||||||
|                         // upload (i.e. when user has pressed copy previous caption & description) |  | ||||||
|                         //hence providing same language code for the current upload. |  | ||||||
|                         descriptionLanguages.setText(uploadMediaDetails.get(position) |  | ||||||
|                             .getLanguageCode()); |  | ||||||
|                         selectedLanguages.remove(position); |  | ||||||
|                         selectedLanguages.put(position, uploadMediaDetails.get(position) |  | ||||||
|                             .getLanguageCode()); |  | ||||||
|                     } else { |  | ||||||
|                         if (position == 0) { |  | ||||||
|                             final int defaultLocaleIndex = languagesAdapter |  | ||||||
|                                 .getIndexOfUserDefaultLocale(descriptionLanguages |  | ||||||
|                                     .getContext()); |  | ||||||
|                             descriptionLanguages |  | ||||||
|                                 .setText(languagesAdapter.getLanguageCode(defaultLocaleIndex)); |  | ||||||
|                             description.setLanguageCode( |  | ||||||
|                                 languagesAdapter.getLanguageCode(defaultLocaleIndex)); |  | ||||||
|                             selectedLanguages.remove(position); |  | ||||||
|                             selectedLanguages.put(position, |  | ||||||
|                                 languagesAdapter.getLanguageCode(defaultLocaleIndex)); |  | ||||||
|                         } else { |  | ||||||
|                             description.setLanguageCode(languagesAdapter.getLanguageCode(0)); |  | ||||||
|                             descriptionLanguages.setText(languagesAdapter.getLanguageCode(0)); |  | ||||||
|                             selectedLanguages.remove(position); |  | ||||||
|                             selectedLanguages.put(position, languagesAdapter.getLanguageCode(0)); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|             } else { |  | ||||||
|                 descriptionLanguages.setText(description.getLanguageCode()); |  | ||||||
|                 selectedLanguages.remove(position); |  | ||||||
|                 selectedLanguages.put(position, description.getLanguageCode()); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Handles click event for recent language section |  | ||||||
|          */ |  | ||||||
|         private void onRecentLanguageClicked(final Dialog dialog, final AdapterView<?> adapterView, |  | ||||||
|             final int position, final UploadMediaDetail description) { |  | ||||||
|             description.setSelectedLanguageIndex(position); |  | ||||||
|             final String languageCode = ((RecentLanguagesAdapter) adapterView.getAdapter()) |  | ||||||
|                 .getLanguageCode(position); |  | ||||||
|             description.setLanguageCode(languageCode); |  | ||||||
|             final String languageName = ((RecentLanguagesAdapter) adapterView.getAdapter()) |  | ||||||
|                 .getLanguageName(position); |  | ||||||
|             final boolean isExists = recentLanguagesDao.findRecentLanguage(languageCode); |  | ||||||
|             if (isExists) { |  | ||||||
|                 recentLanguagesDao.deleteRecentLanguage(languageCode); |  | ||||||
|             } |  | ||||||
|             recentLanguagesDao.addRecentLanguage(new Language(languageName, languageCode)); |  | ||||||
| 
 |  | ||||||
|             selectedLanguages.clear(); |  | ||||||
|             selectedLanguages.put(position, languageCode); |  | ||||||
|             ((RecentLanguagesAdapter) adapterView |  | ||||||
|                 .getAdapter()).setSelectedLangCode(languageCode); |  | ||||||
|             Timber.d("Description language code is: %s", languageCode); |  | ||||||
|             if (descriptionLanguages!=null) { |  | ||||||
|                 descriptionLanguages.setText(languageCode); |  | ||||||
|             } |  | ||||||
|             dialog.dismiss(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Hides recent languages section |  | ||||||
|          */ |  | ||||||
|         private void hideRecentLanguagesSection() { |  | ||||||
|             languageHistoryListView.setVisibility(View.GONE); |  | ||||||
|             recentLanguagesTextView.setVisibility(View.GONE); |  | ||||||
|             separator.setVisibility(View.GONE); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Set up recent languages section |  | ||||||
|          * |  | ||||||
|          * @param recentLanguages recently used languages |  | ||||||
|          */ |  | ||||||
|         private void setUpRecentLanguagesSection(final List<Language> recentLanguages) { |  | ||||||
|             if (recentLanguages.isEmpty()) { |  | ||||||
|                 languageHistoryListView.setVisibility(View.GONE); |  | ||||||
|                 recentLanguagesTextView.setVisibility(View.GONE); |  | ||||||
|                 separator.setVisibility(View.GONE); |  | ||||||
|             } else { |  | ||||||
|                 if (recentLanguages.size() > 5) { |  | ||||||
|                     for (int i = recentLanguages.size() - 1; i >= 5; i--) { |  | ||||||
|                         recentLanguagesDao.deleteRecentLanguage(recentLanguages.get(i) |  | ||||||
|                             .getLanguageCode()); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 languageHistoryListView.setVisibility(View.VISIBLE); |  | ||||||
|                 recentLanguagesTextView.setVisibility(View.VISIBLE); |  | ||||||
|                 separator.setVisibility(View.VISIBLE); |  | ||||||
| 
 |  | ||||||
|                 if (descriptionLanguages!=null) { |  | ||||||
|                     final RecentLanguagesAdapter recentLanguagesAdapter |  | ||||||
|                         = new RecentLanguagesAdapter( |  | ||||||
|                         descriptionLanguages.getContext(), |  | ||||||
|                         recentLanguagesDao.getRecentLanguages(), |  | ||||||
|                         selectedLanguages); |  | ||||||
|                     languageHistoryListView.setAdapter(recentLanguagesAdapter); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Convert Ideographic space to Latin space |  | ||||||
|          * |  | ||||||
|          * @param source the source text |  | ||||||
|          * @return a string with Latin spaces instead of Ideographic spaces |  | ||||||
|          */ |  | ||||||
|         public String convertIdeographicSpaceToLatinSpace(String source) { |  | ||||||
|             Pattern ideographicSpacePattern = Pattern.compile("\\x{3000}"); |  | ||||||
|             return ideographicSpacePattern.matcher(source).replaceAll(" "); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Hides the visibility of the "Add" button for all items in the RecyclerView except |  | ||||||
|      * the last item in RecyclerView |  | ||||||
|      */ |  | ||||||
|     private void updateAddButtonVisibility() { |  | ||||||
|         int lastItemPosition = getItemCount() - 1; |  | ||||||
|         // Hide Add Button for all items |  | ||||||
|         for (int i = 0; i < getItemCount(); i++) { |  | ||||||
|             if (fragment != null) { |  | ||||||
|                 if (fragment.getView() != null) { |  | ||||||
|                     ViewHolder holder = (ViewHolder) ((RecyclerView) fragment.getView() |  | ||||||
|                         .findViewById(R.id.rv_descriptions)).findViewHolderForAdapterPosition(i); |  | ||||||
|                     if (holder != null) { |  | ||||||
|                         holder.addButton.setVisibility(View.GONE); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 if (this.activity != null) { |  | ||||||
|                     ViewHolder holder = (ViewHolder) ((RecyclerView) activity.findViewById( |  | ||||||
|                         R.id.rv_descriptions_captions)).findViewHolderForAdapterPosition(i); |  | ||||||
|                     if (holder != null) { |  | ||||||
|                         holder.addButton.setVisibility(View.GONE); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Show Add Button for the last item |  | ||||||
|         if (fragment != null) { |  | ||||||
|             if (fragment.getView() != null) { |  | ||||||
|                 ViewHolder lastItemHolder = (ViewHolder) ((RecyclerView) fragment.getView() |  | ||||||
|                     .findViewById(R.id.rv_descriptions)).findViewHolderForAdapterPosition( |  | ||||||
|                     lastItemPosition); |  | ||||||
|                 if (lastItemHolder != null) { |  | ||||||
|                     lastItemHolder.addButton.setVisibility(View.VISIBLE); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             if (this.activity != null) { |  | ||||||
|                 ViewHolder lastItemHolder = (ViewHolder) ((RecyclerView) activity |  | ||||||
|                     .findViewById(R.id.rv_descriptions_captions)).findViewHolderForAdapterPosition( |  | ||||||
|                     lastItemPosition); |  | ||||||
|                 if (lastItemHolder != null) { |  | ||||||
|                     lastItemHolder.addButton.setVisibility(View.VISIBLE); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public interface Callback { |  | ||||||
| 
 |  | ||||||
|         void showAlert(int mediaDetailDescription, int descriptionInfo); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public interface EventListener { |  | ||||||
| 
 |  | ||||||
|         void onPrimaryCaptionTextChange(boolean isNotEmpty); |  | ||||||
| 
 |  | ||||||
|         void addLanguage(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     enum SelectedVoiceIcon { |  | ||||||
|         CAPTION, |  | ||||||
|         DESCRIPTION |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,563 @@ | ||||||
|  | package fr.free.nrw.commons.upload | ||||||
|  | 
 | ||||||
|  | import android.app.Activity | ||||||
|  | import android.app.Dialog | ||||||
|  | import android.content.Intent | ||||||
|  | import android.speech.RecognizerIntent.ACTION_RECOGNIZE_SPEECH | ||||||
|  | import android.speech.RecognizerIntent.EXTRA_LANGUAGE | ||||||
|  | import android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL | ||||||
|  | import android.speech.RecognizerIntent.LANGUAGE_MODEL_FREE_FORM | ||||||
|  | import android.text.Editable | ||||||
|  | import android.text.InputFilter | ||||||
|  | import android.text.TextUtils | ||||||
|  | import android.text.TextWatcher | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.AdapterView | ||||||
|  | import android.widget.AdapterView.OnItemClickListener | ||||||
|  | import android.widget.EditText | ||||||
|  | import android.widget.FrameLayout | ||||||
|  | import android.widget.ImageView | ||||||
|  | import android.widget.LinearLayout | ||||||
|  | import android.widget.ListView | ||||||
|  | import android.widget.TextView | ||||||
|  | import androidx.activity.result.ActivityResultLauncher | ||||||
|  | import androidx.constraintlayout.widget.ConstraintLayout | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import com.google.android.material.textfield.TextInputLayout | ||||||
|  | import fr.free.nrw.commons.R | ||||||
|  | import fr.free.nrw.commons.databinding.RowItemDescriptionBinding | ||||||
|  | import fr.free.nrw.commons.recentlanguages.Language | ||||||
|  | import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter | ||||||
|  | import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao | ||||||
|  | import fr.free.nrw.commons.utils.AbstractTextWatcher | ||||||
|  | import timber.log.Timber | ||||||
|  | import java.util.Locale | ||||||
|  | import java.util.regex.Pattern | ||||||
|  | 
 | ||||||
|  | class UploadMediaDetailAdapter : RecyclerView.Adapter<UploadMediaDetailAdapter.ViewHolder> { | ||||||
|  |     private var uploadMediaDetails: MutableList<UploadMediaDetail> | ||||||
|  |     private var selectedLanguages: MutableMap<Int, String> | ||||||
|  |     private val savedLanguageValue: String | ||||||
|  |     private var recentLanguagesTextView: TextView? = null | ||||||
|  |     private var separator: View? = null | ||||||
|  |     private var languageHistoryListView: ListView? = null | ||||||
|  |     private var currentPosition = 0 | ||||||
|  |     private var fragment: Fragment? = null | ||||||
|  |     private var activity: Activity? = null | ||||||
|  |     private val voiceInputResultLauncher: ActivityResultLauncher<Intent> | ||||||
|  |     private var selectedVoiceIcon: SelectedVoiceIcon? = null | ||||||
|  |     var recentLanguagesDao: RecentLanguagesDao | ||||||
|  |     var callback: Callback? = null | ||||||
|  |     var eventListener: EventListener? = null | ||||||
|  |     var items: List<UploadMediaDetail> | ||||||
|  |         get() = uploadMediaDetails | ||||||
|  |         set(value) { | ||||||
|  |             uploadMediaDetails = value.toMutableList() | ||||||
|  |             selectedLanguages = mutableMapOf() | ||||||
|  |             notifyDataSetChanged() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         fragment: Fragment?, | ||||||
|  |         savedLanguageValue: String, | ||||||
|  |         recentLanguagesDao: RecentLanguagesDao, | ||||||
|  |         voiceInputResultLauncher: ActivityResultLauncher<Intent> | ||||||
|  |     ) { | ||||||
|  |         uploadMediaDetails = ArrayList() | ||||||
|  |         selectedLanguages = mutableMapOf() | ||||||
|  |         this.savedLanguageValue = savedLanguageValue | ||||||
|  |         this.recentLanguagesDao = recentLanguagesDao | ||||||
|  |         this.fragment = fragment | ||||||
|  |         this.voiceInputResultLauncher = voiceInputResultLauncher | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         activity: Activity?, | ||||||
|  |         savedLanguageValue: String, | ||||||
|  |         uploadMediaDetails: MutableList<UploadMediaDetail>, | ||||||
|  |         recentLanguagesDao: RecentLanguagesDao, | ||||||
|  |         voiceInputResultLauncher: ActivityResultLauncher<Intent> | ||||||
|  |     ) { | ||||||
|  |         this.uploadMediaDetails = uploadMediaDetails | ||||||
|  |         selectedLanguages = HashMap() | ||||||
|  |         this.savedLanguageValue = savedLanguageValue | ||||||
|  |         this.recentLanguagesDao = recentLanguagesDao | ||||||
|  |         this.activity = activity | ||||||
|  |         this.voiceInputResultLauncher = voiceInputResultLauncher | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { | ||||||
|  |         val inflater = LayoutInflater.from(parent.context) | ||||||
|  |         return ViewHolder(RowItemDescriptionBinding.inflate(inflater, parent, false)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This is a workaround for a known bug by android here | ||||||
|  |      * https://issuetracker.google.com/issues/37095917 makes the edit text on second and subsequent | ||||||
|  |      * fragments inside an adapter receptive to long click for copy/paste options | ||||||
|  |      * | ||||||
|  |      * @param holder the view holder | ||||||
|  |      */ | ||||||
|  |     override fun onViewAttachedToWindow(holder: ViewHolder) { | ||||||
|  |         super.onViewAttachedToWindow(holder) | ||||||
|  |         holder.binding.captionItemEditText.isEnabled = false | ||||||
|  |         holder.binding.captionItemEditText.isEnabled = true | ||||||
|  |         holder.binding.descriptionItemEditText.isEnabled = false | ||||||
|  |         holder.binding.descriptionItemEditText.isEnabled = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||||||
|  |         holder.bind(position) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getItemCount(): Int { | ||||||
|  |         return uploadMediaDetails.size | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun addDescription(uploadMediaDetail: UploadMediaDetail) { | ||||||
|  |         selectedLanguages[uploadMediaDetails.size] = "en" | ||||||
|  |         uploadMediaDetails.add(uploadMediaDetail) | ||||||
|  |         notifyItemInserted(uploadMediaDetails.size) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun startSpeechInput(locale: String) { | ||||||
|  |         try { | ||||||
|  |             voiceInputResultLauncher.launch(Intent(ACTION_RECOGNIZE_SPEECH).apply { | ||||||
|  |                 putExtra(EXTRA_LANGUAGE_MODEL, LANGUAGE_MODEL_FREE_FORM) | ||||||
|  |                 putExtra(EXTRA_LANGUAGE, locale) | ||||||
|  |             }) | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             Timber.e(e) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles the result of the speech input by processing the spoken text. | ||||||
|  |      * If the spoken text is not empty, it capitalizes the first letter of the spoken text | ||||||
|  |      * and updates the appropriate field (caption or description) of the current | ||||||
|  |      * UploadMediaDetail based on the selected voice icon. | ||||||
|  |      * Finally, it notifies the adapter that the data set has changed. | ||||||
|  |      * | ||||||
|  |      * @param spokenText the text input received from speech recognition. | ||||||
|  |      */ | ||||||
|  |     fun handleSpeechResult(spokenText: String) { | ||||||
|  |         if (spokenText.isNotEmpty()) { | ||||||
|  |             val spokenTextCapitalized = | ||||||
|  |                 spokenText.substring(0, 1).uppercase(Locale.getDefault()) + spokenText.substring(1) | ||||||
|  |             if (currentPosition < uploadMediaDetails.size) { | ||||||
|  |                 val uploadMediaDetail = uploadMediaDetails[currentPosition] | ||||||
|  |                 when (selectedVoiceIcon) { | ||||||
|  |                     SelectedVoiceIcon.CAPTION -> uploadMediaDetail.captionText = | ||||||
|  |                         spokenTextCapitalized | ||||||
|  | 
 | ||||||
|  |                     SelectedVoiceIcon.DESCRIPTION -> uploadMediaDetail.descriptionText = | ||||||
|  |                         spokenTextCapitalized | ||||||
|  | 
 | ||||||
|  |                     null -> {} | ||||||
|  |                 } | ||||||
|  |                 notifyDataSetChanged() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Remove description based on position from the list and notifies the RecyclerView Adapter that | ||||||
|  |      * data in adapter has been removed at that particular position. | ||||||
|  |      */ | ||||||
|  |     fun removeDescription(uploadMediaDetail: UploadMediaDetail, position: Int) { | ||||||
|  |         selectedLanguages.remove(position) | ||||||
|  |         uploadMediaDetails.remove(uploadMediaDetail) | ||||||
|  |         var i = position + 1 | ||||||
|  |         while (selectedLanguages.containsKey(i)) { | ||||||
|  |             selectedLanguages.remove(i) | ||||||
|  |             i++ | ||||||
|  |         } | ||||||
|  |         notifyItemRemoved(position) | ||||||
|  |         notifyItemRangeChanged(position, uploadMediaDetails.size - position) | ||||||
|  |         updateAddButtonVisibility() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     inner class ViewHolder(val binding: RowItemDescriptionBinding) : | ||||||
|  |         RecyclerView.ViewHolder(binding.root) { | ||||||
|  | 
 | ||||||
|  |         var addButton: ImageView? = null | ||||||
|  | 
 | ||||||
|  |         var clParent: ConstraintLayout? = null | ||||||
|  | 
 | ||||||
|  |         var betterCaptionLinearLayout: LinearLayout? = null | ||||||
|  | 
 | ||||||
|  |         var betterDescriptionLinearLayout: LinearLayout? = null | ||||||
|  | 
 | ||||||
|  |         private var captionListener: AbstractTextWatcher? = null | ||||||
|  | 
 | ||||||
|  |         var descriptionListener: AbstractTextWatcher? = null | ||||||
|  | 
 | ||||||
|  |         fun bind(position: Int) { | ||||||
|  |             val uploadMediaDetail = uploadMediaDetails[position] | ||||||
|  |             Timber.d("UploadMediaDetail is %s", uploadMediaDetail) | ||||||
|  | 
 | ||||||
|  |             addButton = binding.btnAdd | ||||||
|  |             clParent = binding.clParent | ||||||
|  |             betterCaptionLinearLayout = binding.llWriteBetterCaption | ||||||
|  |             betterDescriptionLinearLayout = binding.llWriteBetterDescription | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             binding.descriptionLanguages.isFocusable = false | ||||||
|  |             binding.captionItemEditText.addTextChangedListener(AbstractTextWatcher { value: String -> | ||||||
|  |                 if (position == 0) { | ||||||
|  |                     eventListener!!.onPrimaryCaptionTextChange(value.length != 0) | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |             binding.captionItemEditText.removeTextChangedListener(captionListener) | ||||||
|  |             binding.descriptionItemEditText.removeTextChangedListener(descriptionListener) | ||||||
|  |             binding.captionItemEditText.setText(uploadMediaDetail.captionText) | ||||||
|  |             binding.descriptionItemEditText.setText(uploadMediaDetail.descriptionText) | ||||||
|  |             binding.captionItemEditTextInputLayout.endIconMode = TextInputLayout.END_ICON_CUSTOM | ||||||
|  |             binding.captionItemEditTextInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice) | ||||||
|  |             binding.captionItemEditTextInputLayout.setEndIconOnClickListener { v: View? -> | ||||||
|  |                 currentPosition = position | ||||||
|  |                 selectedVoiceIcon = SelectedVoiceIcon.CAPTION | ||||||
|  |                 startSpeechInput(binding.descriptionLanguages.text.toString()) | ||||||
|  |             } | ||||||
|  |             binding.descriptionItemEditTextInputLayout.endIconMode = TextInputLayout.END_ICON_CUSTOM | ||||||
|  |             binding.descriptionItemEditTextInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice) | ||||||
|  |             binding.descriptionItemEditTextInputLayout.setEndIconOnClickListener { v: View? -> | ||||||
|  |                 currentPosition = position | ||||||
|  |                 selectedVoiceIcon = SelectedVoiceIcon.DESCRIPTION | ||||||
|  |                 startSpeechInput(binding.descriptionLanguages.text.toString()) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (position == 0) { | ||||||
|  |                 binding.btnRemove.visibility = View.GONE | ||||||
|  |                 betterCaptionLinearLayout!!.visibility = View.VISIBLE | ||||||
|  |                 betterCaptionLinearLayout!!.setOnClickListener { v: View? -> | ||||||
|  |                     callback!!.showAlert( | ||||||
|  |                         R.string.media_detail_caption, | ||||||
|  |                         R.string.caption_info | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |                 betterDescriptionLinearLayout!!.visibility = View.VISIBLE | ||||||
|  |                 betterDescriptionLinearLayout!!.setOnClickListener { v: View? -> | ||||||
|  |                     callback!!.showAlert( | ||||||
|  |                         R.string.media_detail_description, | ||||||
|  |                         R.string.description_info | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 binding.captionItemEditTextInputLayout.editText?.let { | ||||||
|  |                     it.filters = arrayOf<InputFilter>(UploadMediaDetailInputFilter()) | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 binding.btnRemove.visibility = View.VISIBLE | ||||||
|  |                 betterCaptionLinearLayout!!.visibility = View.GONE | ||||||
|  |                 betterDescriptionLinearLayout!!.visibility = View.GONE | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             binding.btnRemove.setOnClickListener { v: View? -> | ||||||
|  |                 removeDescription( | ||||||
|  |                     uploadMediaDetail, | ||||||
|  |                     position | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |             captionListener = AbstractTextWatcher { captionText: String -> | ||||||
|  |                 uploadMediaDetail.captionText = | ||||||
|  |                     convertIdeographicSpaceToLatinSpace(captionText.trim()) | ||||||
|  |             } | ||||||
|  |             descriptionListener = AbstractTextWatcher { value: String? -> | ||||||
|  |                 uploadMediaDetail.descriptionText = value | ||||||
|  |             } | ||||||
|  |             binding.captionItemEditText.addTextChangedListener(captionListener) | ||||||
|  |             initLanguage(position, uploadMediaDetail) | ||||||
|  | 
 | ||||||
|  |             binding.descriptionItemEditText.addTextChangedListener(descriptionListener) | ||||||
|  |             initLanguage(position, uploadMediaDetail) | ||||||
|  | 
 | ||||||
|  |             if (fragment != null) { | ||||||
|  |                 val newLayoutParams = clParent!!.layoutParams as FrameLayout.LayoutParams | ||||||
|  |                 newLayoutParams.topMargin = 0 | ||||||
|  |                 newLayoutParams.leftMargin = 0 | ||||||
|  |                 newLayoutParams.rightMargin = 0 | ||||||
|  |                 newLayoutParams.bottomMargin = 0 | ||||||
|  |                 clParent!!.layoutParams = newLayoutParams | ||||||
|  |             } | ||||||
|  |             updateAddButtonVisibility() | ||||||
|  |             addButton!!.setOnClickListener { v: View? -> eventListener!!.addLanguage() } | ||||||
|  | 
 | ||||||
|  |             //If the description was manually added by the user, it deserves focus, if not, let the user decide | ||||||
|  |             if (uploadMediaDetail.isManuallyAdded) { | ||||||
|  |                 binding.captionItemEditText.requestFocus() | ||||||
|  |             } else { | ||||||
|  |                 binding.captionItemEditText.clearFocus() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         private fun initLanguage(position: Int, description: UploadMediaDetail) { | ||||||
|  |             val recentLanguages = recentLanguagesDao.getRecentLanguages() | ||||||
|  | 
 | ||||||
|  |             val languagesAdapter = LanguagesAdapter( | ||||||
|  |                 binding.descriptionLanguages.context, | ||||||
|  |                 selectedLanguages | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             binding.descriptionLanguages.setOnClickListener { view -> | ||||||
|  |                 val dialog = Dialog(view.context) | ||||||
|  |                 dialog.setContentView(R.layout.dialog_select_language) | ||||||
|  |                 dialog.setCancelable(false) | ||||||
|  |                 dialog.window!!.setLayout( | ||||||
|  |                     (view.context.resources.displayMetrics.widthPixels | ||||||
|  |                             * 0.90).toInt(), | ||||||
|  |                     (view.context.resources.displayMetrics.heightPixels | ||||||
|  |                             * 0.90).toInt() | ||||||
|  |                 ) | ||||||
|  |                 dialog.show() | ||||||
|  | 
 | ||||||
|  |                 val editText = | ||||||
|  |                     dialog.findViewById<EditText>(R.id.search_language) | ||||||
|  |                 val listView = | ||||||
|  |                     dialog.findViewById<ListView>(R.id.language_list) | ||||||
|  |                 languageHistoryListView = | ||||||
|  |                     dialog.findViewById(R.id.language_history_list) | ||||||
|  |                 recentLanguagesTextView = | ||||||
|  |                     dialog.findViewById(R.id.recent_searches) | ||||||
|  |                 separator = | ||||||
|  |                     dialog.findViewById(R.id.separator) | ||||||
|  |                 setUpRecentLanguagesSection(recentLanguages) | ||||||
|  | 
 | ||||||
|  |                 listView.adapter = languagesAdapter | ||||||
|  | 
 | ||||||
|  |                 editText.addTextChangedListener(object : TextWatcher { | ||||||
|  |                     override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) = | ||||||
|  |                         hideRecentLanguagesSection() | ||||||
|  | 
 | ||||||
|  |                     override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) { | ||||||
|  |                         languagesAdapter.filter.filter(charSequence) | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     override fun afterTextChanged(editable: Editable) = Unit | ||||||
|  |                 }) | ||||||
|  | 
 | ||||||
|  |                 languageHistoryListView?.setOnItemClickListener { adapterView: AdapterView<*>, view1: View?, position: Int, id: Long -> | ||||||
|  |                     onRecentLanguageClicked(dialog, adapterView, position, description) | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 listView.onItemClickListener = OnItemClickListener { adapterView, _, i, l -> | ||||||
|  |                         description.selectedLanguageIndex = i | ||||||
|  |                         val languageCode = (adapterView.adapter as LanguagesAdapter).getLanguageCode(i) | ||||||
|  |                         description.languageCode = languageCode | ||||||
|  |                         val languageName = (adapterView.adapter as LanguagesAdapter).getLanguageName(i) | ||||||
|  |                         val isExists = recentLanguagesDao.findRecentLanguage(languageCode) | ||||||
|  |                         if (isExists) { | ||||||
|  |                             recentLanguagesDao.deleteRecentLanguage(languageCode) | ||||||
|  |                         } | ||||||
|  |                         recentLanguagesDao.addRecentLanguage(Language(languageName, languageCode)) | ||||||
|  | 
 | ||||||
|  |                         selectedLanguages.clear() | ||||||
|  |                         selectedLanguages[position] = languageCode | ||||||
|  |                         (adapterView.adapter as LanguagesAdapter).selectedLangCode = languageCode | ||||||
|  |                         Timber.d("Description language code is: %s", languageCode) | ||||||
|  |                     binding.descriptionLanguages.text = languageCode | ||||||
|  |                         dialog.dismiss() | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                 dialog.setOnDismissListener { | ||||||
|  |                     languagesAdapter.filter.filter("") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (description.selectedLanguageIndex == -1) { | ||||||
|  |                 if (!TextUtils.isEmpty(savedLanguageValue)) { | ||||||
|  |                     // If user has chosen a default language from settings activity | ||||||
|  |                     // savedLanguageValue is not null | ||||||
|  |                     if (!TextUtils.isEmpty(description.languageCode)) { | ||||||
|  |                         binding.descriptionLanguages.text = description.languageCode | ||||||
|  |                         selectedLanguages.remove(position) | ||||||
|  |                         selectedLanguages[position] = description.languageCode!! | ||||||
|  |                     } else { | ||||||
|  |                         description.languageCode = savedLanguageValue | ||||||
|  |                         binding.descriptionLanguages.text = savedLanguageValue | ||||||
|  |                         selectedLanguages.remove(position) | ||||||
|  |                         selectedLanguages[position] = savedLanguageValue | ||||||
|  |                     } | ||||||
|  |                 } else if (!TextUtils.isEmpty(description.languageCode)) { | ||||||
|  |                     binding.descriptionLanguages.text = description.languageCode | ||||||
|  |                     selectedLanguages.remove(position) | ||||||
|  |                     selectedLanguages[position] = description.languageCode!! | ||||||
|  |                 } else { | ||||||
|  |                     //Checking whether Language Code attribute is null or not. | ||||||
|  |                     if (uploadMediaDetails[position].languageCode != null) { | ||||||
|  |                         //If it is not null that means it is fetching details from the previous | ||||||
|  |                         // upload (i.e. when user has pressed copy previous caption & description) | ||||||
|  |                         //hence providing same language code for the current upload. | ||||||
|  |                         binding.descriptionLanguages.text = uploadMediaDetails[position] | ||||||
|  |                             .languageCode | ||||||
|  |                         selectedLanguages.remove(position) | ||||||
|  |                         selectedLanguages[position] = uploadMediaDetails[position].languageCode!! | ||||||
|  |                     } else { | ||||||
|  |                         if (position == 0) { | ||||||
|  |                             val defaultLocaleIndex = languagesAdapter.getIndexOfUserDefaultLocale( | ||||||
|  |                                 binding.descriptionLanguages.getContext()) | ||||||
|  |                             binding.descriptionLanguages.setText(languagesAdapter.getLanguageCode(defaultLocaleIndex)) | ||||||
|  |                             description.languageCode = languagesAdapter.getLanguageCode(defaultLocaleIndex) | ||||||
|  |                             selectedLanguages.remove(position) | ||||||
|  |                             selectedLanguages[position] = | ||||||
|  |                                 languagesAdapter.getLanguageCode(defaultLocaleIndex) | ||||||
|  |                         } else { | ||||||
|  |                             description.languageCode = languagesAdapter.getLanguageCode(0) | ||||||
|  |                             binding.descriptionLanguages.text = languagesAdapter.getLanguageCode(0) | ||||||
|  |                             selectedLanguages.remove(position) | ||||||
|  |                             selectedLanguages[position] = languagesAdapter.getLanguageCode(0) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 binding.descriptionLanguages.text = description.languageCode | ||||||
|  |                 selectedLanguages.remove(position) | ||||||
|  |                 description.languageCode?.let { | ||||||
|  |                     selectedLanguages[position] = it | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Handles click event for recent language section | ||||||
|  |          */ | ||||||
|  |         private fun onRecentLanguageClicked( | ||||||
|  |             dialog: Dialog, adapterView: AdapterView<*>, | ||||||
|  |             position: Int, description: UploadMediaDetail | ||||||
|  |         ) { | ||||||
|  |             description.selectedLanguageIndex = position | ||||||
|  |             val languageCode = (adapterView.adapter as RecentLanguagesAdapter) | ||||||
|  |                 .getLanguageCode(position) | ||||||
|  |             description.languageCode = languageCode | ||||||
|  |             val languageName = (adapterView.adapter as RecentLanguagesAdapter) | ||||||
|  |                 .getLanguageName(position) | ||||||
|  |             val isExists = recentLanguagesDao.findRecentLanguage(languageCode) | ||||||
|  |             if (isExists) { | ||||||
|  |                 recentLanguagesDao.deleteRecentLanguage(languageCode) | ||||||
|  |             } | ||||||
|  |             recentLanguagesDao.addRecentLanguage(Language(languageName, languageCode)) | ||||||
|  | 
 | ||||||
|  |             selectedLanguages.clear() | ||||||
|  |             selectedLanguages[position] = languageCode | ||||||
|  |             (adapterView | ||||||
|  |                 .adapter as RecentLanguagesAdapter).selectedLangCode = languageCode | ||||||
|  |             Timber.d("Description language code is: %s", languageCode) | ||||||
|  |             binding.descriptionLanguages.text = languageCode | ||||||
|  |             dialog.dismiss() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Hides recent languages section | ||||||
|  |          */ | ||||||
|  |         private fun hideRecentLanguagesSection() { | ||||||
|  |             languageHistoryListView!!.visibility = View.GONE | ||||||
|  |             recentLanguagesTextView!!.visibility = View.GONE | ||||||
|  |             separator!!.visibility = View.GONE | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Set up recent languages section | ||||||
|  |          * | ||||||
|  |          * @param recentLanguages recently used languages | ||||||
|  |          */ | ||||||
|  |         private fun setUpRecentLanguagesSection(recentLanguages: List<Language>) { | ||||||
|  |             if (recentLanguages.isEmpty()) { | ||||||
|  |                 languageHistoryListView!!.visibility = View.GONE | ||||||
|  |                 recentLanguagesTextView!!.visibility = View.GONE | ||||||
|  |                 separator!!.visibility = View.GONE | ||||||
|  |             } else { | ||||||
|  |                 if (recentLanguages.size > 5) { | ||||||
|  |                     for (i in recentLanguages.size - 1 downTo 5) { | ||||||
|  |                         recentLanguagesDao.deleteRecentLanguage( | ||||||
|  |                             recentLanguages[i] | ||||||
|  |                                 .languageCode | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 languageHistoryListView!!.visibility = View.VISIBLE | ||||||
|  |                 recentLanguagesTextView!!.visibility = View.VISIBLE | ||||||
|  |                 separator!!.visibility = View.VISIBLE | ||||||
|  | 
 | ||||||
|  |                 val recentLanguagesAdapter = RecentLanguagesAdapter( | ||||||
|  |                     binding.descriptionLanguages.context, | ||||||
|  |                     recentLanguagesDao.getRecentLanguages(), | ||||||
|  |                     selectedLanguages | ||||||
|  |                 ) | ||||||
|  |                 languageHistoryListView!!.adapter = recentLanguagesAdapter | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Convert Ideographic space to Latin space | ||||||
|  |          * | ||||||
|  |          * @param source the source text | ||||||
|  |          * @return a string with Latin spaces instead of Ideographic spaces | ||||||
|  |          */ | ||||||
|  |         fun convertIdeographicSpaceToLatinSpace(source: String): String { | ||||||
|  |             val ideographicSpacePattern = Pattern.compile("\\x{3000}") | ||||||
|  |             return ideographicSpacePattern.matcher(source).replaceAll(" ") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Hides the visibility of the "Add" button for all items in the RecyclerView except | ||||||
|  |      * the last item in RecyclerView | ||||||
|  |      */ | ||||||
|  |     private fun updateAddButtonVisibility() { | ||||||
|  |         val lastItemPosition = itemCount - 1 | ||||||
|  |         // Hide Add Button for all items | ||||||
|  |         for (i in 0 until itemCount) { | ||||||
|  |             if (fragment != null) { | ||||||
|  |                 if (fragment!!.view != null) { | ||||||
|  |                     val holder = (fragment!!.requireView().findViewById<View>(R.id.rv_descriptions) as RecyclerView).findViewHolderForAdapterPosition(i) as ViewHolder? | ||||||
|  |                     if (holder != null) { | ||||||
|  |                         holder.addButton!!.visibility = View.GONE | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 if (activity != null) { | ||||||
|  |                     val holder = (activity!!.findViewById<View>(R.id.rv_descriptions_captions) as RecyclerView).findViewHolderForAdapterPosition(i) as ViewHolder? | ||||||
|  |                     if (holder != null) { | ||||||
|  |                         holder.addButton!!.visibility = View.GONE | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Show Add Button for the last item | ||||||
|  |         if (fragment != null) { | ||||||
|  |             if (fragment!!.view != null) { | ||||||
|  |                 val lastItemHolder = (fragment!!.requireView().findViewById<View>(R.id.rv_descriptions) as RecyclerView).findViewHolderForAdapterPosition(lastItemPosition) as ViewHolder? | ||||||
|  |                 if (lastItemHolder != null) { | ||||||
|  |                     lastItemHolder.addButton!!.visibility = View.VISIBLE | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             if (activity != null) { | ||||||
|  |                 val lastItemHolder = (activity!!.findViewById<View>(R.id.rv_descriptions_captions) as RecyclerView).findViewHolderForAdapterPosition(lastItemPosition) as ViewHolder? | ||||||
|  |                 if (lastItemHolder != null) { | ||||||
|  |                     lastItemHolder.addButton!!.visibility = View.VISIBLE | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun interface Callback { | ||||||
|  |         fun showAlert(mediaDetailDescription: Int, descriptionInfo: Int) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     interface EventListener { | ||||||
|  |         fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) | ||||||
|  |         fun addLanguage() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     internal enum class SelectedVoiceIcon { | ||||||
|  |         CAPTION, | ||||||
|  |         DESCRIPTION | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,297 +0,0 @@ | ||||||
| package fr.free.nrw.commons.upload; |  | ||||||
| 
 |  | ||||||
| import android.content.Context; |  | ||||||
| import android.net.Uri; |  | ||||||
| import fr.free.nrw.commons.Media; |  | ||||||
| import fr.free.nrw.commons.auth.SessionManager; |  | ||||||
| import fr.free.nrw.commons.contributions.Contribution; |  | ||||||
| import fr.free.nrw.commons.filepicker.UploadableFile; |  | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; |  | ||||||
| import fr.free.nrw.commons.location.LatLng; |  | ||||||
| import fr.free.nrw.commons.nearby.Place; |  | ||||||
| import fr.free.nrw.commons.settings.Prefs; |  | ||||||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; |  | ||||||
| import io.reactivex.Observable; |  | ||||||
| import io.reactivex.Single; |  | ||||||
| import io.reactivex.disposables.CompositeDisposable; |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.Date; |  | ||||||
| import java.util.Iterator; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Map; |  | ||||||
| import javax.inject.Inject; |  | ||||||
| import javax.inject.Named; |  | ||||||
| import javax.inject.Singleton; |  | ||||||
| import org.jetbrains.annotations.NotNull; |  | ||||||
| import timber.log.Timber; |  | ||||||
| 
 |  | ||||||
| @Singleton |  | ||||||
| public class UploadModel { |  | ||||||
| 
 |  | ||||||
|     private final JsonKvStore store; |  | ||||||
|     private final List<String> licenses; |  | ||||||
|     private final Context context; |  | ||||||
|     private String license; |  | ||||||
|     private final Map<String, String> licensesByName; |  | ||||||
|     private final List<UploadItem> items = new ArrayList<>(); |  | ||||||
|     private final CompositeDisposable compositeDisposable = new CompositeDisposable(); |  | ||||||
| 
 |  | ||||||
|     private final SessionManager sessionManager; |  | ||||||
|     private final FileProcessor fileProcessor; |  | ||||||
|     private final ImageProcessingService imageProcessingService; |  | ||||||
|     private final List<String> selectedCategories = new ArrayList<>(); |  | ||||||
|     private final List<DepictedItem> selectedDepictions = new ArrayList<>(); |  | ||||||
|     /** |  | ||||||
|      * Existing depicts which are selected |  | ||||||
|      */ |  | ||||||
|     private List<String> selectedExistingDepictions = new ArrayList<>(); |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     UploadModel(@Named("licenses") final List<String> licenses, |  | ||||||
|             @Named("default_preferences") final JsonKvStore store, |  | ||||||
|             @Named("licenses_by_name") final Map<String, String> licensesByName, |  | ||||||
|             final Context context, |  | ||||||
|             final SessionManager sessionManager, |  | ||||||
|             final FileProcessor fileProcessor, |  | ||||||
|             final ImageProcessingService imageProcessingService) { |  | ||||||
|         this.licenses = licenses; |  | ||||||
|         this.store = store; |  | ||||||
|         this.license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); |  | ||||||
|         this.licensesByName = licensesByName; |  | ||||||
|         this.context = context; |  | ||||||
|         this.sessionManager = sessionManager; |  | ||||||
|         this.fileProcessor = fileProcessor; |  | ||||||
|         this.imageProcessingService = imageProcessingService; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * cleanup the resources, I am Singleton, preparing for fresh upload |  | ||||||
|      */ |  | ||||||
|     public void cleanUp() { |  | ||||||
|         compositeDisposable.clear(); |  | ||||||
|         fileProcessor.cleanup(); |  | ||||||
|         items.clear(); |  | ||||||
|         selectedCategories.clear(); |  | ||||||
|         selectedDepictions.clear(); |  | ||||||
|         selectedExistingDepictions.clear(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setSelectedCategories(List<String> selectedCategories) { |  | ||||||
|         this.selectedCategories.clear(); |  | ||||||
|         this.selectedCategories.addAll(selectedCategories); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * pre process a one item at a time |  | ||||||
|      */ |  | ||||||
|     public Observable<UploadItem> preProcessImage(final UploadableFile uploadableFile, |  | ||||||
|         final Place place, |  | ||||||
|         final SimilarImageInterface similarImageInterface, |  | ||||||
|         LatLng inAppPictureLocation) { |  | ||||||
|         return Observable.just( |  | ||||||
|             createAndAddUploadItem(uploadableFile, place, similarImageInterface, inAppPictureLocation)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Calls validateImage() of ImageProcessingService to check quality of image |  | ||||||
|      * |  | ||||||
|      * @param uploadItem UploadItem whose quality is to be checked |  | ||||||
|      * @param inAppPictureLocation In app picture location (if any) |  | ||||||
|      * @return Quality of UploadItem |  | ||||||
|      */ |  | ||||||
|     public Single<Integer> getImageQuality(final UploadItem uploadItem, LatLng inAppPictureLocation) { |  | ||||||
|         return imageProcessingService.validateImage(uploadItem, inAppPictureLocation); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Calls checkDuplicateImage() of ImageProcessingService to check if image is duplicate |  | ||||||
|      * |  | ||||||
|      * @param filePath file to be checked |  | ||||||
|      * @return IMAGE_DUPLICATE or IMAGE_OK |  | ||||||
|      */ |  | ||||||
|     public Single<Integer> checkDuplicateImage(String filePath){ |  | ||||||
|         return imageProcessingService.checkDuplicateImage(filePath); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Calls validateCaption() of ImageProcessingService to check caption of image |  | ||||||
|      * |  | ||||||
|      * @param uploadItem UploadItem whose caption is to be checked |  | ||||||
|      * @return Quality of caption of the UploadItem |  | ||||||
|      */ |  | ||||||
|     public Single<Integer> getCaptionQuality(final UploadItem uploadItem) { |  | ||||||
|         return imageProcessingService.validateCaption(uploadItem); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile, |  | ||||||
|         final Place place, |  | ||||||
|         final SimilarImageInterface similarImageInterface, |  | ||||||
|         LatLng inAppPictureLocation) { |  | ||||||
|         final UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile |  | ||||||
|                 .getFileCreatedDate(context); |  | ||||||
|         long fileCreatedDate = -1; |  | ||||||
|         String createdTimestampSource = ""; |  | ||||||
|         String fileCreatedDateString = ""; |  | ||||||
|         if (dateTimeWithSource != null) { |  | ||||||
|             fileCreatedDate = dateTimeWithSource.getEpochDate(); |  | ||||||
|             fileCreatedDateString = dateTimeWithSource.getDateString(); |  | ||||||
|             createdTimestampSource = dateTimeWithSource.getSource(); |  | ||||||
|         } |  | ||||||
|         Timber.d("File created date is %d", fileCreatedDate); |  | ||||||
|         final ImageCoordinates imageCoordinates = fileProcessor |  | ||||||
|                 .processFileCoordinates(similarImageInterface, uploadableFile.getFilePath(), |  | ||||||
|                     inAppPictureLocation); |  | ||||||
|         final UploadItem uploadItem = new UploadItem( |  | ||||||
|             Uri.parse(uploadableFile.getFilePath()), |  | ||||||
|                 uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate, |  | ||||||
|                 createdTimestampSource, |  | ||||||
|                 uploadableFile.getContentUri(), |  | ||||||
|                 fileCreatedDateString); |  | ||||||
| 
 |  | ||||||
|         // If an uploadItem of the same uploadableFile has been created before, we return that. |  | ||||||
|         // This is to avoid multiple instances of uploadItem of same file passed around. |  | ||||||
|         if (items.contains(uploadItem)) { |  | ||||||
|             return items.get(items.indexOf(uploadItem)); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (place != null) { |  | ||||||
|             uploadItem.getUploadMediaDetails().set(0, new UploadMediaDetail(place)); |  | ||||||
|         } |  | ||||||
|         if (!items.contains(uploadItem)) { |  | ||||||
|             items.add(uploadItem); |  | ||||||
|         } |  | ||||||
|         return uploadItem; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getCount() { |  | ||||||
|         return items.size(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public List<UploadItem> getUploads() { |  | ||||||
|         return items; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public List<String> getLicenses() { |  | ||||||
|         return licenses; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public String getSelectedLicense() { |  | ||||||
|         return license; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setSelectedLicense(final String licenseName) { |  | ||||||
|         this.license = licensesByName.get(licenseName); |  | ||||||
|         store.putString(Prefs.DEFAULT_LICENSE, license); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public Observable<Contribution> buildContributions() { |  | ||||||
|         return Observable.fromIterable(items).map(item -> |  | ||||||
|         { |  | ||||||
|             String imageSHA1 = FileUtils.INSTANCE.getSHA1(context.getContentResolver().openInputStream(item.getContentUri())); |  | ||||||
| 
 |  | ||||||
|             final Contribution contribution = new Contribution( |  | ||||||
|                 item, sessionManager, newListOf(selectedDepictions), newListOf(selectedCategories), imageSHA1); |  | ||||||
| 
 |  | ||||||
|             contribution.setHasInvalidLocation(item.hasInvalidLocation()); |  | ||||||
| 
 |  | ||||||
|             Timber.d("Created timestamp while building contribution is %s, %s", |  | ||||||
|                 item.getCreatedTimestamp(), |  | ||||||
|                 new Date(item.getCreatedTimestamp())); |  | ||||||
| 
 |  | ||||||
|             if (item.getCreatedTimestamp() != -1L) { |  | ||||||
|                 contribution.setDateCreated(new Date(item.getCreatedTimestamp())); |  | ||||||
|                 contribution.setDateCreatedSource(item.getCreatedTimestampSource()); |  | ||||||
|                 //Set the date only if you have it, else the upload service is gonna try it the other way |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (contribution.getWikidataPlace() != null) { |  | ||||||
|                 if (item.isWLMUpload()) { |  | ||||||
|                     contribution.getWikidataPlace().setMonumentUpload(true); |  | ||||||
|                 } else { |  | ||||||
|                     contribution.getWikidataPlace().setMonumentUpload(false); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             contribution.setCountryCode(item.getCountryCode()); |  | ||||||
|             return contribution; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void deletePicture(final String filePath) { |  | ||||||
|         final Iterator<UploadItem> iterator = items.iterator(); |  | ||||||
|         while (iterator.hasNext()) { |  | ||||||
|             if (iterator.next().getMediaUri().toString().contains(filePath)) { |  | ||||||
|                 iterator.remove(); |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         if (items.isEmpty()) { |  | ||||||
|             cleanUp(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public List<UploadItem> getItems() { |  | ||||||
|         return items; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void onDepictItemClicked(DepictedItem depictedItem, Media media) { |  | ||||||
|         if (media == null) { |  | ||||||
|             if (depictedItem.isSelected()) { |  | ||||||
|                 selectedDepictions.add(depictedItem); |  | ||||||
|             } else { |  | ||||||
|                 selectedDepictions.remove(depictedItem); |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             if (depictedItem.isSelected()) { |  | ||||||
|                 if (media.getDepictionIds().contains(depictedItem.getId())) { |  | ||||||
|                     selectedExistingDepictions.add(depictedItem.getId()); |  | ||||||
|                 } else { |  | ||||||
|                     selectedDepictions.add(depictedItem); |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 if (media.getDepictionIds().contains(depictedItem.getId())) { |  | ||||||
|                     selectedExistingDepictions.remove(depictedItem.getId()); |  | ||||||
|                     if (!media.getDepictionIds().contains(depictedItem.getId())) { |  | ||||||
|                         final List<String> depictsList = new ArrayList<>(); |  | ||||||
|                         depictsList.add(depictedItem.getId()); |  | ||||||
|                         depictsList.addAll(media.getDepictionIds()); |  | ||||||
|                         media.setDepictionIds(depictsList); |  | ||||||
|                     } |  | ||||||
|                 } else { |  | ||||||
|                     selectedDepictions.remove(depictedItem); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @NotNull |  | ||||||
|     private <T> List<T> newListOf(final List<T> items) { |  | ||||||
|         return items != null ? new ArrayList<>(items) : new ArrayList<>(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void useSimilarPictureCoordinates(final ImageCoordinates imageCoordinates, final int uploadItemIndex) { |  | ||||||
|         fileProcessor.prePopulateCategoriesAndDepictionsBy(imageCoordinates); |  | ||||||
|         items.get(uploadItemIndex).setGpsCoords(imageCoordinates); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public List<DepictedItem> getSelectedDepictions() { |  | ||||||
|         return selectedDepictions; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Provides selected existing depicts |  | ||||||
|      * |  | ||||||
|      * @return selected existing depicts |  | ||||||
|      */ |  | ||||||
|     public List<String> getSelectedExistingDepictions() { |  | ||||||
|         return selectedExistingDepictions; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Initialize existing depicts |  | ||||||
|      * |  | ||||||
|      * @param selectedExistingDepictions existing depicts |  | ||||||
|      */ |  | ||||||
|     public void setSelectedExistingDepictions(final List<String> selectedExistingDepictions) { |  | ||||||
|         this.selectedExistingDepictions = selectedExistingDepictions; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										242
									
								
								app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,242 @@ | ||||||
|  | package fr.free.nrw.commons.upload | ||||||
|  | 
 | ||||||
|  | import android.content.Context | ||||||
|  | import android.net.Uri | ||||||
|  | import fr.free.nrw.commons.Media | ||||||
|  | import fr.free.nrw.commons.auth.SessionManager | ||||||
|  | import fr.free.nrw.commons.contributions.Contribution | ||||||
|  | import fr.free.nrw.commons.filepicker.UploadableFile | ||||||
|  | import fr.free.nrw.commons.kvstore.JsonKvStore | ||||||
|  | import fr.free.nrw.commons.location.LatLng | ||||||
|  | import fr.free.nrw.commons.nearby.Place | ||||||
|  | import fr.free.nrw.commons.settings.Prefs | ||||||
|  | import fr.free.nrw.commons.upload.FileUtils.getSHA1 | ||||||
|  | import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | ||||||
|  | import io.reactivex.Observable | ||||||
|  | import io.reactivex.Single | ||||||
|  | import io.reactivex.disposables.CompositeDisposable | ||||||
|  | import timber.log.Timber | ||||||
|  | import java.util.Date | ||||||
|  | import javax.inject.Inject | ||||||
|  | import javax.inject.Named | ||||||
|  | import javax.inject.Singleton | ||||||
|  | 
 | ||||||
|  | @Singleton | ||||||
|  | class UploadModel @Inject internal constructor( | ||||||
|  |     @param:Named("licenses") val licenses: List<String>, | ||||||
|  |     @param:Named("default_preferences") val store: JsonKvStore, | ||||||
|  |     @param:Named("licenses_by_name") val licensesByName: Map<String, String>, | ||||||
|  |     val context: Context, | ||||||
|  |     val sessionManager: SessionManager, | ||||||
|  |     val fileProcessor: FileProcessor, | ||||||
|  |     val imageProcessingService: ImageProcessingService | ||||||
|  | ) { | ||||||
|  |     var license: String? = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3) | ||||||
|  |     val items: MutableList<UploadItem> = mutableListOf() | ||||||
|  |     val compositeDisposable: CompositeDisposable = CompositeDisposable() | ||||||
|  |     val selectedCategories: MutableList<String> = mutableListOf() | ||||||
|  |     val selectedDepictions: MutableList<DepictedItem> = mutableListOf() | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Existing depicts which are selected | ||||||
|  |      */ | ||||||
|  |     var selectedExistingDepictions: MutableList<String> = mutableListOf() | ||||||
|  |     val count: Int | ||||||
|  |         get() = items.size | ||||||
|  | 
 | ||||||
|  |     val uploads: List<UploadItem> | ||||||
|  |         get() = items | ||||||
|  | 
 | ||||||
|  |     var selectedLicense: String? | ||||||
|  |         get() = license | ||||||
|  |         set(licenseName) { | ||||||
|  |             license = licensesByName[licenseName] | ||||||
|  |             if (license == null) { | ||||||
|  |                 store.remove(Prefs.DEFAULT_LICENSE) | ||||||
|  |             } else { | ||||||
|  |                 store.putString(Prefs.DEFAULT_LICENSE, license!!) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * cleanup the resources, I am Singleton, preparing for fresh upload | ||||||
|  |      */ | ||||||
|  |     fun cleanUp() { | ||||||
|  |         compositeDisposable.clear() | ||||||
|  |         fileProcessor.cleanup() | ||||||
|  |         items.clear() | ||||||
|  |         selectedCategories.clear() | ||||||
|  |         selectedDepictions.clear() | ||||||
|  |         selectedExistingDepictions.clear() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun setSelectedCategories(categories: List<String>) { | ||||||
|  |         selectedCategories.clear() | ||||||
|  |         selectedCategories.addAll(categories) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * pre process a one item at a time | ||||||
|  |      */ | ||||||
|  |     fun preProcessImage( | ||||||
|  |         uploadableFile: UploadableFile?, | ||||||
|  |         place: Place?, | ||||||
|  |         similarImageInterface: SimilarImageInterface?, | ||||||
|  |         inAppPictureLocation: LatLng? | ||||||
|  |     ): Observable<UploadItem> = Observable.just( | ||||||
|  |         createAndAddUploadItem(uploadableFile, place, similarImageInterface, inAppPictureLocation) | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Calls validateImage() of ImageProcessingService to check quality of image | ||||||
|  |      * | ||||||
|  |      * @param uploadItem UploadItem whose quality is to be checked | ||||||
|  |      * @param inAppPictureLocation In app picture location (if any) | ||||||
|  |      * @return Quality of UploadItem | ||||||
|  |      */ | ||||||
|  |     fun getImageQuality(uploadItem: UploadItem, inAppPictureLocation: LatLng?): Single<Int> = | ||||||
|  |         imageProcessingService.validateImage(uploadItem, inAppPictureLocation) | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Calls checkDuplicateImage() of ImageProcessingService to check if image is duplicate | ||||||
|  |      * | ||||||
|  |      * @param filePath file to be checked | ||||||
|  |      * @return IMAGE_DUPLICATE or IMAGE_OK | ||||||
|  |      */ | ||||||
|  |     fun checkDuplicateImage(filePath: String?): Single<Int> = | ||||||
|  |         imageProcessingService.checkDuplicateImage(filePath) | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Calls validateCaption() of ImageProcessingService to check caption of image | ||||||
|  |      * | ||||||
|  |      * @param uploadItem UploadItem whose caption is to be checked | ||||||
|  |      * @return Quality of caption of the UploadItem | ||||||
|  |      */ | ||||||
|  |     fun getCaptionQuality(uploadItem: UploadItem): Single<Int> = | ||||||
|  |         imageProcessingService.validateCaption(uploadItem) | ||||||
|  | 
 | ||||||
|  |     private fun createAndAddUploadItem( | ||||||
|  |         uploadableFile: UploadableFile?, | ||||||
|  |         place: Place?, | ||||||
|  |         similarImageInterface: SimilarImageInterface?, | ||||||
|  |         inAppPictureLocation: LatLng? | ||||||
|  |     ): UploadItem { | ||||||
|  |         val dateTimeWithSource = uploadableFile?.getFileCreatedDate(context) | ||||||
|  |         var fileCreatedDate: Long = -1 | ||||||
|  |         var createdTimestampSource = "" | ||||||
|  |         var fileCreatedDateString: String? = "" | ||||||
|  |         if (dateTimeWithSource != null) { | ||||||
|  |             fileCreatedDate = dateTimeWithSource.epochDate | ||||||
|  |             fileCreatedDateString = dateTimeWithSource.dateString | ||||||
|  |             createdTimestampSource = dateTimeWithSource.source | ||||||
|  |         } | ||||||
|  |         Timber.d("File created date is %d", fileCreatedDate) | ||||||
|  |         val imageCoordinates = fileProcessor | ||||||
|  |             .processFileCoordinates( | ||||||
|  |                 similarImageInterface, uploadableFile?.getFilePath(), | ||||||
|  |                 inAppPictureLocation | ||||||
|  |             ) | ||||||
|  |         val uploadItem = UploadItem( | ||||||
|  |             Uri.parse(uploadableFile?.getFilePath()), | ||||||
|  |             uploadableFile?.getMimeType(context), imageCoordinates, place, fileCreatedDate, | ||||||
|  |             createdTimestampSource, | ||||||
|  |             uploadableFile?.contentUri, | ||||||
|  |             fileCreatedDateString | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         // If an uploadItem of the same uploadableFile has been created before, we return that. | ||||||
|  |         // This is to avoid multiple instances of uploadItem of same file passed around. | ||||||
|  |         if (items.contains(uploadItem)) { | ||||||
|  |             return items[items.indexOf(uploadItem)] | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         uploadItem.uploadMediaDetails[0] = UploadMediaDetail(place) | ||||||
|  |         if (!items.contains(uploadItem)) { | ||||||
|  |             items.add(uploadItem) | ||||||
|  |         } | ||||||
|  |         return uploadItem | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun buildContributions(): Observable<Contribution> { | ||||||
|  |         return Observable.fromIterable(items).map { item: UploadItem -> | ||||||
|  |             val imageSHA1 = getSHA1( | ||||||
|  |                 context.contentResolver.openInputStream(item.contentUri!!)!! | ||||||
|  |             ) | ||||||
|  |             val contribution = Contribution( | ||||||
|  |                 item, | ||||||
|  |                 sessionManager, | ||||||
|  |                 buildList { addAll(selectedDepictions) }, | ||||||
|  |                 buildList { addAll(selectedCategories) }, | ||||||
|  |                 imageSHA1 | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             contribution.setHasInvalidLocation(item.hasInvalidLocation()) | ||||||
|  | 
 | ||||||
|  |             Timber.d( | ||||||
|  |                 "Created timestamp while building contribution is %s, %s", | ||||||
|  |                 item.createdTimestamp, | ||||||
|  |                 item.createdTimestamp?.let { Date(it) } | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             if (item.createdTimestamp != -1L) { | ||||||
|  |                 contribution.dateCreated = item.createdTimestamp?.let { Date(it) } | ||||||
|  |                 contribution.dateCreatedSource = item.createdTimestampSource | ||||||
|  |                 //Set the date only if you have it, else the upload service is gonna try it the other way | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (contribution.wikidataPlace != null) { | ||||||
|  |                 contribution.wikidataPlace!!.isMonumentUpload = item.isWLMUpload | ||||||
|  |             } | ||||||
|  |             contribution.countryCode = item.countryCode | ||||||
|  |             contribution | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun deletePicture(filePath: String) { | ||||||
|  |         val iterator = items.iterator() | ||||||
|  |         while (iterator.hasNext()) { | ||||||
|  |             if (iterator.next().mediaUri.toString().contains(filePath)) { | ||||||
|  |                 iterator.remove() | ||||||
|  |                 break | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (items.isEmpty()) { | ||||||
|  |             cleanUp() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun onDepictItemClicked(depictedItem: DepictedItem, media: Media?) { | ||||||
|  |         if (media == null) { | ||||||
|  |             if (depictedItem.isSelected) { | ||||||
|  |                 selectedDepictions.add(depictedItem) | ||||||
|  |             } else { | ||||||
|  |                 selectedDepictions.remove(depictedItem) | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             if (depictedItem.isSelected) { | ||||||
|  |                 if (media.depictionIds.contains(depictedItem.id)) { | ||||||
|  |                     selectedExistingDepictions.add(depictedItem.id) | ||||||
|  |                 } else { | ||||||
|  |                     selectedDepictions.add(depictedItem) | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 if (media.depictionIds.contains(depictedItem.id)) { | ||||||
|  |                     selectedExistingDepictions.remove(depictedItem.id) | ||||||
|  |                     if (!media.depictionIds.contains(depictedItem.id)) { | ||||||
|  |                         media.depictionIds = mutableListOf<String>().apply { | ||||||
|  |                             add(depictedItem.id) | ||||||
|  |                             addAll(media.depictionIds) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     selectedDepictions.remove(depictedItem) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates, uploadItemIndex: Int) { | ||||||
|  |         fileProcessor.prePopulateCategoriesAndDepictionsBy(imageCoordinates) | ||||||
|  |         items[uploadItemIndex].gpsCoords = imageCoordinates | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| package fr.free.nrw.commons.upload | package fr.free.nrw.commons.upload | ||||||
| 
 | 
 | ||||||
| import android.annotation.SuppressLint | 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.CommonsApplication.Companion.IS_LIMITED_CONNECTION_MODE_ENABLED | ||||||
| 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 fr.free.nrw.commons.kvstore.BasicKvStore | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore | import fr.free.nrw.commons.kvstore.JsonKvStore | ||||||
| import fr.free.nrw.commons.repository.UploadRepository | import fr.free.nrw.commons.repository.UploadRepository | ||||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract | import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract | ||||||
|  | @ -34,6 +34,8 @@ class UploadPresenter @Inject internal constructor( | ||||||
| 
 | 
 | ||||||
|     private val compositeDisposable = CompositeDisposable() |     private val compositeDisposable = CompositeDisposable() | ||||||
| 
 | 
 | ||||||
|  |     lateinit var basicKvStoreFactory: (String) -> BasicKvStore | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Called by the submit button in [UploadActivity] |      * Called by the submit button in [UploadActivity] | ||||||
|      */ |      */ | ||||||
|  | @ -69,8 +71,7 @@ class UploadPresenter @Inject internal constructor( | ||||||
|     private fun processContributionsForSubmission() { |     private fun processContributionsForSubmission() { | ||||||
|         if (view.isLoggedIn()) { |         if (view.isLoggedIn()) { | ||||||
|             view.showProgress(true) |             view.showProgress(true) | ||||||
|             repository.buildContributions() |             repository.buildContributions().observeOn(Schedulers.io()) | ||||||
|                 ?.observeOn(Schedulers.io()) |  | ||||||
|                 ?.subscribe(object : Observer<Contribution> { |                 ?.subscribe(object : Observer<Contribution> { | ||||||
|                     override fun onSubscribe(d: Disposable) { |                     override fun onSubscribe(d: Disposable) { | ||||||
|                         view.showProgress(false) |                         view.showProgress(false) | ||||||
|  | @ -127,14 +128,20 @@ class UploadPresenter @Inject internal constructor( | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     override fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore) { | ||||||
|  |         basicKvStoreFactory = factory | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Calls checkImageQuality of UploadMediaPresenter to check image quality of next image |      * Calls checkImageQuality of UploadMediaPresenter to check image quality of next image | ||||||
|      * |      * | ||||||
|      * @param uploadItemIndex Index of next image, whose quality is to be checked |      * @param uploadItemIndex Index of next image, whose quality is to be checked | ||||||
|      */ |      */ | ||||||
|     override fun checkImageQuality(uploadItemIndex: Int) { |     override fun checkImageQuality(uploadItemIndex: Int) { | ||||||
|         val uploadItem = repository.getUploadItem(uploadItemIndex) |         repository.getUploadItem(uploadItemIndex)?.let { | ||||||
|         presenter.checkImageQuality(uploadItem, uploadItemIndex) |             presenter.setupBasicKvStoreFactory(basicKvStoreFactory) | ||||||
|  |             presenter.checkImageQuality(it, uploadItemIndex) | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun deletePictureAtIndex(index: Int) { |     override fun deletePictureAtIndex(index: Int) { | ||||||
|  | @ -156,8 +163,9 @@ class UploadPresenter @Inject internal constructor( | ||||||
|         view.onUploadMediaDeleted(index) |         view.onUploadMediaDeleted(index) | ||||||
|         if (index != uploadableFiles.size && index != 0) { |         if (index != uploadableFiles.size && index != 0) { | ||||||
|             // if the deleted image was not the last item to be uploaded, check quality of next |             // if the deleted image was not the last item to be uploaded, check quality of next | ||||||
|             val uploadItem = repository.getUploadItem(index) |             repository.getUploadItem(index)?.let { | ||||||
|             presenter.checkImageQuality(uploadItem, index) |                 presenter.checkImageQuality(it, index) | ||||||
|  |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (uploadableFiles.size < 2) { |         if (uploadableFiles.size < 2) { | ||||||
|  |  | ||||||
|  | @ -95,12 +95,10 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { | ||||||
|         } |         } | ||||||
|         if (media == null) { |         if (media == null) { | ||||||
|             if (callback != null) { |             if (callback != null) { | ||||||
|                 binding!!.tvTitle.text = getString( |                 binding!!.tvTitle.text = getString(R.string.step_count, | ||||||
|                     R.string.step_count, callback!!.getIndexInViewFlipper( |                     callback!!.getIndexInViewFlipper(this) + 1, | ||||||
|                         this |                     callback!!.totalNumberOfSteps, | ||||||
|                     ) + 1, |                     getString(R.string.categories_activity_title)) | ||||||
|                     callback!!.totalNumberOfSteps, getString(R.string.categories_activity_title) |  | ||||||
|                 ) |  | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             binding!!.tvTitle.setText(R.string.edit_categories) |             binding!!.tvTitle.setText(R.string.edit_categories) | ||||||
|  | @ -220,7 +218,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun goToNextScreen() { |     override fun goToNextScreen() { | ||||||
|         callback!!.onNextButtonClicked(callback!!.getIndexInViewFlipper(this)) |         callback?.let { it.onNextButtonClicked(it.getIndexInViewFlipper(this)) } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun showNoCategorySelected() { |     override fun showNoCategorySelected() { | ||||||
|  | @ -322,7 +320,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { | ||||||
|             mediaDetailFragment.onResume() |             mediaDetailFragment.onResume() | ||||||
|             goBackToPreviousScreen() |             goBackToPreviousScreen() | ||||||
|         } else { |         } else { | ||||||
|             callback!!.onPreviousButtonClicked(callback!!.getIndexInViewFlipper(this)) |             callback?.let { it.onPreviousButtonClicked(it.getIndexInViewFlipper(this)) } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -96,11 +96,10 @@ class DepictsFragment : UploadBaseFragment(), DepictsContract.View { | ||||||
| 
 | 
 | ||||||
|         if (media == null) { |         if (media == null) { | ||||||
|             binding.depictsTitle.text = |             binding.depictsTitle.text = | ||||||
|                 String.format( |                 String.format(getString(R.string.step_count), | ||||||
|                     getString(R.string.step_count), callback!!.getIndexInViewFlipper( |                     callback!!.getIndexInViewFlipper(this) + 1, | ||||||
|                         this |                     callback!!.totalNumberOfSteps, | ||||||
|                     ) + 1, |                     getString(R.string.depicts_step_title) | ||||||
|                     callback!!.totalNumberOfSteps, getString(R.string.depicts_step_title) |  | ||||||
|                 ) |                 ) | ||||||
|         } else { |         } else { | ||||||
|             binding.depictsTitle.setText(R.string.edit_depictions) |             binding.depictsTitle.setText(R.string.edit_depictions) | ||||||
|  |  | ||||||
|  | @ -45,8 +45,7 @@ class MediaLicenseFragment : UploadBaseFragment(), MediaLicenseContract.View { | ||||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|         super.onViewCreated(view, savedInstanceState) |         super.onViewCreated(view, savedInstanceState) | ||||||
| 
 | 
 | ||||||
|         binding.tvTitle.text = getString( |         binding.tvTitle.text = getString(R.string.step_count, | ||||||
|             R.string.step_count, |  | ||||||
|             callback!!.getIndexInViewFlipper(this) + 1, |             callback!!.getIndexInViewFlipper(this) + 1, | ||||||
|             callback!!.totalNumberOfSteps, |             callback!!.totalNumberOfSteps, | ||||||
|             getString(R.string.license_step_title) |             getString(R.string.license_step_title) | ||||||
|  |  | ||||||
|  | @ -1,922 +0,0 @@ | ||||||
| package fr.free.nrw.commons.upload.mediaDetails; |  | ||||||
| 
 |  | ||||||
| import static android.app.Activity.RESULT_OK; |  | ||||||
| 
 |  | ||||||
| import android.annotation.SuppressLint; |  | ||||||
| import android.app.Activity; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.graphics.drawable.Drawable; |  | ||||||
| import android.net.Uri; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.os.Parcelable; |  | ||||||
| import android.speech.RecognizerIntent; |  | ||||||
| import android.text.TextUtils; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.CheckBox; |  | ||||||
| import android.widget.ImageView; |  | ||||||
| import android.widget.Toast; |  | ||||||
| import androidx.activity.result.ActivityResult; |  | ||||||
| import androidx.activity.result.ActivityResultLauncher; |  | ||||||
| import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| import androidx.exifinterface.media.ExifInterface; |  | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager; |  | ||||||
| import fr.free.nrw.commons.CameraPosition; |  | ||||||
| import fr.free.nrw.commons.locationpicker.LocationPicker; |  | ||||||
| import fr.free.nrw.commons.R; |  | ||||||
| import fr.free.nrw.commons.contributions.MainActivity; |  | ||||||
| import fr.free.nrw.commons.databinding.FragmentUploadMediaDetailFragmentBinding; |  | ||||||
| import fr.free.nrw.commons.edit.EditActivity; |  | ||||||
| import fr.free.nrw.commons.filepicker.UploadableFile; |  | ||||||
| import fr.free.nrw.commons.kvstore.BasicKvStore; |  | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; |  | ||||||
| import fr.free.nrw.commons.location.LatLng; |  | ||||||
| import fr.free.nrw.commons.nearby.Place; |  | ||||||
| import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao; |  | ||||||
| import fr.free.nrw.commons.settings.Prefs; |  | ||||||
| import fr.free.nrw.commons.upload.ImageCoordinates; |  | ||||||
| import fr.free.nrw.commons.upload.SimilarImageDialogFragment; |  | ||||||
| import fr.free.nrw.commons.upload.UploadActivity; |  | ||||||
| import fr.free.nrw.commons.upload.UploadBaseFragment; |  | ||||||
| import fr.free.nrw.commons.upload.UploadItem; |  | ||||||
| import fr.free.nrw.commons.upload.UploadMediaDetail; |  | ||||||
| import fr.free.nrw.commons.upload.UploadMediaDetailAdapter; |  | ||||||
| import fr.free.nrw.commons.utils.ActivityUtils; |  | ||||||
| import fr.free.nrw.commons.utils.DialogUtil; |  | ||||||
| import fr.free.nrw.commons.utils.ImageUtils; |  | ||||||
| import fr.free.nrw.commons.utils.NetworkUtils; |  | ||||||
| import fr.free.nrw.commons.utils.ViewUtil; |  | ||||||
| import java.io.File; |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Locale; |  | ||||||
| import java.util.Objects; |  | ||||||
| import javax.inject.Inject; |  | ||||||
| import javax.inject.Named; |  | ||||||
| import timber.log.Timber; |  | ||||||
| 
 |  | ||||||
| public class UploadMediaDetailFragment extends UploadBaseFragment implements |  | ||||||
|     UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener { |  | ||||||
| 
 |  | ||||||
|     private UploadMediaDetailAdapter uploadMediaDetailAdapter; |  | ||||||
| 
 |  | ||||||
|     private final ActivityResultLauncher<Intent> startForResult = registerForActivityResult( |  | ||||||
|         new StartActivityForResult(), result -> { |  | ||||||
|                 onCameraPosition(result); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|     private final ActivityResultLauncher<Intent> startForEditActivityResult = registerForActivityResult( |  | ||||||
|         new StartActivityForResult(), result -> { |  | ||||||
|             onEditActivityResult(result); |  | ||||||
|         } |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     private final ActivityResultLauncher<Intent> voiceInputResultLauncher = registerForActivityResult( |  | ||||||
|         new StartActivityForResult(), result -> { |  | ||||||
|             onVoiceInput(result); |  | ||||||
|         } |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     public static Activity activity ; |  | ||||||
| 
 |  | ||||||
|     private int indexOfFragment; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * A key for applicationKvStore. By this key we can retrieve the location of last UploadItem ex. |  | ||||||
|      * 12.3433,54.78897 from applicationKvStore. |  | ||||||
|      */ |  | ||||||
|     public static final String LAST_LOCATION = "last_location_while_uploading"; |  | ||||||
|     public static final String LAST_ZOOM = "last_zoom_level_while_uploading"; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     public static final String UPLOADABLE_FILE = "uploadable_file"; |  | ||||||
| 
 |  | ||||||
|     public static final String UPLOAD_MEDIA_DETAILS = "upload_media_detail_adapter"; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * True when user removes location from the current image |  | ||||||
|      */ |  | ||||||
|     private boolean hasUserRemovedLocation; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     UploadMediaDetailsContract.UserActionListener presenter; |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     @Named("default_preferences") |  | ||||||
|     JsonKvStore defaultKvStore; |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     RecentLanguagesDao recentLanguagesDao; |  | ||||||
| 
 |  | ||||||
|     private UploadableFile uploadableFile; |  | ||||||
|     private Place place; |  | ||||||
| 
 |  | ||||||
|     private boolean isExpanded = true; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * True if location is added via the "missing location" popup dialog (which appears after |  | ||||||
|      * tapping "Next" if the picture has no geographical coordinates). |  | ||||||
|      */ |  | ||||||
|     private boolean isMissingLocationDialog; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * showNearbyFound will be true, if any nearby location found that needs pictures and the nearby |  | ||||||
|      * popup is yet to be shown Used to show and check if the nearby found popup is already shown |  | ||||||
|      */ |  | ||||||
|     private boolean showNearbyFound; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * nearbyPlace holds the detail of nearby place that need pictures, if any found |  | ||||||
|      */ |  | ||||||
|     private Place nearbyPlace; |  | ||||||
|     private UploadItem uploadItem; |  | ||||||
|     /** |  | ||||||
|      * inAppPictureLocation: use location recorded while using the in-app camera if device camera |  | ||||||
|      * does not record it in the EXIF |  | ||||||
|      */ |  | ||||||
|     private LatLng inAppPictureLocation; |  | ||||||
|     /** |  | ||||||
|      * editableUploadItem : Storing the upload item before going to update the coordinates |  | ||||||
|      */ |  | ||||||
|     private UploadItem editableUploadItem; |  | ||||||
| 
 |  | ||||||
|     private BasicKvStore basicKvStore; |  | ||||||
|      |  | ||||||
|     private final String keyForShowingAlertDialog = "isNoNetworkAlertDialogShowing"; |  | ||||||
| 
 |  | ||||||
|     private UploadMediaDetailFragmentCallback callback; |  | ||||||
| 
 |  | ||||||
|     private FragmentUploadMediaDetailFragmentBinding binding; |  | ||||||
| 
 |  | ||||||
|     public void setCallback(UploadMediaDetailFragmentCallback callback) { |  | ||||||
|         this.callback = callback; |  | ||||||
|         UploadMediaPresenter.presenterCallback = callback; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onCreate(@Nullable Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         if(savedInstanceState!=null && uploadableFile==null) { |  | ||||||
|             uploadableFile = savedInstanceState.getParcelable(UPLOADABLE_FILE); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     public void setImageToBeUploaded(UploadableFile uploadableFile, Place place, |  | ||||||
|         LatLng inAppPictureLocation) { |  | ||||||
|         this.uploadableFile = uploadableFile; |  | ||||||
|         this.place = place; |  | ||||||
|         this.inAppPictureLocation = inAppPictureLocation; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Nullable |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, |  | ||||||
|         @Nullable Bundle savedInstanceState) { |  | ||||||
|         binding = FragmentUploadMediaDetailFragmentBinding.inflate(inflater, container, false); |  | ||||||
|         return binding.getRoot(); |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { |  | ||||||
|         super.onViewCreated(view, savedInstanceState); |  | ||||||
| 
 |  | ||||||
|         activity = getActivity(); |  | ||||||
|         basicKvStore = new BasicKvStore(activity, "CurrentUploadImageQualities"); |  | ||||||
| 
 |  | ||||||
|         if (callback != null) { |  | ||||||
|             indexOfFragment = callback.getIndexInViewFlipper(this); |  | ||||||
|             init(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if(savedInstanceState!=null){ |  | ||||||
|                 if(uploadMediaDetailAdapter.getItems().size()==0 && callback != null){ |  | ||||||
|                     uploadMediaDetailAdapter.setItems(savedInstanceState.getParcelableArrayList(UPLOAD_MEDIA_DETAILS)); |  | ||||||
|                     presenter.setUploadMediaDetails(uploadMediaDetailAdapter.getItems(), |  | ||||||
|                         indexOfFragment); |  | ||||||
|                 } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         try { |  | ||||||
|             if(!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, getActivity())) { |  | ||||||
|                 ActivityUtils.startActivityWithFlags( |  | ||||||
|                 getActivity(), MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, |  | ||||||
|                 Intent.FLAG_ACTIVITY_SINGLE_TOP); |  | ||||||
|             } |  | ||||||
|         } catch (Exception e) { |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void init() { |  | ||||||
|         if (binding == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         binding.tvTitle.setText(getString(R.string.step_count, (indexOfFragment + 1), |  | ||||||
|             callback.getTotalNumberOfSteps(), getString(R.string.media_detail_step_title))); |  | ||||||
|         binding.tooltip.setOnClickListener( |  | ||||||
|             v -> showInfoAlert(R.string.media_detail_step_title, R.string.media_details_tooltip)); |  | ||||||
|         initPresenter(); |  | ||||||
|         presenter.receiveImage(uploadableFile, place, inAppPictureLocation); |  | ||||||
|         initRecyclerView(); |  | ||||||
| 
 |  | ||||||
|         if (indexOfFragment == 0) { |  | ||||||
|             binding.btnPrevious.setEnabled(false); |  | ||||||
|             binding.btnPrevious.setAlpha(0.5f); |  | ||||||
|         } else { |  | ||||||
|             binding.btnPrevious.setEnabled(true); |  | ||||||
|             binding.btnPrevious.setAlpha(1.0f); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // If the image EXIF data contains the location, show the map icon with a green tick |  | ||||||
|         if (inAppPictureLocation != null || |  | ||||||
|                 (uploadableFile != null && uploadableFile.hasLocation())) { |  | ||||||
|             Drawable mapTick = getResources().getDrawable(R.drawable.ic_map_available_20dp); |  | ||||||
|             binding.locationImageView.setImageDrawable(mapTick); |  | ||||||
|             binding.locationTextView.setText(R.string.edit_location); |  | ||||||
|         } else { |  | ||||||
|             // Otherwise, show the map icon with a red question mark |  | ||||||
|             Drawable mapQuestionMark = |  | ||||||
|                 getResources().getDrawable(R.drawable.ic_map_not_available_20dp); |  | ||||||
|             binding.locationImageView.setImageDrawable(mapQuestionMark); |  | ||||||
|             binding.locationTextView.setText(R.string.add_location); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         //If this is the last media, we have nothing to copy, lets not show the button |  | ||||||
|         if (indexOfFragment == callback.getTotalNumberOfSteps() - 4) { |  | ||||||
|             binding.btnCopySubsequentMedia.setVisibility(View.GONE); |  | ||||||
|         } else { |  | ||||||
|             binding.btnCopySubsequentMedia.setVisibility(View.VISIBLE); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         binding.btnNext.setOnClickListener(v -> onNextButtonClicked()); |  | ||||||
|         binding.btnPrevious.setOnClickListener(v -> onPreviousButtonClicked()); |  | ||||||
|         binding.llEditImage.setOnClickListener(v -> onEditButtonClicked()); |  | ||||||
|         binding.llContainerTitle.setOnClickListener(v -> onLlContainerTitleClicked()); |  | ||||||
|         binding.llLocationStatus.setOnClickListener(v -> onIbMapClicked()); |  | ||||||
|         binding.btnCopySubsequentMedia.setOnClickListener(v -> onButtonCopyTitleDescToSubsequentMedia()); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         attachImageViewScaleChangeListener(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Attaches the scale change listener to the image view |  | ||||||
|      */ |  | ||||||
|     private void attachImageViewScaleChangeListener() { |  | ||||||
|         binding.backgroundImage.setOnScaleChangeListener( |  | ||||||
|             (scaleFactor, focusX, focusY) -> { |  | ||||||
|                 //Whenever the uses plays with the image, lets collapse the media detail container |  | ||||||
|                 //only if it is not already collapsed, which resolves flickering of arrow |  | ||||||
|                 if (isExpanded) { |  | ||||||
|                     expandCollapseLlMediaDetail(false); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * attach the presenter with the view |  | ||||||
|      */ |  | ||||||
|     private void initPresenter() { |  | ||||||
|         presenter.onAttachView(this); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * init the description recycler veiw and caption recyclerview |  | ||||||
|      */ |  | ||||||
|     private void initRecyclerView() { |  | ||||||
|         uploadMediaDetailAdapter = new UploadMediaDetailAdapter(this, |  | ||||||
|             defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao, voiceInputResultLauncher); |  | ||||||
|         uploadMediaDetailAdapter.setCallback(this::showInfoAlert); |  | ||||||
|         uploadMediaDetailAdapter.setEventListener(this); |  | ||||||
|         binding.rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext())); |  | ||||||
|         binding.rvDescriptions.setAdapter(uploadMediaDetailAdapter); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * show dialog with info |  | ||||||
|      * @param titleStringID |  | ||||||
|      * @param messageStringId |  | ||||||
|      */ |  | ||||||
|     private void showInfoAlert(int titleStringID, int messageStringId) { |  | ||||||
|         DialogUtil.showAlertDialog(getActivity(), getString(titleStringID), |  | ||||||
|             getString(messageStringId), getString(android.R.string.ok), null); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     public void onNextButtonClicked() { |  | ||||||
|         if (callback == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         presenter.displayLocDialog(indexOfFragment, inAppPictureLocation, hasUserRemovedLocation); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void onPreviousButtonClicked() { |  | ||||||
|         if (callback == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         callback.onPreviousButtonClicked(indexOfFragment); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void onEditButtonClicked() { |  | ||||||
|         presenter.onEditButtonClicked(indexOfFragment); |  | ||||||
|     } |  | ||||||
|     @Override |  | ||||||
|     public void showSimilarImageFragment(String originalFilePath, String possibleFilePath, |  | ||||||
|         ImageCoordinates similarImageCoordinates) { |  | ||||||
|         BasicKvStore basicKvStore = new BasicKvStore(getActivity(), "IsAnyImageCancelled"); |  | ||||||
|         if (!basicKvStore.getBoolean("IsAnyImageCancelled", false)) { |  | ||||||
|             SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment(); |  | ||||||
|             newFragment.setCancelable(false); |  | ||||||
|             newFragment.setCallback(new SimilarImageDialogFragment.Callback() { |  | ||||||
|                 @Override |  | ||||||
|                 public void onPositiveResponse() { |  | ||||||
|                     Timber.d("positive response from similar image fragment"); |  | ||||||
|                     presenter.useSimilarPictureCoordinates(similarImageCoordinates, |  | ||||||
|                         indexOfFragment); |  | ||||||
| 
 |  | ||||||
|                     // set the description text when user selects to use coordinate from the other image |  | ||||||
|                     // which was taken within 120s |  | ||||||
|                     // fixing: https://github.com/commons-app/apps-android-commons/issues/4700 |  | ||||||
|                     uploadMediaDetailAdapter.getItems().get(0).setDescriptionText( |  | ||||||
|                         getString(R.string.similar_coordinate_description_auto_set)); |  | ||||||
|                     updateMediaDetails(uploadMediaDetailAdapter.getItems()); |  | ||||||
| 
 |  | ||||||
|                     // Replace the 'Add location' button with 'Edit location' button when user clicks |  | ||||||
|                     // yes in similar image dialog |  | ||||||
|                     // fixing: https://github.com/commons-app/apps-android-commons/issues/5669 |  | ||||||
|                     Drawable mapTick = getResources().getDrawable(R.drawable.ic_map_available_20dp); |  | ||||||
|                     binding.locationImageView.setImageDrawable(mapTick); |  | ||||||
|                     binding.locationTextView.setText(R.string.edit_location); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 @Override |  | ||||||
|                 public void onNegativeResponse() { |  | ||||||
|                     Timber.d("negative response from similar image fragment"); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|             Bundle args = new Bundle(); |  | ||||||
|             args.putString("originalImagePath", originalFilePath); |  | ||||||
|             args.putString("possibleImagePath", possibleFilePath); |  | ||||||
|             newFragment.setArguments(args); |  | ||||||
|             newFragment.show(getChildFragmentManager(), "dialog"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onImageProcessed(UploadItem uploadItem, Place place) { |  | ||||||
|         if (binding == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         binding.backgroundImage.setImageURI(uploadItem.getMediaUri()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Sets variables to Show popup if any nearby location needing pictures matches uploadable picture's GPS location |  | ||||||
|      * @param uploadItem |  | ||||||
|      * @param place |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void onNearbyPlaceFound(UploadItem uploadItem, Place place) { |  | ||||||
|         nearbyPlace = place; |  | ||||||
|         this.uploadItem = uploadItem; |  | ||||||
|         showNearbyFound = true; |  | ||||||
|         if (callback == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         if (indexOfFragment == 0) { |  | ||||||
|             if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) { |  | ||||||
|                 final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace); |  | ||||||
|                 if (response) { |  | ||||||
|                     if (callback != null) { |  | ||||||
|                         presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 showNearbyPlaceFound(nearbyPlace); |  | ||||||
|             } |  | ||||||
|             showNearbyFound = false; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Shows nearby place found popup |  | ||||||
|      * @param place |  | ||||||
|      */ |  | ||||||
|     @SuppressLint("StringFormatInvalid") |  | ||||||
|     // To avoid the unwanted lint warning that string 'upload_nearby_place_found_description' is not of a valid format |  | ||||||
|     private void showNearbyPlaceFound(Place place) { |  | ||||||
|         final View customLayout = getLayoutInflater().inflate(R.layout.custom_nearby_found, null); |  | ||||||
|         ImageView nearbyFoundImage = customLayout.findViewById(R.id.nearbyItemImage); |  | ||||||
|         nearbyFoundImage.setImageURI(uploadItem.getMediaUri()); |  | ||||||
| 
 |  | ||||||
|         final Activity activity = getActivity(); |  | ||||||
| 
 |  | ||||||
|         if (activity instanceof UploadActivity) { |  | ||||||
|             final boolean isMultipleFilesSelected = ((UploadActivity) activity).getIsMultipleFilesSelected(); |  | ||||||
| 
 |  | ||||||
|             // Determine the message based on the selection status |  | ||||||
|             String message; |  | ||||||
|             if (isMultipleFilesSelected) { |  | ||||||
|                 // Use plural message if multiple files are selected |  | ||||||
|                 message = String.format(Locale.getDefault(), |  | ||||||
|                     getString(R.string.upload_nearby_place_found_description_plural), |  | ||||||
|                     place.getName()); |  | ||||||
|             } else { |  | ||||||
|                 // Use singular message if only one file is selected |  | ||||||
|                 message = String.format(Locale.getDefault(), |  | ||||||
|                     getString(R.string.upload_nearby_place_found_description_singular), |  | ||||||
|                     place.getName()); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // Show the AlertDialog with the determined message |  | ||||||
|             DialogUtil.showAlertDialog(getActivity(), |  | ||||||
|                 getString(R.string.upload_nearby_place_found_title), |  | ||||||
|                 message, |  | ||||||
|                 () -> { |  | ||||||
|                     // Execute when user confirms the upload is of the specified place |  | ||||||
|                     UploadActivity.nearbyPopupAnswers.put(place, true); |  | ||||||
|                     presenter.onUserConfirmedUploadIsOfPlace(place, indexOfFragment); |  | ||||||
|                 }, |  | ||||||
|                 () -> { |  | ||||||
|                     // Execute when user cancels the upload of the specified place |  | ||||||
|                     UploadActivity.nearbyPopupAnswers.put(place, false); |  | ||||||
|                 }, |  | ||||||
|                 customLayout |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void showProgress(boolean shouldShow) { |  | ||||||
|         if (callback == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         callback.showProgress(shouldShow); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onImageValidationSuccess() { |  | ||||||
|         if (callback == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         callback.onNextButtonClicked(indexOfFragment); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method gets called whenever the next/previous button is pressed |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     protected void onBecameVisible() { |  | ||||||
|         super.onBecameVisible(); |  | ||||||
|         if (callback == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         presenter.fetchTitleAndDescription(indexOfFragment); |  | ||||||
|         if (showNearbyFound) { |  | ||||||
|             if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) { |  | ||||||
|                 final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace); |  | ||||||
|                 if (response) { |  | ||||||
|                     presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment); |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 showNearbyPlaceFound(nearbyPlace); |  | ||||||
|             } |  | ||||||
|             showNearbyFound = false; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void showMessage(int stringResourceId, int colorResourceId) { |  | ||||||
|         ViewUtil.showLongToast(getContext(), stringResourceId); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void showMessage(String message, int colorResourceId) { |  | ||||||
|         ViewUtil.showLongToast(getContext(), message); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void showDuplicatePicturePopup(UploadItem uploadItem) { |  | ||||||
|         if (defaultKvStore.getBoolean("showDuplicatePicturePopup", true)) { |  | ||||||
|             String uploadTitleFormat = getString(R.string.upload_title_duplicate); |  | ||||||
|             View checkBoxView = View |  | ||||||
|                 .inflate(getActivity(), R.layout.nearby_permission_dialog, null); |  | ||||||
|             CheckBox checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again); |  | ||||||
|             checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { |  | ||||||
|                 if (isChecked) { |  | ||||||
|                     defaultKvStore.putBoolean("showDuplicatePicturePopup", false); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|             DialogUtil.showAlertDialog(getActivity(), |  | ||||||
|                 getString(R.string.duplicate_file_name), |  | ||||||
|                 String.format(Locale.getDefault(), |  | ||||||
|                     uploadTitleFormat, |  | ||||||
|                     uploadItem.getFilename()), |  | ||||||
|                 getString(R.string.upload), |  | ||||||
|                 getString(R.string.cancel), |  | ||||||
|                 () -> { |  | ||||||
|                     uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP); |  | ||||||
|                     onImageValidationSuccess(); |  | ||||||
|                 }, null, |  | ||||||
|                 checkBoxView); |  | ||||||
|         } else { |  | ||||||
|             uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP); |  | ||||||
|             onImageValidationSuccess(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Shows a dialog alerting the user that internet connection is required for upload process |  | ||||||
|      * Does nothing if there is network connectivity and then the user presses okay |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void showConnectionErrorPopupForCaptionCheck() { |  | ||||||
|         DialogUtil.showAlertDialog(getActivity(), |  | ||||||
|             getString(R.string.upload_connection_error_alert_title), |  | ||||||
|             getString(R.string.upload_connection_error_alert_detail), |  | ||||||
|             getString(R.string.ok), |  | ||||||
|             getString(R.string.cancel_upload), |  | ||||||
|             () -> { |  | ||||||
|                 if (!NetworkUtils.isInternetConnectionEstablished(activity)) { |  | ||||||
|                     showConnectionErrorPopupForCaptionCheck(); |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             () -> { |  | ||||||
|                 activity.finish(); |  | ||||||
|             }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Shows a dialog alerting the user that internet connection is required for upload process |  | ||||||
|      * Recalls UploadMediaPresenter.getImageQuality for all the next upload items, |  | ||||||
|      * if there is network connectivity and then the user presses okay |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void showConnectionErrorPopup() { |  | ||||||
|         try { |  | ||||||
|             boolean FLAG_ALERT_DIALOG_SHOWING = basicKvStore.getBoolean( |  | ||||||
|                 keyForShowingAlertDialog, false); |  | ||||||
|             if (!FLAG_ALERT_DIALOG_SHOWING) { |  | ||||||
|                 basicKvStore.putBoolean(keyForShowingAlertDialog, true); |  | ||||||
|                 DialogUtil.showAlertDialog(getActivity(), |  | ||||||
|                     getString(R.string.upload_connection_error_alert_title), |  | ||||||
|                     getString(R.string.upload_connection_error_alert_detail), |  | ||||||
|                     getString(R.string.ok), |  | ||||||
|                     getString(R.string.cancel_upload), |  | ||||||
|                     () -> { |  | ||||||
|                         basicKvStore.putBoolean(keyForShowingAlertDialog, false); |  | ||||||
|                         if (NetworkUtils.isInternetConnectionEstablished(activity)) { |  | ||||||
|                             int sizeOfUploads = basicKvStore.getInt( |  | ||||||
|                                 UploadActivity.keyForCurrentUploadImagesSize); |  | ||||||
|                             for (int i = indexOfFragment; i < sizeOfUploads; i++) { |  | ||||||
|                                 presenter.getImageQuality(i, inAppPictureLocation, activity); |  | ||||||
|                             } |  | ||||||
|                         } else { |  | ||||||
|                             showConnectionErrorPopup(); |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     () -> { |  | ||||||
|                         basicKvStore.putBoolean(keyForShowingAlertDialog, false); |  | ||||||
|                         activity.finish(); |  | ||||||
|                     }, |  | ||||||
|                     null |  | ||||||
|                 ); |  | ||||||
|             } |  | ||||||
|         } catch (Exception e) { |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void showExternalMap(final UploadItem uploadItem) { |  | ||||||
|         goToLocationPickerActivity(uploadItem); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Launches the image editing activity to edit the specified UploadItem. |  | ||||||
|      * |  | ||||||
|      * @param uploadItem The UploadItem to be edited. |  | ||||||
|      * |  | ||||||
|      * This method is called to start the image editing activity for a specific UploadItem. |  | ||||||
|      * It sets the UploadItem as the currently editable item, creates an intent to launch the |  | ||||||
|      * EditActivity, and passes the image file path as an extra in the intent. The activity |  | ||||||
|      * is started using resultLauncher that handles the result in respective callback. |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void showEditActivity(UploadItem uploadItem) { |  | ||||||
|         editableUploadItem = uploadItem; |  | ||||||
|         Intent intent = new Intent(getContext(), EditActivity.class); |  | ||||||
|         intent.putExtra("image", uploadableFile.getFilePath().toString()); |  | ||||||
|         startForEditActivityResult.launch(intent); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Start Location picker activity. Show the location first then user can modify it by clicking |  | ||||||
|      * modify location button. |  | ||||||
|      * @param uploadItem current upload item |  | ||||||
|      */ |  | ||||||
|     private void goToLocationPickerActivity(final UploadItem uploadItem) { |  | ||||||
| 
 |  | ||||||
|         editableUploadItem = uploadItem; |  | ||||||
|         double defaultLatitude = 37.773972; |  | ||||||
|         double defaultLongitude = -122.431297; |  | ||||||
|         double defaultZoom = 16.0; |  | ||||||
| 
 |  | ||||||
|         final Intent locationPickerIntent; |  | ||||||
| 
 |  | ||||||
|         /* Retrieve image location from EXIF if present or |  | ||||||
|            check if user has provided location while using the in-app camera. |  | ||||||
|            Use location of last UploadItem if none of them is available */ |  | ||||||
|         if (uploadItem.getGpsCoords() != null && uploadItem.getGpsCoords() |  | ||||||
|             .getDecLatitude() != 0.0 && uploadItem.getGpsCoords().getDecLongitude() != 0.0) { |  | ||||||
|             defaultLatitude = uploadItem.getGpsCoords() |  | ||||||
|                 .getDecLatitude(); |  | ||||||
|             defaultLongitude = uploadItem.getGpsCoords().getDecLongitude(); |  | ||||||
|             defaultZoom = uploadItem.getGpsCoords().getZoomLevel(); |  | ||||||
| 
 |  | ||||||
|             locationPickerIntent = new LocationPicker.IntentBuilder() |  | ||||||
|                 .defaultLocation(new CameraPosition(defaultLatitude,defaultLongitude,defaultZoom)) |  | ||||||
|                 .activityKey("UploadActivity") |  | ||||||
|                 .build(getActivity()); |  | ||||||
|         } else { |  | ||||||
|             if (defaultKvStore.getString(LAST_LOCATION) != null) { |  | ||||||
|                 final String[] locationLatLng |  | ||||||
|                     = defaultKvStore.getString(LAST_LOCATION).split(","); |  | ||||||
|                 defaultLatitude = Double.parseDouble(locationLatLng[0]); |  | ||||||
|                 defaultLongitude = Double.parseDouble(locationLatLng[1]); |  | ||||||
|             } |  | ||||||
|             if (defaultKvStore.getString(LAST_ZOOM) != null) { |  | ||||||
|                 defaultZoom = Double.parseDouble(defaultKvStore.getString(LAST_ZOOM)); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             locationPickerIntent = new LocationPicker.IntentBuilder() |  | ||||||
|                 .defaultLocation(new CameraPosition(defaultLatitude,defaultLongitude,defaultZoom)) |  | ||||||
|                 .activityKey("NoLocationUploadActivity") |  | ||||||
|                 .build(getActivity()); |  | ||||||
|         } |  | ||||||
|         startForResult.launch(locationPickerIntent); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void onCameraPosition(ActivityResult result){ |  | ||||||
|         if (result.getResultCode() == RESULT_OK) { |  | ||||||
| 
 |  | ||||||
|             assert result.getData() != null; |  | ||||||
|             final CameraPosition cameraPosition = LocationPicker.getCameraPosition(result.getData()); |  | ||||||
| 
 |  | ||||||
|             if (cameraPosition != null) { |  | ||||||
| 
 |  | ||||||
|                 final String latitude = String.valueOf(cameraPosition.getLatitude()); |  | ||||||
|                 final String longitude = String.valueOf(cameraPosition.getLongitude()); |  | ||||||
|                 final double zoom = cameraPosition.getZoom(); |  | ||||||
| 
 |  | ||||||
|                 editLocation(latitude, longitude, zoom); |  | ||||||
|                 // If isMissingLocationDialog is true, it means that the user has already tapped the |  | ||||||
|                 // "Next" button, so go directly to the next step. |  | ||||||
|                 if (isMissingLocationDialog) { |  | ||||||
|                     isMissingLocationDialog = false; |  | ||||||
|                     onNextButtonClicked(); |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 // If camera position is null means location is removed by the user |  | ||||||
|                 removeLocation(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void onVoiceInput(ActivityResult result) { |  | ||||||
|         if (result.getResultCode() == RESULT_OK && result.getData() != null) { |  | ||||||
|             ArrayList<String> resultData = result.getData().getStringArrayListExtra( |  | ||||||
|                 RecognizerIntent.EXTRA_RESULTS); |  | ||||||
|             uploadMediaDetailAdapter.handleSpeechResult(resultData.get(0)); |  | ||||||
|         }else { |  | ||||||
|             Timber.e("Error %s", result.getResultCode()); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void onEditActivityResult(ActivityResult result){ |  | ||||||
|         if (result.getResultCode() == RESULT_OK) { |  | ||||||
|             String path = result.getData().getStringExtra("editedImageFilePath"); |  | ||||||
| 
 |  | ||||||
|             if (Objects.equals(result, "Error")) { |  | ||||||
|                 Timber.e("Error in rotating image"); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|             try { |  | ||||||
|                 if (binding != null){ |  | ||||||
|                     binding.backgroundImage.setImageURI(Uri.fromFile(new File(path))); |  | ||||||
|                 } |  | ||||||
|                 editableUploadItem.setContentAndMediaUri(Uri.fromFile(new File(path))); |  | ||||||
|                 callback.changeThumbnail(indexOfFragment, |  | ||||||
|                     path); |  | ||||||
|             } catch (Exception e) { |  | ||||||
|                 Timber.e(e); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Removes the location data from the image, by setting them to null |  | ||||||
|      */ |  | ||||||
|     public void removeLocation() { |  | ||||||
|         editableUploadItem.getGpsCoords().setDecimalCoords(null); |  | ||||||
|         try { |  | ||||||
|             ExifInterface sourceExif = new ExifInterface(uploadableFile.getFilePath()); |  | ||||||
|             String[] exifTags = { |  | ||||||
|                 ExifInterface.TAG_GPS_LATITUDE, |  | ||||||
|                 ExifInterface.TAG_GPS_LATITUDE_REF, |  | ||||||
|                 ExifInterface.TAG_GPS_LONGITUDE, |  | ||||||
|                 ExifInterface.TAG_GPS_LONGITUDE_REF, |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             for (String tag : exifTags) { |  | ||||||
|                 sourceExif.setAttribute(tag, null); |  | ||||||
|             } |  | ||||||
|             sourceExif.saveAttributes(); |  | ||||||
| 
 |  | ||||||
|             Drawable mapQuestion = getResources().getDrawable(R.drawable.ic_map_not_available_20dp); |  | ||||||
| 
 |  | ||||||
|             if (binding != null) { |  | ||||||
|                 binding.locationImageView.setImageDrawable(mapQuestion); |  | ||||||
|                 binding.locationTextView.setText(R.string.add_location); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             editableUploadItem.getGpsCoords().setDecLatitude(0.0); |  | ||||||
|             editableUploadItem.getGpsCoords().setDecLongitude(0.0); |  | ||||||
|             editableUploadItem.getGpsCoords().setImageCoordsExists(false); |  | ||||||
|             hasUserRemovedLocation = true; |  | ||||||
| 
 |  | ||||||
|             Toast.makeText(getContext(), getString(R.string.location_removed), Toast.LENGTH_LONG) |  | ||||||
|                 .show(); |  | ||||||
|         } catch (Exception e) { |  | ||||||
|             Timber.d(e); |  | ||||||
|             Toast.makeText(getContext(), "Location could not be removed due to internal error", |  | ||||||
|                 Toast.LENGTH_LONG).show(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Update the old coordinates with new one |  | ||||||
|      * @param latitude new latitude |  | ||||||
|      * @param longitude new longitude |  | ||||||
|      */ |  | ||||||
|     public void editLocation(final String latitude, final String longitude, final double zoom) { |  | ||||||
| 
 |  | ||||||
|         editableUploadItem.getGpsCoords().setDecLatitude(Double.parseDouble(latitude)); |  | ||||||
|         editableUploadItem.getGpsCoords().setDecLongitude(Double.parseDouble(longitude)); |  | ||||||
|         editableUploadItem.getGpsCoords().setDecimalCoords(latitude + "|" + longitude); |  | ||||||
|         editableUploadItem.getGpsCoords().setImageCoordsExists(true); |  | ||||||
|         editableUploadItem.getGpsCoords().setZoomLevel(zoom); |  | ||||||
| 
 |  | ||||||
|         // Replace the map icon using the one with a green tick |  | ||||||
|         Drawable mapTick = getResources().getDrawable(R.drawable.ic_map_available_20dp); |  | ||||||
| 
 |  | ||||||
|         if (binding != null) { |  | ||||||
|             binding.locationImageView.setImageDrawable(mapTick); |  | ||||||
|             binding.locationTextView.setText(R.string.edit_location); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         Toast.makeText(getContext(), getString(R.string.location_updated), Toast.LENGTH_LONG).show(); |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void updateMediaDetails(List<UploadMediaDetail> uploadMediaDetails) { |  | ||||||
|         uploadMediaDetailAdapter.setItems(uploadMediaDetails); |  | ||||||
|         showNearbyFound = |  | ||||||
|             showNearbyFound && ( |  | ||||||
|                 uploadMediaDetails == null || uploadMediaDetails.isEmpty() |  | ||||||
|                     || listContainsEmptyDetails( |  | ||||||
|                     uploadMediaDetails)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * if the media details that come in here are empty |  | ||||||
|      * (empty caption AND empty description, with caption being the decider here) |  | ||||||
|      * this method allows usage of nearby place caption and description if any |  | ||||||
|      * else it takes the media details saved in prior for this picture |  | ||||||
|      * @param uploadMediaDetails saved media details, |  | ||||||
|      *                           ex: in case when "copy to subsequent media" button is clicked |  | ||||||
|      *                           for a previous image |  | ||||||
|      * @return boolean whether the details are empty or not |  | ||||||
|      */ |  | ||||||
|     private boolean listContainsEmptyDetails(List<UploadMediaDetail> uploadMediaDetails) { |  | ||||||
|         for (UploadMediaDetail uploadDetail: uploadMediaDetails) { |  | ||||||
|             if (!TextUtils.isEmpty(uploadDetail.getCaptionText()) && !TextUtils.isEmpty(uploadDetail.getDescriptionText())) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Showing dialog for adding location |  | ||||||
|      * |  | ||||||
|      * @param onSkipClicked proceed for verifying image quality |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void displayAddLocationDialog(final Runnable onSkipClicked) { |  | ||||||
|         isMissingLocationDialog = true; |  | ||||||
|         DialogUtil.showAlertDialog(requireActivity(), |  | ||||||
|             getString(R.string.no_location_found_title), |  | ||||||
|             getString(R.string.no_location_found_message), |  | ||||||
|             getString(R.string.add_location), |  | ||||||
|             getString(R.string.skip_login), |  | ||||||
|             this::onIbMapClicked, |  | ||||||
|             onSkipClicked); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onDestroyView() { |  | ||||||
|         super.onDestroyView(); |  | ||||||
|         presenter.onDetachView(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void onLlContainerTitleClicked() { |  | ||||||
|         expandCollapseLlMediaDetail(!isExpanded); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * show hide media detail based on |  | ||||||
|      * @param shouldExpand |  | ||||||
|      */ |  | ||||||
|     private void expandCollapseLlMediaDetail(boolean shouldExpand){ |  | ||||||
|         if (binding == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         binding.llContainerMediaDetail.setVisibility(shouldExpand ? View.VISIBLE : View.GONE); |  | ||||||
|         isExpanded = !isExpanded; |  | ||||||
|         binding.ibExpandCollapse.setRotation(binding.ibExpandCollapse.getRotation() + 180); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void onIbMapClicked() { |  | ||||||
|         if (callback == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         presenter.onMapIconClicked(indexOfFragment); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onPrimaryCaptionTextChange(boolean isNotEmpty) { |  | ||||||
|         if (binding == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         binding.btnCopySubsequentMedia.setEnabled(isNotEmpty); |  | ||||||
|         binding.btnCopySubsequentMedia.setClickable(isNotEmpty); |  | ||||||
|         binding.btnCopySubsequentMedia.setAlpha(isNotEmpty ? 1.0f : 0.5f); |  | ||||||
|         binding.btnNext.setEnabled(isNotEmpty); |  | ||||||
|         binding.btnNext.setClickable(isNotEmpty); |  | ||||||
|         binding.btnNext.setAlpha(isNotEmpty ? 1.0f : 0.5f); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Adds new language item to RecyclerView |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void addLanguage() { |  | ||||||
|         UploadMediaDetail uploadMediaDetail = new UploadMediaDetail(); |  | ||||||
|         uploadMediaDetail.setManuallyAdded(true);//This was manually added by the user |  | ||||||
|         uploadMediaDetailAdapter.addDescription(uploadMediaDetail); |  | ||||||
|         binding.rvDescriptions.smoothScrollToPosition(uploadMediaDetailAdapter.getItemCount()-1); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public interface UploadMediaDetailFragmentCallback extends Callback { |  | ||||||
| 
 |  | ||||||
|         void deletePictureAtIndex(int index); |  | ||||||
| 
 |  | ||||||
|         void changeThumbnail(int index, String uri); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     public void onButtonCopyTitleDescToSubsequentMedia(){ |  | ||||||
|         presenter.copyTitleAndDescriptionToSubsequentMedia(indexOfFragment); |  | ||||||
|         Toast.makeText(getContext(), getResources().getString(R.string.copied_successfully), Toast.LENGTH_SHORT).show(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onSaveInstanceState(final Bundle outState) { |  | ||||||
|         super.onSaveInstanceState(outState); |  | ||||||
| 
 |  | ||||||
|         if(uploadableFile!=null){ |  | ||||||
|             outState.putParcelable(UPLOADABLE_FILE,uploadableFile); |  | ||||||
|         } |  | ||||||
|         if(uploadMediaDetailAdapter!=null){ |  | ||||||
|             outState.putParcelableArrayList(UPLOAD_MEDIA_DETAILS, |  | ||||||
|                 (ArrayList<? extends Parcelable>) uploadMediaDetailAdapter.getItems()); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onDestroy() { |  | ||||||
|         super.onDestroy(); |  | ||||||
|         binding = null; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,903 @@ | ||||||
|  | package fr.free.nrw.commons.upload.mediaDetails | ||||||
|  | 
 | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.app.Activity | ||||||
|  | import android.content.Intent | ||||||
|  | import android.net.Uri | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.speech.RecognizerIntent | ||||||
|  | import android.text.TextUtils | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.CheckBox | ||||||
|  | import android.widget.CompoundButton | ||||||
|  | import android.widget.ImageView | ||||||
|  | import android.widget.Toast | ||||||
|  | import androidx.activity.result.ActivityResult | ||||||
|  | import androidx.activity.result.contract.ActivityResultContracts | ||||||
|  | import androidx.core.content.ContextCompat | ||||||
|  | import androidx.core.os.bundleOf | ||||||
|  | import androidx.exifinterface.media.ExifInterface | ||||||
|  | import androidx.recyclerview.widget.LinearLayoutManager | ||||||
|  | import fr.free.nrw.commons.CameraPosition | ||||||
|  | import fr.free.nrw.commons.R | ||||||
|  | import fr.free.nrw.commons.contributions.MainActivity | ||||||
|  | import fr.free.nrw.commons.databinding.FragmentUploadMediaDetailFragmentBinding | ||||||
|  | import fr.free.nrw.commons.edit.EditActivity | ||||||
|  | import fr.free.nrw.commons.filepicker.UploadableFile | ||||||
|  | import fr.free.nrw.commons.kvstore.BasicKvStore | ||||||
|  | import fr.free.nrw.commons.kvstore.JsonKvStore | ||||||
|  | import fr.free.nrw.commons.location.LatLng | ||||||
|  | import fr.free.nrw.commons.locationpicker.LocationPicker | ||||||
|  | import fr.free.nrw.commons.locationpicker.LocationPicker.getCameraPosition | ||||||
|  | import fr.free.nrw.commons.nearby.Place | ||||||
|  | import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao | ||||||
|  | import fr.free.nrw.commons.settings.Prefs | ||||||
|  | import fr.free.nrw.commons.upload.ImageCoordinates | ||||||
|  | import fr.free.nrw.commons.upload.SimilarImageDialogFragment | ||||||
|  | import fr.free.nrw.commons.upload.UploadActivity | ||||||
|  | import fr.free.nrw.commons.upload.UploadBaseFragment | ||||||
|  | import fr.free.nrw.commons.upload.UploadItem | ||||||
|  | import fr.free.nrw.commons.upload.UploadMediaDetail | ||||||
|  | import fr.free.nrw.commons.upload.UploadMediaDetailAdapter | ||||||
|  | import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter.Companion.presenterCallback | ||||||
|  | import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags | ||||||
|  | import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | ||||||
|  | import fr.free.nrw.commons.utils.ImageUtils | ||||||
|  | import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK | ||||||
|  | import fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult | ||||||
|  | import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished | ||||||
|  | import fr.free.nrw.commons.utils.ViewUtil.showLongToast | ||||||
|  | import timber.log.Timber | ||||||
|  | import java.io.File | ||||||
|  | import java.util.ArrayList | ||||||
|  | import java.util.Locale | ||||||
|  | import java.util.Objects | ||||||
|  | import javax.inject.Inject | ||||||
|  | import javax.inject.Named | ||||||
|  | 
 | ||||||
|  | class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContract.View, | ||||||
|  |     UploadMediaDetailAdapter.EventListener { | ||||||
|  | 
 | ||||||
|  |     private val startForResult = registerForActivityResult<Intent, ActivityResult>( | ||||||
|  |         ActivityResultContracts.StartActivityForResult(), ::onCameraPosition) | ||||||
|  | 
 | ||||||
|  |     private val startForEditActivityResult = registerForActivityResult<Intent, ActivityResult>( | ||||||
|  |         ActivityResultContracts.StartActivityForResult(), ::onEditActivityResult) | ||||||
|  | 
 | ||||||
|  |     private val voiceInputResultLauncher = registerForActivityResult<Intent, ActivityResult>( | ||||||
|  |         ActivityResultContracts.StartActivityForResult(), ::onVoiceInput) | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     lateinit var presenter: UploadMediaDetailsContract.UserActionListener | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     @field:Named("default_preferences") | ||||||
|  |     lateinit var defaultKvStore: JsonKvStore | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     lateinit var recentLanguagesDao: RecentLanguagesDao | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * True when user removes location from the current image | ||||||
|  |      */ | ||||||
|  |     var hasUserRemovedLocation = false | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * True if location is added via the "missing location" popup dialog (which appears after | ||||||
|  |      * tapping "Next" if the picture has no geographical coordinates). | ||||||
|  |      */ | ||||||
|  |     private var isMissingLocationDialog = false | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * showNearbyFound will be true, if any nearby location found that needs pictures and the nearby | ||||||
|  |      * popup is yet to be shown Used to show and check if the nearby found popup is already shown | ||||||
|  |      */ | ||||||
|  |     private var showNearbyFound = false | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * nearbyPlace holds the detail of nearby place that need pictures, if any found | ||||||
|  |      */ | ||||||
|  |     private var nearbyPlace: Place? = null | ||||||
|  |     private var uploadItem: UploadItem? = null | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * inAppPictureLocation: use location recorded while using the in-app camera if device camera | ||||||
|  |      * does not record it in the EXIF | ||||||
|  |      */ | ||||||
|  |     var inAppPictureLocation: LatLng? = null | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * editableUploadItem : Storing the upload item before going to update the coordinates | ||||||
|  |      */ | ||||||
|  |     private var editableUploadItem: UploadItem? = null | ||||||
|  | 
 | ||||||
|  |     private var _binding: FragmentUploadMediaDetailFragmentBinding? = null | ||||||
|  |     private val binding: FragmentUploadMediaDetailFragmentBinding get() = _binding!! | ||||||
|  | 
 | ||||||
|  |     private var basicKvStore: BasicKvStore? = null | ||||||
|  |     private val keyForShowingAlertDialog = "isNoNetworkAlertDialogShowing" | ||||||
|  |     private var uploadableFile: UploadableFile? = null | ||||||
|  |     private var place: Place? = null | ||||||
|  |     private lateinit var uploadMediaDetailAdapter: UploadMediaDetailAdapter | ||||||
|  |     var indexOfFragment = 0 | ||||||
|  |     var isExpanded = true | ||||||
|  |     var fragmentCallback: UploadMediaDetailFragmentCallback? = null | ||||||
|  |         set(value) { | ||||||
|  |             field = value | ||||||
|  |             UploadMediaPresenter.presenterCallback = value | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  | 
 | ||||||
|  |         if (savedInstanceState != null && uploadableFile == null) { | ||||||
|  |             uploadableFile = savedInstanceState.getParcelable(UPLOADABLE_FILE) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun setImageToBeUploaded( | ||||||
|  |         uploadableFile: UploadableFile?, place: Place?, inAppPictureLocation: LatLng? | ||||||
|  |     ) { | ||||||
|  |         this.uploadableFile = uploadableFile | ||||||
|  |         this.place = place | ||||||
|  |         this.inAppPictureLocation = inAppPictureLocation | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onCreateView( | ||||||
|  |         inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? | ||||||
|  |     ): View { | ||||||
|  |         _binding = FragmentUploadMediaDetailFragmentBinding.inflate(inflater, container, false) | ||||||
|  |         return binding.root | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  | 
 | ||||||
|  |         basicKvStore = BasicKvStore(requireActivity(), "CurrentUploadImageQualities") | ||||||
|  | 
 | ||||||
|  |         if (fragmentCallback != null) { | ||||||
|  |             indexOfFragment = fragmentCallback!!.getIndexInViewFlipper(this) | ||||||
|  |             initializeFragment() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (savedInstanceState != null) { | ||||||
|  |             if (uploadMediaDetailAdapter.items.isEmpty() && fragmentCallback != null) { | ||||||
|  |                 uploadMediaDetailAdapter.items = savedInstanceState.getParcelableArrayList(UPLOAD_MEDIA_DETAILS)!! | ||||||
|  |                 presenter.setUploadMediaDetails(uploadMediaDetailAdapter.items, indexOfFragment) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             if (!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, requireActivity())) { | ||||||
|  |                 startActivityWithFlags( | ||||||
|  |                     requireActivity(), | ||||||
|  |                     MainActivity::class.java, | ||||||
|  |                     Intent.FLAG_ACTIVITY_CLEAR_TOP, | ||||||
|  |                     Intent.FLAG_ACTIVITY_SINGLE_TOP | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } catch (_: Exception) { | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun initializeFragment() { | ||||||
|  |         if (_binding == null) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         binding.tvTitle.text = getString( | ||||||
|  |             R.string.step_count, (indexOfFragment + 1), | ||||||
|  |             fragmentCallback!!.totalNumberOfSteps, getString(R.string.media_detail_step_title) | ||||||
|  |         ) | ||||||
|  |         binding.tooltip.setOnClickListener { | ||||||
|  |             showInfoAlert( | ||||||
|  |                 R.string.media_detail_step_title, | ||||||
|  |                 R.string.media_details_tooltip | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         presenter.onAttachView(this) | ||||||
|  |         presenter.setupBasicKvStoreFactory { BasicKvStore(requireActivity(), it) } | ||||||
|  | 
 | ||||||
|  |         presenter.receiveImage(uploadableFile, place, inAppPictureLocation) | ||||||
|  |         initRecyclerView() | ||||||
|  | 
 | ||||||
|  |         with (binding){ | ||||||
|  |             if (indexOfFragment == 0) { | ||||||
|  |                 btnPrevious.isEnabled = false | ||||||
|  |                 btnPrevious.alpha = 0.5f | ||||||
|  |             } else { | ||||||
|  |                 btnPrevious.isEnabled = true | ||||||
|  |                 btnPrevious.alpha = 1.0f | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // If the image EXIF data contains the location, show the map icon with a green tick | ||||||
|  |             if (inAppPictureLocation != null || (uploadableFile != null && uploadableFile!!.hasLocation())) { | ||||||
|  |                 val mapTick = | ||||||
|  |                     ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_available_20dp) | ||||||
|  |                 locationImageView.setImageDrawable(mapTick) | ||||||
|  |                 locationTextView.setText(R.string.edit_location) | ||||||
|  |             } else { | ||||||
|  |                 // Otherwise, show the map icon with a red question mark | ||||||
|  |                 val mapQuestionMark = ContextCompat.getDrawable( | ||||||
|  |                     requireContext(), | ||||||
|  |                     R.drawable.ic_map_not_available_20dp | ||||||
|  |                 ) | ||||||
|  |                 locationImageView.setImageDrawable(mapQuestionMark) | ||||||
|  |                 locationTextView.setText(R.string.add_location) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             //If this is the last media, we have nothing to copy, lets not show the button | ||||||
|  |             btnCopySubsequentMedia.visibility = | ||||||
|  |                 if (indexOfFragment == fragmentCallback!!.totalNumberOfSteps - 4) { | ||||||
|  |                     View.GONE | ||||||
|  |                 } else { | ||||||
|  |                     View.VISIBLE | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |             btnNext.setOnClickListener { presenter.displayLocDialog(indexOfFragment, inAppPictureLocation, hasUserRemovedLocation) } | ||||||
|  |             btnPrevious.setOnClickListener { fragmentCallback?.onPreviousButtonClicked(indexOfFragment) } | ||||||
|  |             llEditImage.setOnClickListener { presenter.onEditButtonClicked(indexOfFragment) } | ||||||
|  |             llContainerTitle.setOnClickListener { expandCollapseLlMediaDetail(!isExpanded) } | ||||||
|  |             llLocationStatus.setOnClickListener { presenter.onMapIconClicked(indexOfFragment) } | ||||||
|  |             btnCopySubsequentMedia.setOnClickListener { onButtonCopyTitleDescToSubsequentMedia() } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         attachImageViewScaleChangeListener() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Attaches the scale change listener to the image view | ||||||
|  |      */ | ||||||
|  |     private fun attachImageViewScaleChangeListener() { | ||||||
|  |         binding.backgroundImage.setOnScaleChangeListener { _: Float, _: Float, _: Float -> | ||||||
|  |             //Whenever the uses plays with the image, lets collapse the media detail container | ||||||
|  |             //only if it is not already collapsed, which resolves flickering of arrow | ||||||
|  |             if (isExpanded) { | ||||||
|  |                 expandCollapseLlMediaDetail(false) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * init the description recycler veiw and caption recyclerview | ||||||
|  |      */ | ||||||
|  |     private fun initRecyclerView() { | ||||||
|  |         uploadMediaDetailAdapter = UploadMediaDetailAdapter( | ||||||
|  |             this, | ||||||
|  |             defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, "")!!, | ||||||
|  |             recentLanguagesDao, voiceInputResultLauncher | ||||||
|  |         ) | ||||||
|  |         uploadMediaDetailAdapter.callback = | ||||||
|  |             UploadMediaDetailAdapter.Callback { titleStringID: Int, messageStringId: Int -> | ||||||
|  |                 showInfoAlert(titleStringID, messageStringId) | ||||||
|  |             } | ||||||
|  |         uploadMediaDetailAdapter.eventListener = this | ||||||
|  |         binding.rvDescriptions.layoutManager = LinearLayoutManager(context) | ||||||
|  |         binding.rvDescriptions.adapter = uploadMediaDetailAdapter | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun showInfoAlert(titleStringID: Int, messageStringId: Int) { | ||||||
|  |         showAlertDialog( | ||||||
|  |             requireActivity(), | ||||||
|  |             getString(titleStringID), | ||||||
|  |             getString(messageStringId), | ||||||
|  |             getString(android.R.string.ok), | ||||||
|  |             null | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun showSimilarImageFragment( | ||||||
|  |         originalFilePath: String?, possibleFilePath: String?, | ||||||
|  |         similarImageCoordinates: ImageCoordinates? | ||||||
|  |     ) { | ||||||
|  |         val basicKvStore = BasicKvStore(requireActivity(), "IsAnyImageCancelled") | ||||||
|  |         if (!basicKvStore.getBoolean("IsAnyImageCancelled", false)) { | ||||||
|  |             val newFragment = SimilarImageDialogFragment() | ||||||
|  |             newFragment.isCancelable = false | ||||||
|  |             newFragment.callback = object : SimilarImageDialogFragment.Callback { | ||||||
|  |                 override fun onPositiveResponse() { | ||||||
|  |                     Timber.d("positive response from similar image fragment") | ||||||
|  |                     presenter.useSimilarPictureCoordinates( | ||||||
|  |                         similarImageCoordinates!!, | ||||||
|  |                         indexOfFragment | ||||||
|  |                     ) | ||||||
|  | 
 | ||||||
|  |                     // set the description text when user selects to use coordinate from the other image | ||||||
|  |                     // which was taken within 120s | ||||||
|  |                     // fixing: https://github.com/commons-app/apps-android-commons/issues/4700 | ||||||
|  |                     uploadMediaDetailAdapter.items[0].descriptionText = | ||||||
|  |                         getString(R.string.similar_coordinate_description_auto_set) | ||||||
|  |                     updateMediaDetails(uploadMediaDetailAdapter.items) | ||||||
|  | 
 | ||||||
|  |                     // Replace the 'Add location' button with 'Edit location' button when user clicks | ||||||
|  |                     // yes in similar image dialog | ||||||
|  |                     // fixing: https://github.com/commons-app/apps-android-commons/issues/5669 | ||||||
|  |                     val mapTick = ContextCompat.getDrawable( | ||||||
|  |                         requireContext(), | ||||||
|  |                         R.drawable.ic_map_available_20dp | ||||||
|  |                     ) | ||||||
|  |                     binding.locationImageView.setImageDrawable(mapTick) | ||||||
|  |                     binding.locationTextView.setText(R.string.edit_location) | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 override fun onNegativeResponse() { | ||||||
|  |                     Timber.d("negative response from similar image fragment") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             newFragment.arguments = bundleOf( | ||||||
|  |                 "originalImagePath" to originalFilePath, | ||||||
|  |                 "possibleImagePath" to possibleFilePath | ||||||
|  |             ) | ||||||
|  |             newFragment.show(childFragmentManager, "dialog") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onImageProcessed(uploadItem: UploadItem) { | ||||||
|  |         if (_binding == null) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         binding.backgroundImage.setImageURI(uploadItem.mediaUri) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onNearbyPlaceFound( | ||||||
|  |         uploadItem: UploadItem, place: Place? | ||||||
|  |     ) { | ||||||
|  |         nearbyPlace = place | ||||||
|  |         this.uploadItem = uploadItem | ||||||
|  |         showNearbyFound = true | ||||||
|  |         if (fragmentCallback == null) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         if (indexOfFragment == 0) { | ||||||
|  |             if (UploadActivity.nearbyPopupAnswers!!.containsKey(nearbyPlace!!)) { | ||||||
|  |                 val response = UploadActivity.nearbyPopupAnswers!![nearbyPlace!!]!! | ||||||
|  |                 if (response) { | ||||||
|  |                     if (fragmentCallback != null) { | ||||||
|  |                         presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 showNearbyPlaceFound(nearbyPlace!!) | ||||||
|  |             } | ||||||
|  |             showNearbyFound = false | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @SuppressLint("StringFormatInvalid") // To avoid the unwanted lint warning that string 'upload_nearby_place_found_description' is not of a valid format | ||||||
|  |     private fun showNearbyPlaceFound(place: Place) { | ||||||
|  |         val customLayout = layoutInflater.inflate(R.layout.custom_nearby_found, null) | ||||||
|  |         val nearbyFoundImage = customLayout.findViewById<ImageView>(R.id.nearbyItemImage) | ||||||
|  |         nearbyFoundImage.setImageURI(uploadItem!!.mediaUri) | ||||||
|  | 
 | ||||||
|  |         val activity: Activity? = activity | ||||||
|  | 
 | ||||||
|  |         if (activity is UploadActivity) { | ||||||
|  |             val isMultipleFilesSelected = activity.isMultipleFilesSelected | ||||||
|  | 
 | ||||||
|  |             // Determine the message based on the selection status | ||||||
|  |             val message = if (isMultipleFilesSelected) { | ||||||
|  |                 // Use plural message if multiple files are selected | ||||||
|  |                 String.format( | ||||||
|  |                     Locale.getDefault(), | ||||||
|  |                     getString(R.string.upload_nearby_place_found_description_plural), | ||||||
|  |                     place.getName() | ||||||
|  |                 ) | ||||||
|  |             } else { | ||||||
|  |                 // Use singular message if only one file is selected | ||||||
|  |                 String.format( | ||||||
|  |                     Locale.getDefault(), | ||||||
|  |                     getString(R.string.upload_nearby_place_found_description_singular), | ||||||
|  |                     place.getName() | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Show the AlertDialog with the determined message | ||||||
|  |             showAlertDialog( | ||||||
|  |                 requireActivity(), | ||||||
|  |                 getString(R.string.upload_nearby_place_found_title), | ||||||
|  |                 message, | ||||||
|  |                 { | ||||||
|  |                     // Execute when user confirms the upload is of the specified place | ||||||
|  |                     UploadActivity.nearbyPopupAnswers!![place] = true | ||||||
|  |                     presenter.onUserConfirmedUploadIsOfPlace(place, indexOfFragment) | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     // Execute when user cancels the upload of the specified place | ||||||
|  |                     UploadActivity.nearbyPopupAnswers!![place] = false | ||||||
|  |                 }, | ||||||
|  |                 customLayout | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun showProgress(shouldShow: Boolean) { | ||||||
|  |         if (fragmentCallback == null) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         fragmentCallback!!.showProgress(shouldShow) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onImageValidationSuccess() { | ||||||
|  |         if (fragmentCallback == null) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         fragmentCallback!!.onNextButtonClicked(indexOfFragment) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method gets called whenever the next/previous button is pressed | ||||||
|  |      */ | ||||||
|  |     override fun onBecameVisible() { | ||||||
|  |         super.onBecameVisible() | ||||||
|  |         if (fragmentCallback == null) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         presenter.fetchTitleAndDescription(indexOfFragment) | ||||||
|  |         if (showNearbyFound) { | ||||||
|  |             if (UploadActivity.nearbyPopupAnswers!!.containsKey(nearbyPlace!!)) { | ||||||
|  |                 val response = UploadActivity.nearbyPopupAnswers!![nearbyPlace!!]!! | ||||||
|  |                 if (response) { | ||||||
|  |                     presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment) | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 showNearbyPlaceFound(nearbyPlace!!) | ||||||
|  |             } | ||||||
|  |             showNearbyFound = false | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun showMessage(stringResourceId: Int, colorResourceId: Int) = | ||||||
|  |         showLongToast(requireContext(), stringResourceId) | ||||||
|  | 
 | ||||||
|  |     override fun showMessage(message: String, colorResourceId: Int) = | ||||||
|  |         showLongToast(requireContext(), message) | ||||||
|  | 
 | ||||||
|  |     override fun showDuplicatePicturePopup(uploadItem: UploadItem) { | ||||||
|  |         if (defaultKvStore.getBoolean("showDuplicatePicturePopup", true)) { | ||||||
|  |             val uploadTitleFormat = getString(R.string.upload_title_duplicate) | ||||||
|  |             val checkBoxView = View | ||||||
|  |                 .inflate(activity, R.layout.nearby_permission_dialog, null) | ||||||
|  |             val checkBox = checkBoxView.findViewById<View>(R.id.never_ask_again) as CheckBox | ||||||
|  |             checkBox.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean -> | ||||||
|  |                 if (isChecked) { | ||||||
|  |                     defaultKvStore.putBoolean("showDuplicatePicturePopup", false) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             showAlertDialog( | ||||||
|  |                 requireActivity(), | ||||||
|  |                 getString(R.string.duplicate_file_name), | ||||||
|  |                 String.format( | ||||||
|  |                     Locale.getDefault(), | ||||||
|  |                     uploadTitleFormat, | ||||||
|  |                     uploadItem.filename | ||||||
|  |                 ), | ||||||
|  |                 getString(R.string.upload), | ||||||
|  |                 getString(R.string.cancel), | ||||||
|  |                 { | ||||||
|  |                     uploadItem.imageQuality = ImageUtils.IMAGE_KEEP | ||||||
|  |                     onImageValidationSuccess() | ||||||
|  |                 }, null, | ||||||
|  |                 checkBoxView | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             uploadItem.imageQuality = ImageUtils.IMAGE_KEEP | ||||||
|  |             onImageValidationSuccess() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Shows a dialog alerting the user that internet connection is required for upload process | ||||||
|  |      * Does nothing if there is network connectivity and then the user presses okay | ||||||
|  |      */ | ||||||
|  |     override fun showConnectionErrorPopupForCaptionCheck() { | ||||||
|  |         showAlertDialog(requireActivity(), | ||||||
|  |             getString(R.string.upload_connection_error_alert_title), | ||||||
|  |             getString(R.string.upload_connection_error_alert_detail), | ||||||
|  |             getString(R.string.ok), | ||||||
|  |             getString(R.string.cancel_upload), | ||||||
|  |             { | ||||||
|  |                 if (!isInternetConnectionEstablished(requireActivity())) { | ||||||
|  |                     showConnectionErrorPopupForCaptionCheck() | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 requireActivity().finish() | ||||||
|  |             }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Shows a dialog alerting the user that internet connection is required for upload process | ||||||
|  |      * Recalls UploadMediaPresenter.getImageQuality for all the next upload items, | ||||||
|  |      * if there is network connectivity and then the user presses okay | ||||||
|  |      */ | ||||||
|  |     override fun showConnectionErrorPopup() { | ||||||
|  |         try { | ||||||
|  |             val FLAG_ALERT_DIALOG_SHOWING = basicKvStore!!.getBoolean( | ||||||
|  |                 keyForShowingAlertDialog, false | ||||||
|  |             ) | ||||||
|  |             if (!FLAG_ALERT_DIALOG_SHOWING) { | ||||||
|  |                 basicKvStore!!.putBoolean(keyForShowingAlertDialog, true) | ||||||
|  |                 showAlertDialog( | ||||||
|  |                     requireActivity(), | ||||||
|  |                     getString(R.string.upload_connection_error_alert_title), | ||||||
|  |                     getString(R.string.upload_connection_error_alert_detail), | ||||||
|  |                     getString(R.string.ok), | ||||||
|  |                     getString(R.string.cancel_upload), | ||||||
|  |                     { | ||||||
|  |                         basicKvStore!!.putBoolean(keyForShowingAlertDialog, false) | ||||||
|  |                         if (isInternetConnectionEstablished(requireActivity())) { | ||||||
|  |                             val sizeOfUploads = basicKvStore!!.getInt( | ||||||
|  |                                 UploadActivity.keyForCurrentUploadImagesSize | ||||||
|  |                             ) | ||||||
|  |                             for (i in indexOfFragment until sizeOfUploads) { | ||||||
|  |                                 presenter.getImageQuality( | ||||||
|  |                                     i, | ||||||
|  |                                     inAppPictureLocation, | ||||||
|  |                                     requireActivity() | ||||||
|  |                                 ) | ||||||
|  |                             } | ||||||
|  |                         } else { | ||||||
|  |                             showConnectionErrorPopup() | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         basicKvStore!!.putBoolean(keyForShowingAlertDialog, false) | ||||||
|  |                         requireActivity().finish() | ||||||
|  |                     }, | ||||||
|  |                     null | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             Timber.e(e) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun showExternalMap(uploadItem: UploadItem) = | ||||||
|  |         goToLocationPickerActivity(uploadItem) | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Launches the image editing activity to edit the specified UploadItem. | ||||||
|  |      * | ||||||
|  |      * @param uploadItem The UploadItem to be edited. | ||||||
|  |      * | ||||||
|  |      * This method is called to start the image editing activity for a specific UploadItem. | ||||||
|  |      * It sets the UploadItem as the currently editable item, creates an intent to launch the | ||||||
|  |      * EditActivity, and passes the image file path as an extra in the intent. The activity | ||||||
|  |      * is started using resultLauncher that handles the result in respective callback. | ||||||
|  |      */ | ||||||
|  |     override fun showEditActivity(uploadItem: UploadItem) { | ||||||
|  |         editableUploadItem = uploadItem | ||||||
|  |         val intent = Intent(context, EditActivity::class.java) | ||||||
|  |         intent.putExtra("image", uploadableFile!!.getFilePath().toString()) | ||||||
|  |         startForEditActivityResult.launch(intent) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Start Location picker activity. Show the location first then user can modify it by clicking | ||||||
|  |      * modify location button. | ||||||
|  |      * @param uploadItem current upload item | ||||||
|  |      */ | ||||||
|  |     private fun goToLocationPickerActivity(uploadItem: UploadItem) { | ||||||
|  |         editableUploadItem = uploadItem | ||||||
|  |         var defaultLatitude = 37.773972 | ||||||
|  |         var defaultLongitude = -122.431297 | ||||||
|  |         var defaultZoom = 16.0 | ||||||
|  | 
 | ||||||
|  |         val locationPickerIntent: Intent | ||||||
|  | 
 | ||||||
|  |         /* Retrieve image location from EXIF if present or | ||||||
|  |            check if user has provided location while using the in-app camera. | ||||||
|  |            Use location of last UploadItem if none of them is available */ | ||||||
|  |         if (uploadItem.gpsCoords != null && uploadItem.gpsCoords!! | ||||||
|  |                 .decLatitude != 0.0 && uploadItem.gpsCoords!!.decLongitude != 0.0 | ||||||
|  |         ) { | ||||||
|  |             defaultLatitude = uploadItem.gpsCoords!! | ||||||
|  |                 .decLatitude | ||||||
|  |             defaultLongitude = uploadItem.gpsCoords!!.decLongitude | ||||||
|  |             defaultZoom = uploadItem.gpsCoords!!.zoomLevel | ||||||
|  | 
 | ||||||
|  |             locationPickerIntent = LocationPicker.IntentBuilder() | ||||||
|  |                 .defaultLocation(CameraPosition(defaultLatitude, defaultLongitude, defaultZoom)) | ||||||
|  |                 .activityKey("UploadActivity") | ||||||
|  |                 .build(requireActivity()) | ||||||
|  |         } else { | ||||||
|  |             if (defaultKvStore.getString(LAST_LOCATION) != null) { | ||||||
|  |                 val locationLatLng = defaultKvStore.getString(LAST_LOCATION)!! | ||||||
|  |                     .split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() | ||||||
|  |                 defaultLatitude = locationLatLng[0].toDouble() | ||||||
|  |                 defaultLongitude = locationLatLng[1].toDouble() | ||||||
|  |             } | ||||||
|  |             if (defaultKvStore.getString(LAST_ZOOM) != null) { | ||||||
|  |                 defaultZoom = defaultKvStore.getString(LAST_ZOOM)!! | ||||||
|  |                     .toDouble() | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             locationPickerIntent = LocationPicker.IntentBuilder() | ||||||
|  |                 .defaultLocation(CameraPosition(defaultLatitude, defaultLongitude, defaultZoom)) | ||||||
|  |                 .activityKey("NoLocationUploadActivity") | ||||||
|  |                 .build(requireActivity()) | ||||||
|  |         } | ||||||
|  |         startForResult.launch(locationPickerIntent) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun onCameraPosition(result: ActivityResult) { | ||||||
|  |         if (result.resultCode == Activity.RESULT_OK) { | ||||||
|  |             checkNotNull(result.data) | ||||||
|  |             val cameraPosition = getCameraPosition( | ||||||
|  |                 result.data!! | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             if (cameraPosition != null) { | ||||||
|  |                 val latitude = cameraPosition.latitude.toString() | ||||||
|  |                 val longitude = cameraPosition.longitude.toString() | ||||||
|  |                 val zoom = cameraPosition.zoom | ||||||
|  | 
 | ||||||
|  |                 editLocation(latitude, longitude, zoom) | ||||||
|  |                 // If isMissingLocationDialog is true, it means that the user has already tapped the | ||||||
|  |                 // "Next" button, so go directly to the next step. | ||||||
|  |                 if (isMissingLocationDialog) { | ||||||
|  |                     isMissingLocationDialog = false | ||||||
|  |                     presenter.displayLocDialog( | ||||||
|  |                         indexOfFragment, | ||||||
|  |                         inAppPictureLocation, | ||||||
|  |                         hasUserRemovedLocation | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 // If camera position is null means location is removed by the user | ||||||
|  |                 removeLocation() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun onVoiceInput(result: ActivityResult) { | ||||||
|  |         if (result.resultCode == Activity.RESULT_OK && result.data != null) { | ||||||
|  |             val resultData = result.data!!.getStringArrayListExtra( | ||||||
|  |                 RecognizerIntent.EXTRA_RESULTS | ||||||
|  |             ) | ||||||
|  |             uploadMediaDetailAdapter.handleSpeechResult(resultData!![0]) | ||||||
|  |         } else { | ||||||
|  |             Timber.e("Error %s", result.resultCode) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun onEditActivityResult(result: ActivityResult) { | ||||||
|  |         if (result.resultCode == Activity.RESULT_OK) { | ||||||
|  |             val path = result.data!!.getStringExtra("editedImageFilePath") | ||||||
|  | 
 | ||||||
|  |             if (Objects.equals(result, "Error")) { | ||||||
|  |                 Timber.e("Error in rotating image") | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |             try { | ||||||
|  |                 if (_binding != null) { | ||||||
|  |                     binding.backgroundImage.setImageURI(Uri.fromFile(File(path!!))) | ||||||
|  |                 } | ||||||
|  |                 editableUploadItem!!.setContentAndMediaUri(Uri.fromFile(File(path!!))) | ||||||
|  |                 fragmentCallback!!.changeThumbnail( | ||||||
|  |                     indexOfFragment, | ||||||
|  |                     path | ||||||
|  |                 ) | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 Timber.e(e) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Removes the location data from the image, by setting them to null | ||||||
|  |      */ | ||||||
|  |     private fun removeLocation() { | ||||||
|  |         editableUploadItem!!.gpsCoords!!.decimalCoords = null | ||||||
|  |         try { | ||||||
|  |             val sourceExif = ExifInterface( | ||||||
|  |                 uploadableFile!!.getFilePath() | ||||||
|  |             ) | ||||||
|  |             val exifTags = arrayOf( | ||||||
|  |                 ExifInterface.TAG_GPS_LATITUDE, | ||||||
|  |                 ExifInterface.TAG_GPS_LATITUDE_REF, | ||||||
|  |                 ExifInterface.TAG_GPS_LONGITUDE, | ||||||
|  |                 ExifInterface.TAG_GPS_LONGITUDE_REF, | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             for (tag in exifTags) { | ||||||
|  |                 sourceExif.setAttribute(tag, null) | ||||||
|  |             } | ||||||
|  |             sourceExif.saveAttributes() | ||||||
|  | 
 | ||||||
|  |             val mapQuestion = | ||||||
|  |                 ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_not_available_20dp) | ||||||
|  | 
 | ||||||
|  |             if (_binding != null) { | ||||||
|  |                 binding.locationImageView.setImageDrawable(mapQuestion) | ||||||
|  |                 binding.locationTextView.setText(R.string.add_location) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             editableUploadItem!!.gpsCoords!!.decLatitude = 0.0 | ||||||
|  |             editableUploadItem!!.gpsCoords!!.decLongitude = 0.0 | ||||||
|  |             editableUploadItem!!.gpsCoords!!.imageCoordsExists = false | ||||||
|  |             hasUserRemovedLocation = true | ||||||
|  | 
 | ||||||
|  |             Toast.makeText(context, getString(R.string.location_removed), Toast.LENGTH_LONG) | ||||||
|  |                 .show() | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             Timber.d(e) | ||||||
|  |             Toast.makeText( | ||||||
|  |                 context, "Location could not be removed due to internal error", | ||||||
|  |                 Toast.LENGTH_LONG | ||||||
|  |             ).show() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Update the old coordinates with new one | ||||||
|  |      * @param latitude new latitude | ||||||
|  |      * @param longitude new longitude | ||||||
|  |      */ | ||||||
|  |     fun editLocation(latitude: String, longitude: String, zoom: Double) { | ||||||
|  |         editableUploadItem!!.gpsCoords!!.decLatitude = latitude.toDouble() | ||||||
|  |         editableUploadItem!!.gpsCoords!!.decLongitude = longitude.toDouble() | ||||||
|  |         editableUploadItem!!.gpsCoords!!.decimalCoords = "$latitude|$longitude" | ||||||
|  |         editableUploadItem!!.gpsCoords!!.imageCoordsExists = true | ||||||
|  |         editableUploadItem!!.gpsCoords!!.zoomLevel = zoom | ||||||
|  | 
 | ||||||
|  |         // Replace the map icon using the one with a green tick | ||||||
|  |         val mapTick = ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_available_20dp) | ||||||
|  | 
 | ||||||
|  |         if (_binding != null) { | ||||||
|  |             binding.locationImageView.setImageDrawable(mapTick) | ||||||
|  |             binding.locationTextView.setText(R.string.edit_location) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Toast.makeText(context, getString(R.string.location_updated), Toast.LENGTH_LONG).show() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun updateMediaDetails(uploadMediaDetails: List<UploadMediaDetail>) { | ||||||
|  |         uploadMediaDetailAdapter.items = uploadMediaDetails | ||||||
|  |         showNearbyFound = | ||||||
|  |             showNearbyFound && (uploadMediaDetails.isEmpty() || listContainsEmptyDetails( | ||||||
|  |                 uploadMediaDetails | ||||||
|  |             )) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * if the media details that come in here are empty | ||||||
|  |      * (empty caption AND empty description, with caption being the decider here) | ||||||
|  |      * this method allows usage of nearby place caption and description if any | ||||||
|  |      * else it takes the media details saved in prior for this picture | ||||||
|  |      * @param uploadMediaDetails saved media details, | ||||||
|  |      * ex: in case when "copy to subsequent media" button is clicked | ||||||
|  |      * for a previous image | ||||||
|  |      * @return boolean whether the details are empty or not | ||||||
|  |      */ | ||||||
|  |     private fun listContainsEmptyDetails(uploadMediaDetails: List<UploadMediaDetail>): Boolean { | ||||||
|  |         for ((_, descriptionText, captionText) in uploadMediaDetails) { | ||||||
|  |             if (!TextUtils.isEmpty(captionText) && !TextUtils.isEmpty( | ||||||
|  |                     descriptionText | ||||||
|  |                 ) | ||||||
|  |             ) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Showing dialog for adding location | ||||||
|  |      * | ||||||
|  |      * @param runnable proceed for verifying image quality | ||||||
|  |      */ | ||||||
|  |     override fun displayAddLocationDialog(runnable: Runnable) { | ||||||
|  |         isMissingLocationDialog = true | ||||||
|  |         showAlertDialog( | ||||||
|  |             requireActivity(), | ||||||
|  |             getString(R.string.no_location_found_title), | ||||||
|  |             getString(R.string.no_location_found_message), | ||||||
|  |             getString(R.string.add_location), | ||||||
|  |             getString(R.string.skip_login), | ||||||
|  |             { | ||||||
|  |                 presenter.onMapIconClicked(indexOfFragment) | ||||||
|  |             }, | ||||||
|  |             runnable | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun showBadImagePopup(errorCode: Int, index: Int, uploadItem: UploadItem) { | ||||||
|  |         //If the error message is null, we will probably not show anything | ||||||
|  |         val activity = requireActivity() | ||||||
|  |         val errorMessageForResult = getErrorMessageForResult(activity, errorCode) | ||||||
|  |         if (errorMessageForResult.isNotEmpty()) { | ||||||
|  |             showAlertDialog( | ||||||
|  |                 activity, | ||||||
|  |                 activity.getString(R.string.upload_problem_image), | ||||||
|  |                 errorMessageForResult, | ||||||
|  |                 activity.getString(R.string.upload), | ||||||
|  |                 activity.getString(R.string.cancel), | ||||||
|  |                 { | ||||||
|  |                     showProgress(false) | ||||||
|  |                     uploadItem.imageQuality = IMAGE_OK | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     presenterCallback!!.deletePictureAtIndex(index) | ||||||
|  |                 } | ||||||
|  |             )?.setCancelable(false) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onDestroyView() { | ||||||
|  |         super.onDestroyView() | ||||||
|  |         presenter.onDetachView() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun expandCollapseLlMediaDetail(shouldExpand: Boolean) { | ||||||
|  |         if (_binding == null) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         binding.llContainerMediaDetail.visibility = | ||||||
|  |             if (shouldExpand) View.VISIBLE else View.GONE | ||||||
|  |         isExpanded = !isExpanded | ||||||
|  |         binding.ibExpandCollapse.rotation = binding.ibExpandCollapse.rotation + 180 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) { | ||||||
|  |         if (_binding == null) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         binding.btnCopySubsequentMedia.isEnabled = isNotEmpty | ||||||
|  |         binding.btnCopySubsequentMedia.isClickable = isNotEmpty | ||||||
|  |         binding.btnCopySubsequentMedia.alpha = if (isNotEmpty) 1.0f else 0.5f | ||||||
|  |         binding.btnNext.isEnabled = isNotEmpty | ||||||
|  |         binding.btnNext.isClickable = isNotEmpty | ||||||
|  |         binding.btnNext.alpha = if (isNotEmpty) 1.0f else 0.5f | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Adds new language item to RecyclerView | ||||||
|  |      */ | ||||||
|  |     override fun addLanguage() { | ||||||
|  |         val uploadMediaDetail = UploadMediaDetail() | ||||||
|  |         uploadMediaDetail.isManuallyAdded = true //This was manually added by the user | ||||||
|  |         uploadMediaDetailAdapter.addDescription(uploadMediaDetail) | ||||||
|  |         binding.rvDescriptions.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun onButtonCopyTitleDescToSubsequentMedia() { | ||||||
|  |         presenter.copyTitleAndDescriptionToSubsequentMedia(indexOfFragment) | ||||||
|  |         Toast.makeText(context, R.string.copied_successfully, Toast.LENGTH_SHORT).show() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onSaveInstanceState(outState: Bundle) { | ||||||
|  |         super.onSaveInstanceState(outState) | ||||||
|  | 
 | ||||||
|  |         if (uploadableFile != null) { | ||||||
|  |             outState.putParcelable(UPLOADABLE_FILE, uploadableFile) | ||||||
|  |         } | ||||||
|  |         outState.putParcelableArrayList( | ||||||
|  |             UPLOAD_MEDIA_DETAILS, | ||||||
|  |             ArrayList(uploadMediaDetailAdapter.items) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onDestroy() { | ||||||
|  |         super.onDestroy() | ||||||
|  |         _binding = null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     interface UploadMediaDetailFragmentCallback : Callback { | ||||||
|  |         fun deletePictureAtIndex(index: Int) | ||||||
|  | 
 | ||||||
|  |         fun changeThumbnail(index: Int, uri: String) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         /** | ||||||
|  |          * A key for applicationKvStore. By this key we can retrieve the location of last UploadItem ex. | ||||||
|  |          * 12.3433,54.78897 from applicationKvStore. | ||||||
|  |          */ | ||||||
|  |         const val LAST_LOCATION: String = "last_location_while_uploading" | ||||||
|  |         const val LAST_ZOOM: String = "last_zoom_level_while_uploading" | ||||||
|  |         const val UPLOADABLE_FILE: String = "uploadable_file" | ||||||
|  |         const val UPLOAD_MEDIA_DETAILS: String = "upload_media_detail_adapter" | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -3,6 +3,7 @@ package fr.free.nrw.commons.upload.mediaDetails | ||||||
| import android.app.Activity | import android.app.Activity | ||||||
| import fr.free.nrw.commons.BasePresenter | import fr.free.nrw.commons.BasePresenter | ||||||
| import fr.free.nrw.commons.filepicker.UploadableFile | import fr.free.nrw.commons.filepicker.UploadableFile | ||||||
|  | import fr.free.nrw.commons.kvstore.BasicKvStore | ||||||
| import fr.free.nrw.commons.location.LatLng | import fr.free.nrw.commons.location.LatLng | ||||||
| import fr.free.nrw.commons.nearby.Place | import fr.free.nrw.commons.nearby.Place | ||||||
| import fr.free.nrw.commons.upload.ImageCoordinates | import fr.free.nrw.commons.upload.ImageCoordinates | ||||||
|  | @ -15,9 +16,9 @@ import fr.free.nrw.commons.upload.UploadMediaDetail | ||||||
|  */ |  */ | ||||||
| interface UploadMediaDetailsContract { | interface UploadMediaDetailsContract { | ||||||
|     interface View : SimilarImageInterface { |     interface View : SimilarImageInterface { | ||||||
|         fun onImageProcessed(uploadItem: UploadItem?, place: Place?) |         fun onImageProcessed(uploadItem: UploadItem) | ||||||
| 
 | 
 | ||||||
|         fun onNearbyPlaceFound(uploadItem: UploadItem?, place: Place?) |         fun onNearbyPlaceFound(uploadItem: UploadItem, place: Place?) | ||||||
| 
 | 
 | ||||||
|         fun showProgress(shouldShow: Boolean) |         fun showProgress(shouldShow: Boolean) | ||||||
| 
 | 
 | ||||||
|  | @ -25,9 +26,9 @@ interface UploadMediaDetailsContract { | ||||||
| 
 | 
 | ||||||
|         fun showMessage(stringResourceId: Int, colorResourceId: Int) |         fun showMessage(stringResourceId: Int, colorResourceId: Int) | ||||||
| 
 | 
 | ||||||
|         fun showMessage(message: String?, colorResourceId: Int) |         fun showMessage(message: String, colorResourceId: Int) | ||||||
| 
 | 
 | ||||||
|         fun showDuplicatePicturePopup(uploadItem: UploadItem?) |         fun showDuplicatePicturePopup(uploadItem: UploadItem) | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|          * Shows a dialog alerting the user that internet connection is required for upload process |          * Shows a dialog alerting the user that internet connection is required for upload process | ||||||
|  | @ -42,16 +43,20 @@ interface UploadMediaDetailsContract { | ||||||
|          */ |          */ | ||||||
|         fun showConnectionErrorPopupForCaptionCheck() |         fun showConnectionErrorPopupForCaptionCheck() | ||||||
| 
 | 
 | ||||||
|         fun showExternalMap(uploadItem: UploadItem?) |         fun showExternalMap(uploadItem: UploadItem) | ||||||
| 
 | 
 | ||||||
|         fun showEditActivity(uploadItem: UploadItem?) |         fun showEditActivity(uploadItem: UploadItem) | ||||||
| 
 | 
 | ||||||
|         fun updateMediaDetails(uploadMediaDetails: List<UploadMediaDetail?>?) |         fun updateMediaDetails(uploadMediaDetails: List<UploadMediaDetail>) | ||||||
| 
 | 
 | ||||||
|         fun displayAddLocationDialog(runnable: Runnable?) |         fun displayAddLocationDialog(runnable: Runnable) | ||||||
|  | 
 | ||||||
|  |         fun showBadImagePopup(errorCode: Int, index: Int, uploadItem: UploadItem) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     interface UserActionListener : BasePresenter<View?> { |     interface UserActionListener : BasePresenter<View?> { | ||||||
|  |         fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore) | ||||||
|  | 
 | ||||||
|         fun receiveImage( |         fun receiveImage( | ||||||
|             uploadableFile: UploadableFile?, |             uploadableFile: UploadableFile?, | ||||||
|             place: Place?, |             place: Place?, | ||||||
|  | @ -59,7 +64,7 @@ interface UploadMediaDetailsContract { | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         fun setUploadMediaDetails( |         fun setUploadMediaDetails( | ||||||
|             uploadMediaDetails: List<UploadMediaDetail?>?, |             uploadMediaDetails: List<UploadMediaDetail>, | ||||||
|             uploadItemIndex: Int |             uploadItemIndex: Int | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  | @ -74,7 +79,7 @@ interface UploadMediaDetailsContract { | ||||||
|         fun getImageQuality( |         fun getImageQuality( | ||||||
|             uploadItemIndex: Int, |             uploadItemIndex: Int, | ||||||
|             inAppPictureLocation: LatLng?, |             inAppPictureLocation: LatLng?, | ||||||
|             activity: Activity? |             activity: Activity | ||||||
|         ): Boolean |         ): Boolean | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|  | @ -87,7 +92,8 @@ interface UploadMediaDetailsContract { | ||||||
|          * @param hasUserRemovedLocation True if user has removed location from the image |          * @param hasUserRemovedLocation True if user has removed location from the image | ||||||
|          */ |          */ | ||||||
|         fun displayLocDialog( |         fun displayLocDialog( | ||||||
|             uploadItemIndex: Int, inAppPictureLocation: LatLng?, |             uploadItemIndex: Int, | ||||||
|  |             inAppPictureLocation: LatLng?, | ||||||
|             hasUserRemovedLocation: Boolean |             hasUserRemovedLocation: Boolean | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  | @ -97,7 +103,7 @@ interface UploadMediaDetailsContract { | ||||||
|          * @param uploadItem UploadItem whose quality is to be checked |          * @param uploadItem UploadItem whose quality is to be checked | ||||||
|          * @param index Index of the UploadItem whose quality is to be checked |          * @param index Index of the UploadItem whose quality is to be checked | ||||||
|          */ |          */ | ||||||
|         fun checkImageQuality(uploadItem: UploadItem?, index: Int) |         fun checkImageQuality(uploadItem: UploadItem, index: Int) | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|          * Updates the image qualities stored in JSON, whenever an image is deleted |          * Updates the image qualities stored in JSON, whenever an image is deleted | ||||||
|  | @ -111,7 +117,7 @@ interface UploadMediaDetailsContract { | ||||||
| 
 | 
 | ||||||
|         fun fetchTitleAndDescription(indexInViewFlipper: Int) |         fun fetchTitleAndDescription(indexInViewFlipper: Int) | ||||||
| 
 | 
 | ||||||
|         fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates?, uploadItemIndex: Int) |         fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates, uploadItemIndex: Int) | ||||||
| 
 | 
 | ||||||
|         fun onMapIconClicked(indexInViewFlipper: Int) |         fun onMapIconClicked(indexInViewFlipper: Int) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,547 +0,0 @@ | ||||||
| package fr.free.nrw.commons.upload.mediaDetails; |  | ||||||
| 
 |  | ||||||
| import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; |  | ||||||
| import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD; |  | ||||||
| import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.activity; |  | ||||||
| import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION; |  | ||||||
| import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS; |  | ||||||
| import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP; |  | ||||||
| import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; |  | ||||||
| import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult; |  | ||||||
| 
 |  | ||||||
| import android.app.Activity; |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| import fr.free.nrw.commons.R; |  | ||||||
| import fr.free.nrw.commons.filepicker.UploadableFile; |  | ||||||
| import fr.free.nrw.commons.kvstore.BasicKvStore; |  | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; |  | ||||||
| import fr.free.nrw.commons.location.LatLng; |  | ||||||
| import fr.free.nrw.commons.nearby.Place; |  | ||||||
| import fr.free.nrw.commons.repository.UploadRepository; |  | ||||||
| import fr.free.nrw.commons.upload.ImageCoordinates; |  | ||||||
| import fr.free.nrw.commons.upload.SimilarImageInterface; |  | ||||||
| import fr.free.nrw.commons.upload.UploadActivity; |  | ||||||
| import fr.free.nrw.commons.upload.UploadItem; |  | ||||||
| import fr.free.nrw.commons.upload.UploadMediaDetail; |  | ||||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback; |  | ||||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.UserActionListener; |  | ||||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.View; |  | ||||||
| import fr.free.nrw.commons.utils.DialogUtil; |  | ||||||
| import io.github.coordinates2country.Coordinates2Country; |  | ||||||
| import io.reactivex.Maybe; |  | ||||||
| import io.reactivex.Scheduler; |  | ||||||
| import io.reactivex.disposables.CompositeDisposable; |  | ||||||
| import io.reactivex.disposables.Disposable; |  | ||||||
| import java.lang.reflect.Proxy; |  | ||||||
| import java.net.UnknownHostException; |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.Arrays; |  | ||||||
| import java.util.HashMap; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Locale; |  | ||||||
| import java.util.Map; |  | ||||||
| import javax.inject.Inject; |  | ||||||
| import javax.inject.Named; |  | ||||||
| import org.apache.commons.lang3.StringUtils; |  | ||||||
| import org.jetbrains.annotations.NotNull; |  | ||||||
| import org.json.JSONObject; |  | ||||||
| import timber.log.Timber; |  | ||||||
| 
 |  | ||||||
| public class UploadMediaPresenter implements UserActionListener, SimilarImageInterface { |  | ||||||
| 
 |  | ||||||
|     private static final UploadMediaDetailsContract.View DUMMY = (UploadMediaDetailsContract.View) Proxy |  | ||||||
|             .newProxyInstance( |  | ||||||
|                     UploadMediaDetailsContract.View.class.getClassLoader(), |  | ||||||
|                     new Class[]{UploadMediaDetailsContract.View.class}, |  | ||||||
|                     (proxy, method, methodArgs) -> null); |  | ||||||
| 
 |  | ||||||
|     private final UploadRepository repository; |  | ||||||
|     private UploadMediaDetailsContract.View view = DUMMY; |  | ||||||
| 
 |  | ||||||
|     private CompositeDisposable compositeDisposable; |  | ||||||
| 
 |  | ||||||
|     private final JsonKvStore defaultKVStore; |  | ||||||
|     private Scheduler ioScheduler; |  | ||||||
|     private Scheduler mainThreadScheduler; |  | ||||||
| 
 |  | ||||||
|     public static UploadMediaDetailFragmentCallback presenterCallback ; |  | ||||||
| 
 |  | ||||||
|     private final List<String> WLM_SUPPORTED_COUNTRIES= Arrays.asList("am","at","az","br","hr","sv","fi","fr","de","gh","in","ie","il","mk","my","mt","pk","pe","pl","ru","rw","si","es","se","tw","ug","ua","us"); |  | ||||||
|     private Map<String, String> countryNamesAndCodes = null; |  | ||||||
| 
 |  | ||||||
|     private final String keyForCurrentUploadImageQualities = "UploadedImagesQualities"; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Variable used to determine if the battery-optimisation dialog is being shown or not |  | ||||||
|      */ |  | ||||||
|     public static boolean isBatteryDialogShowing; |  | ||||||
| 
 |  | ||||||
|     public static boolean isCategoriesDialogShowing; |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     public UploadMediaPresenter(final UploadRepository uploadRepository, |  | ||||||
|         @Named("default_preferences") final JsonKvStore defaultKVStore, |  | ||||||
|                                 @Named(IO_THREAD) final Scheduler ioScheduler, |  | ||||||
|                                 @Named(MAIN_THREAD) final Scheduler mainThreadScheduler) { |  | ||||||
|         this.repository = uploadRepository; |  | ||||||
|         this.defaultKVStore = defaultKVStore; |  | ||||||
|         this.ioScheduler = ioScheduler; |  | ||||||
|         this.mainThreadScheduler = mainThreadScheduler; |  | ||||||
|         compositeDisposable = new CompositeDisposable(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onAttachView(final View view) { |  | ||||||
|         this.view = view; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onDetachView() { |  | ||||||
|         this.view = DUMMY; |  | ||||||
|         compositeDisposable.clear(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Sets the Upload Media Details for the corresponding upload item |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void setUploadMediaDetails(final List<UploadMediaDetail> uploadMediaDetails, final int uploadItemIndex) { |  | ||||||
|         repository.getUploads().get(uploadItemIndex).setUploadMediaDetails(uploadMediaDetails); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Receives the corresponding uploadable file, processes it and return the view with and uplaod item |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void receiveImage(final UploadableFile uploadableFile, final Place place, |  | ||||||
|                             final LatLng inAppPictureLocation) { |  | ||||||
|         view.showProgress(true); |  | ||||||
|         compositeDisposable.add( |  | ||||||
|             repository |  | ||||||
|                 .preProcessImage(uploadableFile, place, this, inAppPictureLocation) |  | ||||||
|                 .map(uploadItem -> { |  | ||||||
|                     if(place!=null && place.isMonument()){ |  | ||||||
|                         if (place.location != null) { |  | ||||||
|                             final String countryCode = reverseGeoCode(place.location); |  | ||||||
|                             if (countryCode != null && WLM_SUPPORTED_COUNTRIES |  | ||||||
|                                 .contains(countryCode.toLowerCase(Locale.ROOT))) { |  | ||||||
|                                 uploadItem.setWLMUpload(true); |  | ||||||
|                                 uploadItem.setCountryCode(countryCode.toLowerCase(Locale.ROOT)); |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                     return uploadItem; |  | ||||||
|                 }) |  | ||||||
|                 .subscribeOn(ioScheduler) |  | ||||||
|                 .observeOn(mainThreadScheduler) |  | ||||||
|                 .subscribe(uploadItem -> |  | ||||||
|                     { |  | ||||||
|                         view.onImageProcessed(uploadItem, place); |  | ||||||
|                         view.updateMediaDetails(uploadItem.getUploadMediaDetails()); |  | ||||||
|                         view.showProgress(false); |  | ||||||
|                         final ImageCoordinates gpsCoords = uploadItem.getGpsCoords(); |  | ||||||
|                         final boolean hasImageCoordinates = |  | ||||||
|                             gpsCoords != null && gpsCoords.getImageCoordsExists(); |  | ||||||
|                         if (hasImageCoordinates && place == null) { |  | ||||||
|                             checkNearbyPlaces(uploadItem); |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     throwable -> Timber.e(throwable, "Error occurred in processing images"))); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Nullable |  | ||||||
|     private String reverseGeoCode(final LatLng latLng){ |  | ||||||
|         if(countryNamesAndCodes == null){ |  | ||||||
|             countryNamesAndCodes = getCountryNamesAndCodes(); |  | ||||||
|         } |  | ||||||
|         return countryNamesAndCodes.get(Coordinates2Country.country(latLng.getLatitude(), latLng.getLongitude())); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Creates HashMap containing all ISO countries 2-letter codes provided by <code>Locale.getISOCountries()</code> |  | ||||||
|      * and their english names |  | ||||||
|      * |  | ||||||
|      * @return HashMap where Key is country english name and Value is 2-letter country code |  | ||||||
|      * e.g. ["Germany":"DE", ...] |  | ||||||
|      */ |  | ||||||
|     private Map<String, String> getCountryNamesAndCodes(){ |  | ||||||
|         final Map<String, String> result = new HashMap<>(); |  | ||||||
| 
 |  | ||||||
|         final String[] isoCountries = Locale.getISOCountries(); |  | ||||||
| 
 |  | ||||||
|         for (final String isoCountry : isoCountries) { |  | ||||||
|             result.put( |  | ||||||
|                 new Locale("en", isoCountry).getDisplayCountry(Locale.ENGLISH), |  | ||||||
|                 isoCountry |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return result; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method checks for the nearest location that needs images and suggests it to the user. |  | ||||||
|      */ |  | ||||||
|     private void checkNearbyPlaces(final UploadItem uploadItem) { |  | ||||||
|         final Disposable checkNearbyPlaces = Maybe.fromCallable(() -> repository |  | ||||||
|                 .checkNearbyPlaces(uploadItem.getGpsCoords().getDecLatitude(), |  | ||||||
|                         uploadItem.getGpsCoords().getDecLongitude())) |  | ||||||
|                 .subscribeOn(ioScheduler) |  | ||||||
|                 .observeOn(mainThreadScheduler) |  | ||||||
|                 .subscribe(place -> { |  | ||||||
|                         if (place != null) { |  | ||||||
|                             view.onNearbyPlaceFound(uploadItem, place); |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     throwable -> Timber.e(throwable, "Error occurred in processing images")); |  | ||||||
|             compositeDisposable.add(checkNearbyPlaces); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Checks if the image has a location. Displays a dialog alerting user that no |  | ||||||
|      * location has been to added to the image and asking them to add one, if location was not |  | ||||||
|      * removed by the user |  | ||||||
|      * |  | ||||||
|      * @param uploadItemIndex Index of the uploadItem which has no location |  | ||||||
|      * @param inAppPictureLocation In app picture location (if any) |  | ||||||
|      * @param hasUserRemovedLocation True if user has removed location from the image |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void displayLocDialog(final int uploadItemIndex, final LatLng inAppPictureLocation, |  | ||||||
|         final boolean hasUserRemovedLocation) { |  | ||||||
|         final List<UploadItem> uploadItems = repository.getUploads(); |  | ||||||
|         final UploadItem uploadItem = uploadItems.get(uploadItemIndex); |  | ||||||
|         if (uploadItem.getGpsCoords().getDecimalCoords() == null && inAppPictureLocation == null |  | ||||||
|             && !hasUserRemovedLocation) { |  | ||||||
|             final Runnable onSkipClicked = () -> { |  | ||||||
|                 verifyCaptionQuality(uploadItem); |  | ||||||
|             }; |  | ||||||
|             view.displayAddLocationDialog(onSkipClicked); |  | ||||||
|         } else { |  | ||||||
|             verifyCaptionQuality(uploadItem); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Verifies the image's caption and calls function to handle the result |  | ||||||
|      * |  | ||||||
|      * @param uploadItem UploadItem whose caption is checked |  | ||||||
|      */ |  | ||||||
|     private void verifyCaptionQuality(final UploadItem uploadItem) { |  | ||||||
|         view.showProgress(true); |  | ||||||
|         compositeDisposable.add( |  | ||||||
|             repository |  | ||||||
|                 .getCaptionQuality(uploadItem) |  | ||||||
|                 .observeOn(mainThreadScheduler) |  | ||||||
|                 .subscribe(capResult -> { |  | ||||||
|                         view.showProgress(false); |  | ||||||
|                         handleCaptionResult(capResult, uploadItem); |  | ||||||
|                     }, |  | ||||||
|                     throwable -> { |  | ||||||
|                         view.showProgress(false); |  | ||||||
|                         if (throwable instanceof UnknownHostException) { |  | ||||||
|                             view.showConnectionErrorPopupForCaptionCheck(); |  | ||||||
|                         } else { |  | ||||||
|                             view.showMessage("" + throwable.getLocalizedMessage(), |  | ||||||
|                                 R.color.color_error); |  | ||||||
|                         } |  | ||||||
|                         Timber.e(throwable, "Error occurred while handling image"); |  | ||||||
|                     }) |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Handles image's caption results and shows dialog if necessary |  | ||||||
|      * |  | ||||||
|      * @param errorCode Error code of the UploadItem |  | ||||||
|      * @param uploadItem UploadItem whose caption is checked |  | ||||||
|      */ |  | ||||||
|     public void handleCaptionResult(final Integer errorCode, final UploadItem uploadItem) { |  | ||||||
|         // If errorCode is empty caption show message |  | ||||||
|         if (errorCode == EMPTY_CAPTION) { |  | ||||||
|             Timber.d("Captions are empty. Showing toast"); |  | ||||||
|             view.showMessage(R.string.add_caption_toast, R.color.color_error); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // If image with same file name exists check the bit in errorCode is set or not |  | ||||||
|         if ((errorCode & FILE_NAME_EXISTS) != 0) { |  | ||||||
|             Timber.d("Trying to show duplicate picture popup"); |  | ||||||
|             view.showDuplicatePicturePopup(uploadItem); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // If caption is not duplicate or user still wants to upload it |  | ||||||
|         if (errorCode == IMAGE_OK) { |  | ||||||
|             Timber.d("Image captions are okay or user still wants to upload it"); |  | ||||||
|             view.onImageValidationSuccess(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Copies the caption and description of the current item to the subsequent media |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void copyTitleAndDescriptionToSubsequentMedia(final int indexInViewFlipper) { |  | ||||||
|       for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){ |  | ||||||
|         final UploadItem subsequentUploadItem = repository.getUploads().get(i); |  | ||||||
|         subsequentUploadItem.setUploadMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails())); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Fetches and set the caption and description of the item |  | ||||||
|    */ |  | ||||||
|   @Override |  | ||||||
|   public void fetchTitleAndDescription(final int indexInViewFlipper) { |  | ||||||
|     final UploadItem currentUploadItem = repository.getUploads().get(indexInViewFlipper); |  | ||||||
|     view.updateMediaDetails(currentUploadItem.getUploadMediaDetails()); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @NotNull |  | ||||||
|   private List<UploadMediaDetail> deepCopy(final List<UploadMediaDetail> uploadMediaDetails) { |  | ||||||
|     final ArrayList<UploadMediaDetail> newList = new ArrayList<>(); |  | ||||||
|     for (final UploadMediaDetail uploadMediaDetail : uploadMediaDetails) { |  | ||||||
|       newList.add(uploadMediaDetail.javaCopy()); |  | ||||||
|     } |  | ||||||
|     return newList; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @Override |  | ||||||
|   public void useSimilarPictureCoordinates(final ImageCoordinates imageCoordinates, final int uploadItemIndex) { |  | ||||||
|     repository.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @Override |  | ||||||
|   public void onMapIconClicked(final int indexInViewFlipper) { |  | ||||||
|     view.showExternalMap(repository.getUploads().get(indexInViewFlipper)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @Override |  | ||||||
|   public void onEditButtonClicked(final int indexInViewFlipper){ |  | ||||||
|       view.showEditActivity(repository.getUploads().get(indexInViewFlipper)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Updates the information regarding the specified place for the specified upload item |  | ||||||
|    * when the user confirms the suggested nearby place. |  | ||||||
|    * |  | ||||||
|    * @param place The place to be associated with the uploads. |  | ||||||
|    * @param uploadItemIndex Index of the uploadItem whose detected place has been confirmed |  | ||||||
|    */ |  | ||||||
|   @Override |  | ||||||
|   public void onUserConfirmedUploadIsOfPlace(final Place place, final int uploadItemIndex) { |  | ||||||
|       final UploadItem uploadItem = repository.getUploads().get(uploadItemIndex); |  | ||||||
| 
 |  | ||||||
|       uploadItem.setPlace(place); |  | ||||||
|       final List<UploadMediaDetail> uploadMediaDetails = uploadItem.getUploadMediaDetails(); |  | ||||||
|       // Update UploadMediaDetail object for this UploadItem |  | ||||||
|       uploadMediaDetails.set(0, new UploadMediaDetail(place)); |  | ||||||
| 
 |  | ||||||
|       // Now that the UploadItem and its associated UploadMediaDetail objects have been updated, |  | ||||||
|       // update the view with the modified media details of the first upload item |  | ||||||
|       view.updateMediaDetails(uploadMediaDetails); |  | ||||||
|       UploadActivity.setUploadIsOfAPlace(true); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Calculates the image quality |  | ||||||
|      * |  | ||||||
|      * @param uploadItemIndex      Index of the UploadItem whose quality is to be checked |  | ||||||
|      * @param inAppPictureLocation In app picture location (if any) |  | ||||||
|      * @param activity             Context reference |  | ||||||
|      * @return true if no internal error occurs, else returns false |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public boolean getImageQuality(final int uploadItemIndex, final LatLng inAppPictureLocation, |  | ||||||
|         final Activity activity) { |  | ||||||
|         final List<UploadItem> uploadItems = repository.getUploads(); |  | ||||||
|         view.showProgress(true); |  | ||||||
|         if (uploadItems.isEmpty()) { |  | ||||||
|             view.showProgress(false); |  | ||||||
|             // No internationalization required for this error message because it's an internal error. |  | ||||||
|             view.showMessage( |  | ||||||
|                 "Internal error: Zero upload items received by the Upload Media Detail Fragment. Sorry, please upload again.", |  | ||||||
|                 R.color.color_error); |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|         final UploadItem uploadItem = uploadItems.get(uploadItemIndex); |  | ||||||
|         compositeDisposable.add( |  | ||||||
|             repository |  | ||||||
|                 .getImageQuality(uploadItem, inAppPictureLocation) |  | ||||||
|                 .observeOn(mainThreadScheduler) |  | ||||||
|                 .subscribe(imageResult -> { |  | ||||||
|                         storeImageQuality(imageResult, uploadItemIndex, activity, uploadItem); |  | ||||||
|                     }, |  | ||||||
|                     throwable -> { |  | ||||||
|                         if (throwable instanceof UnknownHostException) { |  | ||||||
|                             view.showProgress(false); |  | ||||||
|                             view.showConnectionErrorPopup(); |  | ||||||
|                         } else { |  | ||||||
|                             view.showMessage("" + throwable.getLocalizedMessage(), |  | ||||||
|                                 R.color.color_error); |  | ||||||
|                         } |  | ||||||
|                         Timber.e(throwable, "Error occurred while handling image"); |  | ||||||
|                     }) |  | ||||||
|         ); |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Stores the image quality in JSON format in SharedPrefs |  | ||||||
|      * |  | ||||||
|      * @param imageResult     Image quality |  | ||||||
|      * @param uploadItemIndex Index of the UploadItem whose quality is calculated |  | ||||||
|      * @param activity        Context reference |  | ||||||
|      * @param uploadItem      UploadItem whose quality is to be checked |  | ||||||
|      */ |  | ||||||
|     private void storeImageQuality(final Integer imageResult, final int uploadItemIndex, final Activity activity, |  | ||||||
|         final UploadItem uploadItem) { |  | ||||||
|         final BasicKvStore store = new BasicKvStore(activity, |  | ||||||
|             UploadActivity.storeNameForCurrentUploadImagesSize); |  | ||||||
|         final String value = store.getString(keyForCurrentUploadImageQualities, null); |  | ||||||
|         final JSONObject jsonObject; |  | ||||||
|         try { |  | ||||||
|             if (value != null) { |  | ||||||
|                 jsonObject = new JSONObject(value); |  | ||||||
|             } else { |  | ||||||
|                 jsonObject = new JSONObject(); |  | ||||||
|             } |  | ||||||
|             jsonObject.put("UploadItem" + uploadItemIndex, imageResult); |  | ||||||
|             store.putString(keyForCurrentUploadImageQualities, jsonObject.toString()); |  | ||||||
|         } catch (final Exception e) { |  | ||||||
|             Timber.e(e); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (uploadItemIndex == 0) { |  | ||||||
|             if (!isBatteryDialogShowing && !isCategoriesDialogShowing) { |  | ||||||
|                 // if battery-optimisation dialog is not being shown, call checkImageQuality |  | ||||||
|                 checkImageQuality(uploadItem, uploadItemIndex); |  | ||||||
|             } else { |  | ||||||
|                 view.showProgress(false); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Used to check image quality from stored qualities and display dialogs |  | ||||||
|      * |  | ||||||
|      * @param uploadItem UploadItem whose quality is to be checked |  | ||||||
|      * @param index      Index of the UploadItem whose quality is to be checked |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void checkImageQuality(final UploadItem uploadItem, final int index) { |  | ||||||
|         if ((uploadItem.getImageQuality() != IMAGE_OK) && (uploadItem.getImageQuality() |  | ||||||
|             != IMAGE_KEEP)) { |  | ||||||
|             final BasicKvStore store = new BasicKvStore(activity, |  | ||||||
|                 UploadActivity.storeNameForCurrentUploadImagesSize); |  | ||||||
|             final String value = store.getString(keyForCurrentUploadImageQualities, null); |  | ||||||
|             final JSONObject jsonObject; |  | ||||||
|             try { |  | ||||||
|                 if (value != null) { |  | ||||||
|                     jsonObject = new JSONObject(value); |  | ||||||
|                 } else { |  | ||||||
|                     jsonObject = new JSONObject(); |  | ||||||
|                 } |  | ||||||
|                 final Integer imageQuality = (int) jsonObject.get("UploadItem" + index); |  | ||||||
|                 view.showProgress(false); |  | ||||||
|                 if (imageQuality == IMAGE_OK) { |  | ||||||
|                     uploadItem.setHasInvalidLocation(false); |  | ||||||
|                     uploadItem.setImageQuality(imageQuality); |  | ||||||
|                 } else { |  | ||||||
|                     handleBadImage(imageQuality, uploadItem, index); |  | ||||||
|                 } |  | ||||||
|             } catch (final Exception e) { |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Updates the image qualities stored in JSON, whenever an image is deleted |  | ||||||
|      * |  | ||||||
|      * @param size Size of uploadableFiles |  | ||||||
|      * @param index Index of the UploadItem which was deleted |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void updateImageQualitiesJSON(final int size, final int index) { |  | ||||||
|         final BasicKvStore store = new BasicKvStore(activity, |  | ||||||
|             UploadActivity.storeNameForCurrentUploadImagesSize); |  | ||||||
|         final String value = store.getString(keyForCurrentUploadImageQualities, null); |  | ||||||
|         final JSONObject jsonObject; |  | ||||||
|         try { |  | ||||||
|             if (value != null) { |  | ||||||
|                 jsonObject = new JSONObject(value); |  | ||||||
|             } else { |  | ||||||
|                 jsonObject = new JSONObject(); |  | ||||||
|             } |  | ||||||
|             for (int i = index; i < (size - 1); i++) { |  | ||||||
|                 jsonObject.put("UploadItem" + i, jsonObject.get("UploadItem" + (i + 1))); |  | ||||||
|             } |  | ||||||
|             jsonObject.remove("UploadItem" + (size - 1)); |  | ||||||
|             store.putString(keyForCurrentUploadImageQualities, jsonObject.toString()); |  | ||||||
|         } catch (final Exception e) { |  | ||||||
|             Timber.e(e); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Handles bad pictures, like too dark, already on wikimedia, downloaded from internet |  | ||||||
|      * |  | ||||||
|      * @param errorCode Error code of the bad image quality |  | ||||||
|      * @param uploadItem UploadItem whose quality is bad |  | ||||||
|      * @param index Index of item whose quality is bad |  | ||||||
|      */ |  | ||||||
|     public void handleBadImage(final Integer errorCode, |  | ||||||
|         final UploadItem uploadItem, final int index) { |  | ||||||
|         Timber.d("Handle bad picture with error code %d", errorCode); |  | ||||||
|         if (errorCode >= 8) { // If location of image and nearby does not match |  | ||||||
|             uploadItem.setHasInvalidLocation(true); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // If image has some other problems, show popup accordingly |  | ||||||
|         if (errorCode != EMPTY_CAPTION && errorCode != FILE_NAME_EXISTS) { |  | ||||||
|             showBadImagePopup(errorCode, index, activity, uploadItem); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Shows a dialog describing the potential problems in the current image |  | ||||||
|      * |  | ||||||
|      * @param errorCode  Has the potential problems in the current image |  | ||||||
|      * @param index      Index of the UploadItem which has problems |  | ||||||
|      * @param activity   Context reference |  | ||||||
|      * @param uploadItem UploadItem which has problems |  | ||||||
|      */ |  | ||||||
|     public void showBadImagePopup(final Integer errorCode, |  | ||||||
|         final int index, final Activity activity, final UploadItem uploadItem) { |  | ||||||
|         final String errorMessageForResult = getErrorMessageForResult(activity, errorCode); |  | ||||||
|         if (!StringUtils.isBlank(errorMessageForResult)) { |  | ||||||
|             DialogUtil.showAlertDialog(activity, |  | ||||||
|                 activity.getString(R.string.upload_problem_image), |  | ||||||
|                 errorMessageForResult, |  | ||||||
|                 activity.getString(R.string.upload), |  | ||||||
|                 activity.getString(R.string.cancel), |  | ||||||
|                 () -> { |  | ||||||
|                     view.showProgress(false); |  | ||||||
|                     uploadItem.setImageQuality(IMAGE_OK); |  | ||||||
|                 }, |  | ||||||
|                 () -> { |  | ||||||
|                     presenterCallback.deletePictureAtIndex(index); |  | ||||||
|                 } |  | ||||||
|             ).setCancelable(false); |  | ||||||
|         } |  | ||||||
|         //If the error message is null, we will probably not show anything |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * notifies the user that a similar image exists |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void showSimilarImageFragment(final String originalFilePath, final String possibleFilePath, |  | ||||||
|         final ImageCoordinates similarImageCoordinates) { |  | ||||||
|         view.showSimilarImageFragment(originalFilePath, possibleFilePath, |  | ||||||
|             similarImageCoordinates |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,441 @@ | ||||||
|  | package fr.free.nrw.commons.upload.mediaDetails | ||||||
|  | 
 | ||||||
|  | import android.app.Activity | ||||||
|  | import fr.free.nrw.commons.R | ||||||
|  | import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD | ||||||
|  | import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD | ||||||
|  | import fr.free.nrw.commons.filepicker.UploadableFile | ||||||
|  | import fr.free.nrw.commons.kvstore.BasicKvStore | ||||||
|  | import fr.free.nrw.commons.location.LatLng | ||||||
|  | import fr.free.nrw.commons.nearby.Place | ||||||
|  | import fr.free.nrw.commons.repository.UploadRepository | ||||||
|  | import fr.free.nrw.commons.upload.ImageCoordinates | ||||||
|  | import fr.free.nrw.commons.upload.SimilarImageInterface | ||||||
|  | import fr.free.nrw.commons.upload.UploadActivity | ||||||
|  | import fr.free.nrw.commons.upload.UploadActivity.Companion.setUploadIsOfAPlace | ||||||
|  | import fr.free.nrw.commons.upload.UploadItem | ||||||
|  | import fr.free.nrw.commons.upload.UploadMediaDetail | ||||||
|  | import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback | ||||||
|  | import fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION | ||||||
|  | import fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS | ||||||
|  | import fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP | ||||||
|  | import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK | ||||||
|  | import io.github.coordinates2country.Coordinates2Country | ||||||
|  | import io.reactivex.Maybe | ||||||
|  | import io.reactivex.Scheduler | ||||||
|  | import io.reactivex.disposables.CompositeDisposable | ||||||
|  | import org.json.JSONObject | ||||||
|  | import timber.log.Timber | ||||||
|  | import java.lang.reflect.Method | ||||||
|  | import java.lang.reflect.Proxy | ||||||
|  | import java.net.UnknownHostException | ||||||
|  | import java.util.Locale | ||||||
|  | import javax.inject.Inject | ||||||
|  | import javax.inject.Named | ||||||
|  | 
 | ||||||
|  | class UploadMediaPresenter @Inject constructor( | ||||||
|  |     private val repository: UploadRepository, | ||||||
|  |     @param:Named(IO_THREAD) private val ioScheduler: Scheduler, | ||||||
|  |     @param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler | ||||||
|  | ) : UploadMediaDetailsContract.UserActionListener, SimilarImageInterface { | ||||||
|  |     private var view = DUMMY | ||||||
|  | 
 | ||||||
|  |     private val compositeDisposable = CompositeDisposable() | ||||||
|  | 
 | ||||||
|  |     private val countryNamesAndCodes: Map<String, String> by lazy { | ||||||
|  |         // Create a map containing all ISO countries 2-letter codes provided by | ||||||
|  |         // `Locale.getISOCountries()` and their english names | ||||||
|  |         buildMap { | ||||||
|  |             Locale.getISOCountries().forEach { | ||||||
|  |                 put(Locale("en", it).getDisplayCountry(Locale.ENGLISH), it) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     lateinit var basicKvStoreFactory: (String) -> BasicKvStore | ||||||
|  | 
 | ||||||
|  |     override fun onAttachView(view: UploadMediaDetailsContract.View) { | ||||||
|  |         this.view = view | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onDetachView() { | ||||||
|  |         view = DUMMY | ||||||
|  |         compositeDisposable.clear() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Sets the Upload Media Details for the corresponding upload item | ||||||
|  |      */ | ||||||
|  |     override fun setUploadMediaDetails( | ||||||
|  |         uploadMediaDetails: List<UploadMediaDetail>, | ||||||
|  |         uploadItemIndex: Int | ||||||
|  |     ) { | ||||||
|  |         repository.getUploads()[uploadItemIndex].uploadMediaDetails = uploadMediaDetails.toMutableList() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore) { | ||||||
|  |         basicKvStoreFactory = factory | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Receives the corresponding uploadable file, processes it and return the view with and uplaod item | ||||||
|  |      */ | ||||||
|  |     override fun receiveImage( | ||||||
|  |         uploadableFile: UploadableFile?, | ||||||
|  |         place: Place?, | ||||||
|  |         inAppPictureLocation: LatLng? | ||||||
|  |     ) { | ||||||
|  |         view.showProgress(true) | ||||||
|  |         compositeDisposable.add( | ||||||
|  |             repository.preProcessImage( | ||||||
|  |                 uploadableFile, place, this, inAppPictureLocation | ||||||
|  |             ).map { uploadItem: UploadItem -> | ||||||
|  |                 if (place != null && place.isMonument && place.location != null) { | ||||||
|  |                     val countryCode = countryNamesAndCodes[Coordinates2Country.country( | ||||||
|  |                         place.location.latitude, | ||||||
|  |                         place.location.longitude | ||||||
|  |                     )] | ||||||
|  |                     if (countryCode != null && WLM_SUPPORTED_COUNTRIES.contains(countryCode.lowercase())) { | ||||||
|  |                         uploadItem.isWLMUpload = true | ||||||
|  |                         uploadItem.countryCode = countryCode.lowercase() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 uploadItem | ||||||
|  |             }.subscribeOn(ioScheduler).observeOn(mainThreadScheduler) | ||||||
|  |                 .subscribe({ uploadItem: UploadItem -> | ||||||
|  |                     view.onImageProcessed(uploadItem) | ||||||
|  |                     view.updateMediaDetails(uploadItem.uploadMediaDetails) | ||||||
|  |                     view.showProgress(false) | ||||||
|  |                     val gpsCoords = uploadItem.gpsCoords | ||||||
|  |                     val hasImageCoordinates = gpsCoords != null && gpsCoords.imageCoordsExists | ||||||
|  |                     if (hasImageCoordinates && place == null) { | ||||||
|  |                         checkNearbyPlaces(uploadItem) | ||||||
|  |                     } | ||||||
|  |                 }, { throwable: Throwable? -> | ||||||
|  |                     Timber.e(throwable, "Error occurred in processing images") | ||||||
|  |                 }) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method checks for the nearest location that needs images and suggests it to the user. | ||||||
|  |      */ | ||||||
|  |     private fun checkNearbyPlaces(uploadItem: UploadItem) { | ||||||
|  |         compositeDisposable.add(Maybe.fromCallable { | ||||||
|  |             repository.checkNearbyPlaces( | ||||||
|  |                     uploadItem.gpsCoords!!.decLatitude, uploadItem.gpsCoords!!.decLongitude | ||||||
|  |             ) | ||||||
|  |         }.subscribeOn(ioScheduler).observeOn(mainThreadScheduler).subscribe({ | ||||||
|  |                 view.onNearbyPlaceFound(uploadItem, it) | ||||||
|  |             }, { throwable: Throwable? -> | ||||||
|  |                 Timber.e(throwable, "Error occurred in processing images") | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Checks if the image has a location. Displays a dialog alerting user that no | ||||||
|  |      * location has been to added to the image and asking them to add one, if location was not | ||||||
|  |      * removed by the user | ||||||
|  |      * | ||||||
|  |      * @param uploadItemIndex Index of the uploadItem which has no location | ||||||
|  |      * @param inAppPictureLocation In app picture location (if any) | ||||||
|  |      * @param hasUserRemovedLocation True if user has removed location from the image | ||||||
|  |      */ | ||||||
|  |     override fun displayLocDialog( | ||||||
|  |         uploadItemIndex: Int, inAppPictureLocation: LatLng?, | ||||||
|  |         hasUserRemovedLocation: Boolean | ||||||
|  |     ) { | ||||||
|  |         val uploadItem = repository.getUploads()[uploadItemIndex] | ||||||
|  |         if (uploadItem.gpsCoords!!.decimalCoords == null && inAppPictureLocation == null && !hasUserRemovedLocation) { | ||||||
|  |             view.displayAddLocationDialog { verifyCaptionQuality(uploadItem) } | ||||||
|  |         } else { | ||||||
|  |             verifyCaptionQuality(uploadItem) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Verifies the image's caption and calls function to handle the result | ||||||
|  |      * | ||||||
|  |      * @param uploadItem UploadItem whose caption is checked | ||||||
|  |      */ | ||||||
|  |     private fun verifyCaptionQuality(uploadItem: UploadItem) { | ||||||
|  |         view.showProgress(true) | ||||||
|  |         compositeDisposable.add(repository.getCaptionQuality(uploadItem) | ||||||
|  |             .observeOn(mainThreadScheduler) | ||||||
|  |             .subscribe({ capResult: Int -> | ||||||
|  |                 view.showProgress(false) | ||||||
|  |                 handleCaptionResult(capResult, uploadItem) | ||||||
|  |             }, { throwable: Throwable -> | ||||||
|  |                 view.showProgress(false) | ||||||
|  |                 if (throwable is UnknownHostException) { | ||||||
|  |                     view.showConnectionErrorPopupForCaptionCheck() | ||||||
|  |                 } else { | ||||||
|  |                     view.showMessage(throwable.localizedMessage, R.color.color_error) | ||||||
|  |                 } | ||||||
|  |                 Timber.e(throwable, "Error occurred while handling image") | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles image's caption results and shows dialog if necessary | ||||||
|  |      * | ||||||
|  |      * @param errorCode Error code of the UploadItem | ||||||
|  |      * @param uploadItem UploadItem whose caption is checked | ||||||
|  |      */ | ||||||
|  |     fun handleCaptionResult(errorCode: Int, uploadItem: UploadItem) { | ||||||
|  |         // If errorCode is empty caption show message | ||||||
|  |         if (errorCode == EMPTY_CAPTION) { | ||||||
|  |             Timber.d("Captions are empty. Showing toast") | ||||||
|  |             view.showMessage(R.string.add_caption_toast, R.color.color_error) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // If image with same file name exists check the bit in errorCode is set or not | ||||||
|  |         if ((errorCode and FILE_NAME_EXISTS) != 0) { | ||||||
|  |             Timber.d("Trying to show duplicate picture popup") | ||||||
|  |             view.showDuplicatePicturePopup(uploadItem) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // If caption is not duplicate or user still wants to upload it | ||||||
|  |         if (errorCode == IMAGE_OK) { | ||||||
|  |             Timber.d("Image captions are okay or user still wants to upload it") | ||||||
|  |             view.onImageValidationSuccess() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Copies the caption and description of the current item to the subsequent media | ||||||
|  |      */ | ||||||
|  |     override fun copyTitleAndDescriptionToSubsequentMedia(indexInViewFlipper: Int) { | ||||||
|  |         for (i in indexInViewFlipper + 1 until repository.getCount()) { | ||||||
|  |             val subsequentUploadItem = repository.getUploads()[i] | ||||||
|  |             subsequentUploadItem.uploadMediaDetails = deepCopy( | ||||||
|  |                 repository.getUploads()[indexInViewFlipper].uploadMediaDetails | ||||||
|  |             ).toMutableList() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Fetches and set the caption and description of the item | ||||||
|  |      */ | ||||||
|  |     override fun fetchTitleAndDescription(indexInViewFlipper: Int) = | ||||||
|  |         view.updateMediaDetails(repository.getUploads()[indexInViewFlipper].uploadMediaDetails) | ||||||
|  | 
 | ||||||
|  |     private fun deepCopy(uploadMediaDetails: List<UploadMediaDetail>) = | ||||||
|  |         uploadMediaDetails.map(UploadMediaDetail::javaCopy) | ||||||
|  | 
 | ||||||
|  |     override fun useSimilarPictureCoordinates( | ||||||
|  |         imageCoordinates: ImageCoordinates, uploadItemIndex: Int | ||||||
|  |     ) = repository.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex) | ||||||
|  | 
 | ||||||
|  |     override fun onMapIconClicked(indexInViewFlipper: Int) = | ||||||
|  |         view.showExternalMap(repository.getUploads()[indexInViewFlipper]) | ||||||
|  | 
 | ||||||
|  |     override fun onEditButtonClicked(indexInViewFlipper: Int) = | ||||||
|  |         view.showEditActivity(repository.getUploads()[indexInViewFlipper]) | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Updates the information regarding the specified place for the specified upload item | ||||||
|  |      * when the user confirms the suggested nearby place. | ||||||
|  |      * | ||||||
|  |      * @param place The place to be associated with the uploads. | ||||||
|  |      * @param uploadItemIndex Index of the uploadItem whose detected place has been confirmed | ||||||
|  |      */ | ||||||
|  |     override fun onUserConfirmedUploadIsOfPlace(place: Place?, uploadItemIndex: Int) { | ||||||
|  |         val uploadItem = repository.getUploads()[uploadItemIndex] | ||||||
|  | 
 | ||||||
|  |         uploadItem.place = place | ||||||
|  |         val uploadMediaDetails = uploadItem.uploadMediaDetails | ||||||
|  |         // Update UploadMediaDetail object for this UploadItem | ||||||
|  |         uploadMediaDetails[0] = UploadMediaDetail(place) | ||||||
|  | 
 | ||||||
|  |         // Now that the UploadItem and its associated UploadMediaDetail objects have been updated, | ||||||
|  |         // update the view with the modified media details of the first upload item | ||||||
|  |         view.updateMediaDetails(uploadMediaDetails) | ||||||
|  |         setUploadIsOfAPlace(true) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Calculates the image quality | ||||||
|  |      * | ||||||
|  |      * @param uploadItemIndex      Index of the UploadItem whose quality is to be checked | ||||||
|  |      * @param inAppPictureLocation In app picture location (if any) | ||||||
|  |      * @param activity             Context reference | ||||||
|  |      * @return true if no internal error occurs, else returns false | ||||||
|  |      */ | ||||||
|  |     override fun getImageQuality( | ||||||
|  |         uploadItemIndex: Int, | ||||||
|  |         inAppPictureLocation: LatLng?, | ||||||
|  |         activity: Activity | ||||||
|  |     ): Boolean { | ||||||
|  |         val uploadItems = repository.getUploads() | ||||||
|  |         view.showProgress(true) | ||||||
|  |         if (uploadItems.isEmpty()) { | ||||||
|  |             view.showProgress(false) | ||||||
|  |             // No internationalization required for this error message because it's an internal error. | ||||||
|  |             view.showMessage( | ||||||
|  |                 "Internal error: Zero upload items received by the Upload Media Detail Fragment. Sorry, please upload again.", | ||||||
|  |                 R.color.color_error | ||||||
|  |             ) | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |         val uploadItem = uploadItems[uploadItemIndex] | ||||||
|  |         compositeDisposable.add(repository.getImageQuality(uploadItem, inAppPictureLocation) | ||||||
|  |             .observeOn(mainThreadScheduler) | ||||||
|  |             .subscribe({ imageResult: Int -> | ||||||
|  |                 storeImageQuality(imageResult, uploadItemIndex, activity, uploadItem) | ||||||
|  |             }, { throwable: Throwable -> | ||||||
|  |                 if (throwable is UnknownHostException) { | ||||||
|  |                     view.showProgress(false) | ||||||
|  |                     view.showConnectionErrorPopup() | ||||||
|  |                 } else { | ||||||
|  |                     view.showMessage(throwable.localizedMessage, R.color.color_error) | ||||||
|  |                 } | ||||||
|  |                 Timber.e(throwable, "Error occurred while handling image") | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
|  |         return true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Stores the image quality in JSON format in SharedPrefs | ||||||
|  |      * | ||||||
|  |      * @param imageResult     Image quality | ||||||
|  |      * @param uploadItemIndex Index of the UploadItem whose quality is calculated | ||||||
|  |      * @param activity        Context reference | ||||||
|  |      * @param uploadItem      UploadItem whose quality is to be checked | ||||||
|  |      */ | ||||||
|  |     private fun storeImageQuality( | ||||||
|  |         imageResult: Int, uploadItemIndex: Int, activity: Activity, uploadItem: UploadItem | ||||||
|  |     ) { | ||||||
|  |         val store = BasicKvStore(activity, UploadActivity.storeNameForCurrentUploadImagesSize) | ||||||
|  |         val value = store.getString(UPLOAD_QUALITIES_KEY, null) | ||||||
|  |         try { | ||||||
|  |             val jsonObject = value.asJsonObject().apply { | ||||||
|  |                 put("UploadItem$uploadItemIndex", imageResult) | ||||||
|  |             } | ||||||
|  |             store.putString(UPLOAD_QUALITIES_KEY, jsonObject.toString()) | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             Timber.e(e) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (uploadItemIndex == 0) { | ||||||
|  |             if (!isBatteryDialogShowing && !isCategoriesDialogShowing) { | ||||||
|  |                 // if battery-optimisation dialog is not being shown, call checkImageQuality | ||||||
|  |                 checkImageQuality(uploadItem, uploadItemIndex) | ||||||
|  |             } else { | ||||||
|  |                 view.showProgress(false) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Used to check image quality from stored qualities and display dialogs | ||||||
|  |      * | ||||||
|  |      * @param uploadItem UploadItem whose quality is to be checked | ||||||
|  |      * @param index      Index of the UploadItem whose quality is to be checked | ||||||
|  |      */ | ||||||
|  |     override fun checkImageQuality(uploadItem: UploadItem, index: Int) { | ||||||
|  |         if ((uploadItem.imageQuality != IMAGE_OK) && (uploadItem.imageQuality != IMAGE_KEEP)) { | ||||||
|  |             val value = basicKvStoreFactory(UploadActivity.storeNameForCurrentUploadImagesSize) | ||||||
|  |                 .getString(UPLOAD_QUALITIES_KEY, null) | ||||||
|  |             try { | ||||||
|  |                 val imageQuality = value.asJsonObject()["UploadItem$index"] as Int | ||||||
|  |                 view.showProgress(false) | ||||||
|  |                 if (imageQuality == IMAGE_OK) { | ||||||
|  |                     uploadItem.hasInvalidLocation = false | ||||||
|  |                     uploadItem.imageQuality = imageQuality | ||||||
|  |                 } else { | ||||||
|  |                     handleBadImage(imageQuality, uploadItem, index) | ||||||
|  |                 } | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 Timber.e(e) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Updates the image qualities stored in JSON, whenever an image is deleted | ||||||
|  |      * | ||||||
|  |      * @param size Size of uploadableFiles | ||||||
|  |      * @param index Index of the UploadItem which was deleted | ||||||
|  |      */ | ||||||
|  |     override fun updateImageQualitiesJSON(size: Int, index: Int) { | ||||||
|  |         val value = basicKvStoreFactory(UploadActivity.storeNameForCurrentUploadImagesSize) | ||||||
|  |             .getString(UPLOAD_QUALITIES_KEY, null) | ||||||
|  |         try { | ||||||
|  |             val jsonObject = value.asJsonObject().apply { | ||||||
|  |                 for (i in index until (size - 1)) { | ||||||
|  |                     put("UploadItem$i", this["UploadItem" + (i + 1)]) | ||||||
|  |                 } | ||||||
|  |                 remove("UploadItem" + (size - 1)) | ||||||
|  |             } | ||||||
|  |             basicKvStoreFactory(UploadActivity.storeNameForCurrentUploadImagesSize) | ||||||
|  |                 .putString(UPLOAD_QUALITIES_KEY, jsonObject.toString()) | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             Timber.e(e) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles bad pictures, like too dark, already on wikimedia, downloaded from internet | ||||||
|  |      * | ||||||
|  |      * @param errorCode Error code of the bad image quality | ||||||
|  |      * @param uploadItem UploadItem whose quality is bad | ||||||
|  |      * @param index Index of item whose quality is bad | ||||||
|  |      */ | ||||||
|  |     private fun handleBadImage( | ||||||
|  |         errorCode: Int, | ||||||
|  |         uploadItem: UploadItem, index: Int | ||||||
|  |     ) { | ||||||
|  |         Timber.d("Handle bad picture with error code %d", errorCode) | ||||||
|  |         if (errorCode >= 8) { // If location of image and nearby does not match | ||||||
|  |             uploadItem.hasInvalidLocation = true | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // If image has some other problems, show popup accordingly | ||||||
|  |         if (errorCode != EMPTY_CAPTION && errorCode != FILE_NAME_EXISTS) { | ||||||
|  |             view.showBadImagePopup(errorCode, index, uploadItem) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * notifies the user that a similar image exists | ||||||
|  |      */ | ||||||
|  |     override fun showSimilarImageFragment( | ||||||
|  |         originalFilePath: String?, | ||||||
|  |         possibleFilePath: String?, | ||||||
|  |         similarImageCoordinates: ImageCoordinates? | ||||||
|  |     ) = view.showSimilarImageFragment(originalFilePath, possibleFilePath, similarImageCoordinates) | ||||||
|  | 
 | ||||||
|  |     private fun String?.asJsonObject() = if (this != null) { | ||||||
|  |         JSONObject(this) | ||||||
|  |     } else { | ||||||
|  |         JSONObject() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         private const val UPLOAD_QUALITIES_KEY = "UploadedImagesQualities" | ||||||
|  |         private val WLM_SUPPORTED_COUNTRIES = listOf( | ||||||
|  |             "am", "at", "az", "br", "hr", "sv", "fi", "fr", "de", "gh", | ||||||
|  |             "in", "ie", "il", "mk", "my", "mt", "pk", "pe", "pl", "ru", | ||||||
|  |             "rw", "si", "es", "se", "tw", "ug", "ua", "us" | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         private val DUMMY = Proxy.newProxyInstance( | ||||||
|  |             UploadMediaDetailsContract.View::class.java.classLoader, | ||||||
|  |             arrayOf<Class<*>>(UploadMediaDetailsContract.View::class.java) | ||||||
|  |         ) { _: Any?, _: Method?, _: Array<Any?>? -> null } as UploadMediaDetailsContract.View | ||||||
|  | 
 | ||||||
|  |         var presenterCallback: UploadMediaDetailFragmentCallback? = null | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Variable used to determine if the battery-optimisation dialog is being shown or not | ||||||
|  |          */ | ||||||
|  |         var isBatteryDialogShowing: Boolean = false | ||||||
|  | 
 | ||||||
|  |         var isCategoriesDialogShowing: Boolean = false | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -208,7 +208,7 @@ object PermissionUtils { | ||||||
|                         activity.getString(android.R.string.cancel), |                         activity.getString(android.R.string.cancel), | ||||||
|                         { |                         { | ||||||
|                             if (activity is UploadActivity) { |                             if (activity is UploadActivity) { | ||||||
|                                 activity.setShowPermissionsDialog(true) |                                 activity.isShowPermissionsDialog = true | ||||||
|                             } |                             } | ||||||
|                             token.continuePermissionRequest() |                             token.continuePermissionRequest() | ||||||
|                         }, |                         }, | ||||||
|  |  | ||||||
|  | @ -814,6 +814,10 @@ | ||||||
|   <string name="usages_on_other_wikis_heading">Andre wikier</string> |   <string name="usages_on_other_wikis_heading">Andre wikier</string> | ||||||
|   <string name="bullet_point">•</string> |   <string name="bullet_point">•</string> | ||||||
|   <string name="file_usages_container_heading">Filanvendelser</string> |   <string name="file_usages_container_heading">Filanvendelser</string> | ||||||
|  |   <string name="account">Konto</string> | ||||||
|  |   <string name="vanish_account">Få konto til at forsvinde</string> | ||||||
|  |   <string name="account_vanish_request_confirm_title">Advarsel om forsvinding af konto</string> | ||||||
|  |   <string name="account_vanish_request_confirm">Forsvinding er en <b>sidste udvej</b> og bør <b>kun bruges, når du for altid ønsker at stoppe med at redigere</b> og også for at skjule så mange af dine tidligere tilknytninger som muligt.<br/><br/> Kontosletning på Wikipedia Commons sker ved at ændre dit kontonavn, således at andre ikke kan genkende dine bidrag i en proces, der kaldes kontoforsvinding (Vanishing). <b>Forsvinding garanterer ikke fuldstændig anonymitet eller fjerner bidrag til projekterne</b> .</string> | ||||||
|   <string name="caption">Billedtekst</string> |   <string name="caption">Billedtekst</string> | ||||||
|   <string name="caption_copied_to_clipboard">Billedtekst kopieret til udklipsholder</string> |   <string name="caption_copied_to_clipboard">Billedtekst kopieret til udklipsholder</string> | ||||||
| </resources> | </resources> | ||||||
|  |  | ||||||
|  | @ -846,4 +846,9 @@ | ||||||
|   <string name="usages_on_other_wikis_heading">אתרי ויקי אחרים</string> |   <string name="usages_on_other_wikis_heading">אתרי ויקי אחרים</string> | ||||||
|   <string name="bullet_point">•</string> |   <string name="bullet_point">•</string> | ||||||
|   <string name="file_usages_container_heading">שימושים בקובץ</string> |   <string name="file_usages_container_heading">שימושים בקובץ</string> | ||||||
|  |   <string name="account">חשבון</string> | ||||||
|  |   <string name="vanish_account">העלמת חשבון</string> | ||||||
|  |   <string name="account_vanish_request_confirm_title">אזהרת העלמת חשבון</string> | ||||||
|  |   <string name="caption">כותרת</string> | ||||||
|  |   <string name="caption_copied_to_clipboard">הכותרת הועתקה ללוח</string> | ||||||
| </resources> | </resources> | ||||||
|  |  | ||||||
|  | @ -707,4 +707,7 @@ | ||||||
|   <string name="usages_on_commons_heading">공용</string> |   <string name="usages_on_commons_heading">공용</string> | ||||||
|   <string name="usages_on_other_wikis_heading">다른 위키</string> |   <string name="usages_on_other_wikis_heading">다른 위키</string> | ||||||
|   <string name="file_usages_container_heading">이 파일을 사용하는 문서</string> |   <string name="file_usages_container_heading">이 파일을 사용하는 문서</string> | ||||||
|  |   <string name="account">계정</string> | ||||||
|  |   <string name="caption">캡션</string> | ||||||
|  |   <string name="caption_copied_to_clipboard">캡션이 클립보드에 복사되었습니다</string> | ||||||
| </resources> | </resources> | ||||||
|  |  | ||||||
|  | @ -141,4 +141,5 @@ | ||||||
|   <string name="label">Аталыш</string> |   <string name="label">Аталыш</string> | ||||||
|   <string name="description">Сыпаттама</string> |   <string name="description">Сыпаттама</string> | ||||||
|   <string name="title_page_bookmarks_items">Элементтер</string> |   <string name="title_page_bookmarks_items">Элементтер</string> | ||||||
|  |   <string name="account">Аккаунт</string> | ||||||
| </resources> | </resources> | ||||||
|  |  | ||||||
|  | @ -533,6 +533,8 @@ | ||||||
|   <string name="error_while_loading">Feeler beim Lueden</string> |   <string name="error_while_loading">Feeler beim Lueden</string> | ||||||
|   <string name="usages_on_commons_heading">Commons</string> |   <string name="usages_on_commons_heading">Commons</string> | ||||||
|   <string name="usages_on_other_wikis_heading">Aner Wikien</string> |   <string name="usages_on_other_wikis_heading">Aner Wikien</string> | ||||||
|  |   <string name="account">Kont</string> | ||||||
|  |   <string name="vanish_account">Kont opléisen</string> | ||||||
|   <string name="caption">Beschrëftung</string> |   <string name="caption">Beschrëftung</string> | ||||||
|   <string name="caption_copied_to_clipboard">Text an den Tëschespäicher kopéiert</string> |   <string name="caption_copied_to_clipboard">Text an den Tëschespäicher kopéiert</string> | ||||||
| </resources> | </resources> | ||||||
|  |  | ||||||
|  | @ -810,6 +810,11 @@ | ||||||
|   <string name="usages_on_other_wikis_heading">Други викија</string> |   <string name="usages_on_other_wikis_heading">Други викија</string> | ||||||
|   <string name="bullet_point">•</string> |   <string name="bullet_point">•</string> | ||||||
|   <string name="file_usages_container_heading">Употреби на податотеката</string> |   <string name="file_usages_container_heading">Употреби на податотеката</string> | ||||||
|  |   <string name="title_activity_single_web_view">SingleWebViewActivity</string> | ||||||
|  |   <string name="account">Сметка</string> | ||||||
|  |   <string name="vanish_account">Исчезни сметка</string> | ||||||
|  |   <string name="account_vanish_request_confirm_title">Предупредување за исчезнување на сметка</string> | ||||||
|  |   <string name="account_vanish_request_confirm">Исчезнувањето е <b>крајна мерка</b> и треба да се користи само ако сакате да престанете да уредувате засекогаш/b> и да скриете што повеќе од вашите досегашни врски.<br/><br/>Бришењето сметки на Википедија се врши со менување на името на вашата сметка, така што другите не би можеле да ги препознаат вашите придонеси во постапка наречена „исчезнување“ на сметка.<b>Исчезнувањето не гарантира целосна анонимност и не ги отстранува придонесите на проектите</b>.</string> | ||||||
|   <string name="caption">Толкување</string> |   <string name="caption">Толкување</string> | ||||||
|   <string name="caption_copied_to_clipboard">Толкувањето е ставено во меѓускладот</string> |   <string name="caption_copied_to_clipboard">Толкувањето е ставено во меѓускладот</string> | ||||||
| </resources> | </resources> | ||||||
|  |  | ||||||
|  | @ -51,7 +51,7 @@ | ||||||
|   <string name="upload_failed_notification_title" fuzzy="true"> %1$s उर्ध्वभरण असफल भयो</string> |   <string name="upload_failed_notification_title" fuzzy="true"> %1$s उर्ध्वभरण असफल भयो</string> | ||||||
|   <string name="upload_failed_notification_subtitle">हेर्नको लागि ट्याप गर्नुहोस्</string> |   <string name="upload_failed_notification_subtitle">हेर्नको लागि ट्याप गर्नुहोस्</string> | ||||||
|   <string name="upload_paused_notification_subtitle">हेर्नको लागि ट्याप गर्नुहोस्</string> |   <string name="upload_paused_notification_subtitle">हेर्नको लागि ट्याप गर्नुहोस्</string> | ||||||
|   <string name="title_activity_contributions">मेरा हालैका उर्ध्वभरणहरू</string> |   <string name="title_activity_contributions">मेरा वर्तमानका उर्ध्वभरणहरू</string> | ||||||
|   <string name="contribution_state_queued">लाममा राखियो</string> |   <string name="contribution_state_queued">लाममा राखियो</string> | ||||||
|   <string name="contribution_state_failed">असफल भयो</string> |   <string name="contribution_state_failed">असफल भयो</string> | ||||||
|   <string name="contribution_state_in_progress">%1$d%% पूरा भयो</string> |   <string name="contribution_state_in_progress">%1$d%% पूरा भयो</string> | ||||||
|  |  | ||||||
|  | @ -806,6 +806,11 @@ | ||||||
|   <string name="usages_on_other_wikis_heading">Àutre wiki</string> |   <string name="usages_on_other_wikis_heading">Àutre wiki</string> | ||||||
|   <string name="bullet_point">•</string> |   <string name="bullet_point">•</string> | ||||||
|   <string name="file_usages_container_heading">Usagi dl\'archivi</string> |   <string name="file_usages_container_heading">Usagi dl\'archivi</string> | ||||||
|  |   <string name="title_activity_single_web_view">SingleWebViewActivity</string> | ||||||
|  |   <string name="account">Cont</string> | ||||||
|  |   <string name="vanish_account">Flambé ël cont</string> | ||||||
|  |   <string name="account_vanish_request_confirm_title">Avis d\'eliminassion dël cont</string> | ||||||
|  |   <string name="account_vanish_request_confirm">L\'eliminassion a l\'é <b>l\'ùltima arsorsa</b> e a dovrìa <b>esse dovrà mach si chiel a veul chité ëd modifiché për sempe</b> e ëdcò s\'a veul ëstërmé pi che possìbil soe assossiassion passà.<br/><br/>La dëscancelassion ëd cont su Wikimedia a l\'é fàita an modificand sò stranòm an manera che j\'àutri a peulo pa arconòsse soe contribussion ant un process ciamà dëscancelassion ëd cont. <b>La sparission a garantiss pa l\'anonimà complet ni a gava le contribussion dai proget</b>.</string> | ||||||
|   <string name="caption">Legenda</string> |   <string name="caption">Legenda</string> | ||||||
|   <string name="caption_copied_to_clipboard">Legenda copià an sla taulëtta</string> |   <string name="caption_copied_to_clipboard">Legenda copià an sla taulëtta</string> | ||||||
| </resources> | </resources> | ||||||
|  |  | ||||||
|  | @ -869,4 +869,7 @@ | ||||||
|   <string name="usages_on_commons_heading">Викисклад</string> |   <string name="usages_on_commons_heading">Викисклад</string> | ||||||
|   <string name="usages_on_other_wikis_heading">Другие вики</string> |   <string name="usages_on_other_wikis_heading">Другие вики</string> | ||||||
|   <string name="file_usages_container_heading">Использование файла</string> |   <string name="file_usages_container_heading">Использование файла</string> | ||||||
|  |   <string name="account">Учётная запись</string> | ||||||
|  |   <string name="caption">Подпись</string> | ||||||
|  |   <string name="caption_copied_to_clipboard">Подпись скопирована в буфер обмена</string> | ||||||
| </resources> | </resources> | ||||||
|  |  | ||||||
|  | @ -277,5 +277,6 @@ | ||||||
|   <string name="custom_selector_delete">مٹاؤ</string> |   <string name="custom_selector_delete">مٹاؤ</string> | ||||||
|   <string name="custom_selector_cancel">منسوخ</string> |   <string name="custom_selector_cancel">منسوخ</string> | ||||||
|   <string name="usages_on_commons_heading">کامنز</string> |   <string name="usages_on_commons_heading">کامنز</string> | ||||||
|  |   <string name="account">کھاتہ</string> | ||||||
|   <string name="caption">عنوان</string> |   <string name="caption">عنوان</string> | ||||||
| </resources> | </resources> | ||||||
|  |  | ||||||
|  | @ -743,6 +743,9 @@ | ||||||
|   <string name="failed">Није успело</string> |   <string name="failed">Није успело</string> | ||||||
|   <string name="green_pin">Ово место већ има слику</string> |   <string name="green_pin">Ово место већ има слику</string> | ||||||
|   <string name="grey_pin">Проверавање да ли ово место има слику.</string> |   <string name="grey_pin">Проверавање да ли ово место има слику.</string> | ||||||
|  |   <string name="account">Налог</string> | ||||||
|  |   <string name="vanish_account">Учтиви нестанак</string> | ||||||
|  |   <string name="account_vanish_request_confirm">Нестајање је <b>последња опција</b> и треба је користити <b>само када желите да заувек престанете са уређивањем</b>, као и да сакријете што више својих прошлих асоцијација.<br/><br/>Брисање налога на Викимедијној остави се врши променом назива налога тако да други не могу да препознају Ваше доприносе у процесу који се зове учтиви нестанак. <b>Нестајање не гарантује потпуну анонимност и не уклања доприносе на пројектима</b>.</string> | ||||||
|   <string name="caption">Поднапис</string> |   <string name="caption">Поднапис</string> | ||||||
|   <string name="caption_copied_to_clipboard">Поднапис копиран</string> |   <string name="caption_copied_to_clipboard">Поднапис копиран</string> | ||||||
| </resources> | </resources> | ||||||
|  |  | ||||||
|  | @ -107,6 +107,8 @@ | ||||||
|   <string name="menu_from_camera">Ta foto</string> |   <string name="menu_from_camera">Ta foto</string> | ||||||
|   <string name="menu_nearby">I närheten</string> |   <string name="menu_nearby">I närheten</string> | ||||||
|   <string name="provider_contributions">Mina uppladdningar</string> |   <string name="provider_contributions">Mina uppladdningar</string> | ||||||
|  |   <string name="menu_copy_link">Kopiera länk</string> | ||||||
|  |   <string name="menu_link_copied">Länken har kopierats till urklipp</string> | ||||||
|   <string name="menu_share">Dela</string> |   <string name="menu_share">Dela</string> | ||||||
|   <string name="menu_view_file_page">Visa filsida</string> |   <string name="menu_view_file_page">Visa filsida</string> | ||||||
|   <string name="share_title_hint">Bildtext (obligatoriskt)</string> |   <string name="share_title_hint">Bildtext (obligatoriskt)</string> | ||||||
|  | @ -282,6 +284,7 @@ | ||||||
|   <string name="copy_wikicode">Kopiera wikitexten till urklipp</string> |   <string name="copy_wikicode">Kopiera wikitexten till urklipp</string> | ||||||
|   <string name="wikicode_copied">Wikitexten kopierades till urklipp</string> |   <string name="wikicode_copied">Wikitexten kopierades till urklipp</string> | ||||||
|   <string name="nearby_location_not_available">\"I närheten\" kanske inte fungerar ordentligt. Platsen är inte tillgänglig.</string> |   <string name="nearby_location_not_available">\"I närheten\" kanske inte fungerar ordentligt. Platsen är inte tillgänglig.</string> | ||||||
|  |   <string name="nearby_showing_pins_offline">Internet inte tillgängligt. Visar endast cachade platser.</string> | ||||||
|   <string name="upload_location_access_denied">Platsåtkomst nekad. Ange din plats manuellt för att använda den här funktionen.</string> |   <string name="upload_location_access_denied">Platsåtkomst nekad. Ange din plats manuellt för att använda den här funktionen.</string> | ||||||
|   <string name="location_permission_rationale_nearby">Behörighet krävs för att visa en lista över platser i närheten</string> |   <string name="location_permission_rationale_nearby">Behörighet krävs för att visa en lista över platser i närheten</string> | ||||||
|   <string name="location_permission_rationale_explore">Behörighet krävs för att visa en lista över bilder i närheten</string> |   <string name="location_permission_rationale_explore">Behörighet krävs för att visa en lista över bilder i närheten</string> | ||||||
|  | @ -365,11 +368,13 @@ | ||||||
|   <string name="delete">Radera</string> |   <string name="delete">Radera</string> | ||||||
|   <string name="Achievements">Prestationer</string> |   <string name="Achievements">Prestationer</string> | ||||||
|   <string name="Profile">Profil</string> |   <string name="Profile">Profil</string> | ||||||
|  |   <string name="badges">Märken</string> | ||||||
|   <string name="statistics">Statistik</string> |   <string name="statistics">Statistik</string> | ||||||
|   <string name="statistics_thanks">Mottagna tack</string> |   <string name="statistics_thanks">Mottagna tack</string> | ||||||
|   <string name="statistics_featured">Utvalda bilder</string> |   <string name="statistics_featured">Utvalda bilder</string> | ||||||
|   <string name="statistics_wikidata_edits">Bilder via \"Platser i närheten\"</string> |   <string name="statistics_wikidata_edits">Bilder via \"Platser i närheten\"</string> | ||||||
|   <string name="level" fuzzy="true">Nivå</string> |   <string name="level">Nivå %d</string> | ||||||
|  |   <string name="profileLevel">%s (Nivå %s)</string> | ||||||
|   <string name="images_uploaded">Uppladdade bilder</string> |   <string name="images_uploaded">Uppladdade bilder</string> | ||||||
|   <string name="image_reverts">Bilder som inte har återställts</string> |   <string name="image_reverts">Bilder som inte har återställts</string> | ||||||
|   <string name="images_used_by_wiki">Bilder som används</string> |   <string name="images_used_by_wiki">Bilder som används</string> | ||||||
|  | @ -401,6 +406,7 @@ | ||||||
|   <string name="map_application_missing">Ingen kompatibel kartapp hittades på din enhet. Installera en kartapp för att använda denna funktion.</string> |   <string name="map_application_missing">Ingen kompatibel kartapp hittades på din enhet. Installera en kartapp för att använda denna funktion.</string> | ||||||
|   <string name="title_page_bookmarks_pictures">Bilder</string> |   <string name="title_page_bookmarks_pictures">Bilder</string> | ||||||
|   <string name="title_page_bookmarks_locations">Platser</string> |   <string name="title_page_bookmarks_locations">Platser</string> | ||||||
|  |   <string name="title_page_bookmarks_categories">Kategorier</string> | ||||||
|   <string name="menu_bookmark">Lägg till/ta bort bokmärke</string> |   <string name="menu_bookmark">Lägg till/ta bort bokmärke</string> | ||||||
|   <string name="provider_bookmarks">Bokmärken</string> |   <string name="provider_bookmarks">Bokmärken</string> | ||||||
|   <string name="bookmark_empty">Du har inte lagt till några bokmärken</string> |   <string name="bookmark_empty">Du har inte lagt till några bokmärken</string> | ||||||
|  | @ -802,4 +808,12 @@ | ||||||
|   <string name="red_pin">Det här platsen har ännu ingen bild. Gå och ta en!</string> |   <string name="red_pin">Det här platsen har ännu ingen bild. Gå och ta en!</string> | ||||||
|   <string name="green_pin">Det här platsen har redan en bild.</string> |   <string name="green_pin">Det här platsen har redan en bild.</string> | ||||||
|   <string name="grey_pin">Kollar nu om den här platsen har en bild.</string> |   <string name="grey_pin">Kollar nu om den här platsen har en bild.</string> | ||||||
|  |   <string name="error_while_loading">Fel med inladdning</string> | ||||||
|  |   <string name="no_usages_found">Inga användningar hittades</string> | ||||||
|  |   <string name="usages_on_commons_heading">Commons</string> | ||||||
|  |   <string name="usages_on_other_wikis_heading">Andra wikier</string> | ||||||
|  |   <string name="file_usages_container_heading">Filanvändning</string> | ||||||
|  |   <string name="account">Konto</string> | ||||||
|  |   <string name="caption">Bildtext</string> | ||||||
|  |   <string name="caption_copied_to_clipboard">Bildtext kopierades till urklipp</string> | ||||||
| </resources> | </resources> | ||||||
|  |  | ||||||
|  | @ -13,8 +13,8 @@ import com.nhaarman.mockitokotlin2.verify | ||||||
| import fr.free.nrw.commons.CameraPosition | import fr.free.nrw.commons.CameraPosition | ||||||
| import fr.free.nrw.commons.TestCommonsApplication | import fr.free.nrw.commons.TestCommonsApplication | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore | import fr.free.nrw.commons.kvstore.JsonKvStore | ||||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION | import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Companion.LAST_LOCATION | ||||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM | import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Companion.LAST_ZOOM | ||||||
| import io.reactivex.android.plugins.RxAndroidPlugins | import io.reactivex.android.plugins.RxAndroidPlugins | ||||||
| import io.reactivex.schedulers.Schedulers | import io.reactivex.schedulers.Schedulers | ||||||
| import org.junit.Assert | import org.junit.Assert | ||||||
|  |  | ||||||
|  | @ -223,7 +223,7 @@ class SettingsFragmentUnitTests { | ||||||
|                 RecentLanguagesAdapter( |                 RecentLanguagesAdapter( | ||||||
|                     context, |                     context, | ||||||
|                     listOf(Language("English", "en")), |                     listOf(Language("English", "en")), | ||||||
|                     hashMapOf<String, String>(), |                     mutableMapOf(), | ||||||
|                 ), |                 ), | ||||||
|             ) |             ) | ||||||
|         val method: Method = |         val method: Method = | ||||||
|  |  | ||||||
|  | @ -30,7 +30,7 @@ class LanguagesAdapterTest { | ||||||
|     private lateinit var context: Context |     private lateinit var context: Context | ||||||
| 
 | 
 | ||||||
|     @Mock |     @Mock | ||||||
|     private lateinit var selectedLanguages: HashMap<Integer, String> |     private lateinit var selectedLanguages: MutableMap<Int, String> | ||||||
| 
 | 
 | ||||||
|     @Mock |     @Mock | ||||||
|     private lateinit var parent: ViewGroup |     private lateinit var parent: ViewGroup | ||||||
|  | @ -41,7 +41,7 @@ class LanguagesAdapterTest { | ||||||
| 
 | 
 | ||||||
|     private lateinit var languagesAdapter: LanguagesAdapter |     private lateinit var languagesAdapter: LanguagesAdapter | ||||||
|     private lateinit var convertView: View |     private lateinit var convertView: View | ||||||
|     private var selectLanguages: HashMap<Integer, String> = HashMap() |     private var selectLanguages: MutableMap<Int, String> = mutableMapOf() | ||||||
| 
 | 
 | ||||||
|     @Before |     @Before | ||||||
|     @Throws(Exception::class) |     @Throws(Exception::class) | ||||||
|  | @ -94,8 +94,8 @@ class LanguagesAdapterTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testSelectLanguageNotEmpty() { |     fun testSelectLanguageNotEmpty() { | ||||||
|         selectLanguages[Integer(0)] = "es" |         selectLanguages[0] = "es" | ||||||
|         selectLanguages[Integer(1)] = "de" |         selectLanguages[1] = "de" | ||||||
|         languagesAdapter = LanguagesAdapter(context, selectLanguages) |         languagesAdapter = LanguagesAdapter(context, selectLanguages) | ||||||
| 
 | 
 | ||||||
|         Assertions.assertEquals(false, languagesAdapter.isEnabled(languagesAdapter.getIndexOfLanguageCode("es"))) |         Assertions.assertEquals(false, languagesAdapter.isEnabled(languagesAdapter.getIndexOfLanguageCode("es"))) | ||||||
|  |  | ||||||
|  | @ -246,7 +246,7 @@ class UploadMediaDetailAdapterUnitTest { | ||||||
|                 RecentLanguagesAdapter( |                 RecentLanguagesAdapter( | ||||||
|                     context, |                     context, | ||||||
|                     listOf(Language("English", "en")), |                     listOf(Language("English", "en")), | ||||||
|                     hashMapOf<String, String>(), |                     mutableMapOf(), | ||||||
|                 ), |                 ), | ||||||
|             ) |             ) | ||||||
|         val method: Method = |         val method: Method = | ||||||
|  |  | ||||||
|  | @ -1,10 +1,12 @@ | ||||||
| package fr.free.nrw.commons.upload | package fr.free.nrw.commons.upload | ||||||
| 
 | 
 | ||||||
| import android.net.Uri | import android.net.Uri | ||||||
|  | import com.nhaarman.mockitokotlin2.argumentCaptor | ||||||
|  | import com.nhaarman.mockitokotlin2.isA | ||||||
| import com.nhaarman.mockitokotlin2.mock | import com.nhaarman.mockitokotlin2.mock | ||||||
| import com.nhaarman.mockitokotlin2.whenever | import com.nhaarman.mockitokotlin2.whenever | ||||||
|  | import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.filepicker.UploadableFile | import fr.free.nrw.commons.filepicker.UploadableFile | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore |  | ||||||
| import fr.free.nrw.commons.location.LatLng | import fr.free.nrw.commons.location.LatLng | ||||||
| import fr.free.nrw.commons.nearby.Place | import fr.free.nrw.commons.nearby.Place | ||||||
| import fr.free.nrw.commons.repository.UploadRepository | import fr.free.nrw.commons.repository.UploadRepository | ||||||
|  | @ -24,6 +26,7 @@ import org.junit.Test | ||||||
| import org.junit.runner.RunWith | import org.junit.runner.RunWith | ||||||
| import org.mockito.ArgumentCaptor | import org.mockito.ArgumentCaptor | ||||||
| import org.mockito.ArgumentMatchers | import org.mockito.ArgumentMatchers | ||||||
|  | import org.mockito.ArgumentMatchers.anyInt | ||||||
| import org.mockito.Mock | import org.mockito.Mock | ||||||
| import org.mockito.MockedStatic | import org.mockito.MockedStatic | ||||||
| import org.mockito.Mockito | import org.mockito.Mockito | ||||||
|  | @ -55,7 +58,7 @@ class UploadMediaPresenterTest { | ||||||
|     private lateinit var place: Place |     private lateinit var place: Place | ||||||
| 
 | 
 | ||||||
|     @Mock |     @Mock | ||||||
|     private var location: LatLng? = null |     private lateinit var location: LatLng | ||||||
| 
 | 
 | ||||||
|     @Mock |     @Mock | ||||||
|     private lateinit var uploadItem: UploadItem |     private lateinit var uploadItem: UploadItem | ||||||
|  | @ -63,18 +66,12 @@ class UploadMediaPresenterTest { | ||||||
|     @Mock |     @Mock | ||||||
|     private lateinit var imageCoordinates: ImageCoordinates |     private lateinit var imageCoordinates: ImageCoordinates | ||||||
| 
 | 
 | ||||||
|     @Mock |  | ||||||
|     private lateinit var uploadMediaDetails: List<UploadMediaDetail> |  | ||||||
| 
 |  | ||||||
|     private lateinit var testObservableUploadItem: Observable<UploadItem> |     private lateinit var testObservableUploadItem: Observable<UploadItem> | ||||||
|     private lateinit var testSingleImageResult: Single<Int> |     private lateinit var testSingleImageResult: Single<Int> | ||||||
| 
 | 
 | ||||||
|     private lateinit var testScheduler: TestScheduler |     private lateinit var testScheduler: TestScheduler | ||||||
|     private lateinit var mockedCountry: MockedStatic<Coordinates2Country> |     private lateinit var mockedCountry: MockedStatic<Coordinates2Country> | ||||||
| 
 | 
 | ||||||
|     @Mock |  | ||||||
|     private lateinit var jsonKvStore: JsonKvStore |  | ||||||
| 
 |  | ||||||
|     @Mock |     @Mock | ||||||
|     lateinit var mockActivity: UploadActivity |     lateinit var mockActivity: UploadActivity | ||||||
| 
 | 
 | ||||||
|  | @ -91,7 +88,6 @@ class UploadMediaPresenterTest { | ||||||
|         uploadMediaPresenter = |         uploadMediaPresenter = | ||||||
|             UploadMediaPresenter( |             UploadMediaPresenter( | ||||||
|                 repository, |                 repository, | ||||||
|                 jsonKvStore, |  | ||||||
|                 testScheduler, |                 testScheduler, | ||||||
|                 testScheduler, |                 testScheduler, | ||||||
|             ) |             ) | ||||||
|  | @ -120,10 +116,7 @@ class UploadMediaPresenterTest { | ||||||
|         uploadMediaPresenter.receiveImage(uploadableFile, place, location) |         uploadMediaPresenter.receiveImage(uploadableFile, place, location) | ||||||
|         verify(view).showProgress(true) |         verify(view).showProgress(true) | ||||||
|         testScheduler.triggerActions() |         testScheduler.triggerActions() | ||||||
|         verify(view).onImageProcessed( |         verify(view).onImageProcessed(isA()) | ||||||
|             ArgumentMatchers.any(UploadItem::class.java), |  | ||||||
|             ArgumentMatchers.any(Place::class.java), |  | ||||||
|         ) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -167,7 +160,7 @@ class UploadMediaPresenterTest { | ||||||
|     @Test |     @Test | ||||||
|     fun emptyFileNameTest() { |     fun emptyFileNameTest() { | ||||||
|         uploadMediaPresenter.handleCaptionResult(EMPTY_CAPTION, uploadItem) |         uploadMediaPresenter.handleCaptionResult(EMPTY_CAPTION, uploadItem) | ||||||
|         verify(view).showMessage(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()) |         verify(view).showMessage(R.string.add_caption_toast, R.color.color_error) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -226,12 +219,11 @@ class UploadMediaPresenterTest { | ||||||
|     @Test |     @Test | ||||||
|     fun fetchImageAndTitleTest() { |     fun fetchImageAndTitleTest() { | ||||||
|         whenever(repository.getUploads()).thenReturn(listOf(uploadItem)) |         whenever(repository.getUploads()).thenReturn(listOf(uploadItem)) | ||||||
|         whenever(repository.getUploadItem(ArgumentMatchers.anyInt())) |         whenever(repository.getUploadItem(ArgumentMatchers.anyInt())).thenReturn(uploadItem) | ||||||
|             .thenReturn(uploadItem) |  | ||||||
|         whenever(uploadItem.uploadMediaDetails).thenReturn(mutableListOf()) |         whenever(uploadItem.uploadMediaDetails).thenReturn(mutableListOf()) | ||||||
| 
 | 
 | ||||||
|         uploadMediaPresenter.fetchTitleAndDescription(0) |         uploadMediaPresenter.fetchTitleAndDescription(0) | ||||||
|         verify(view).updateMediaDetails(ArgumentMatchers.any()) |         verify(view).updateMediaDetails(isA()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -273,12 +265,9 @@ class UploadMediaPresenterTest { | ||||||
|         verify(view).showProgress(true) |         verify(view).showProgress(true) | ||||||
|         testScheduler.triggerActions() |         testScheduler.triggerActions() | ||||||
| 
 | 
 | ||||||
|         val captor: ArgumentCaptor<UploadItem> = ArgumentCaptor.forClass(UploadItem::class.java) |         val captor = argumentCaptor<UploadItem>() | ||||||
|         verify(view).onImageProcessed( |         verify(view).onImageProcessed(captor.capture()) | ||||||
|             captor.capture(), |  | ||||||
|             ArgumentMatchers.any(Place::class.java), |  | ||||||
|         ) |  | ||||||
| 
 | 
 | ||||||
|         assertEquals("Exptected contry code", "de", captor.value.countryCode) |         assertEquals("Exptected contry code", "de", captor.firstValue.countryCode) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -140,6 +140,6 @@ class UploadModelUnitTest { | ||||||
|     @Ignore |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun testSetSelectedExistingDepictions() { |     fun testSetSelectedExistingDepictions() { | ||||||
|         uploadModel.selectedExistingDepictions = listOf("") |         uploadModel.selectedExistingDepictions = mutableListOf("") | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,9 @@ | ||||||
| package fr.free.nrw.commons.upload | package fr.free.nrw.commons.upload | ||||||
| 
 | 
 | ||||||
|  | import com.nhaarman.mockitokotlin2.any | ||||||
| import com.nhaarman.mockitokotlin2.mock | import com.nhaarman.mockitokotlin2.mock | ||||||
| import com.nhaarman.mockitokotlin2.verify | import com.nhaarman.mockitokotlin2.verify | ||||||
|  | import com.nhaarman.mockitokotlin2.whenever | ||||||
| import fr.free.nrw.commons.Media | import fr.free.nrw.commons.Media | ||||||
| import fr.free.nrw.commons.category.CategoriesModel | import fr.free.nrw.commons.category.CategoriesModel | ||||||
| import fr.free.nrw.commons.category.CategoryItem | import fr.free.nrw.commons.category.CategoryItem | ||||||
|  | @ -17,6 +19,7 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | ||||||
| import io.reactivex.Completable | import io.reactivex.Completable | ||||||
| import io.reactivex.Observable | import io.reactivex.Observable | ||||||
| import io.reactivex.Single | import io.reactivex.Single | ||||||
|  | import org.junit.Assert.assertSame | ||||||
| import org.junit.Before | import org.junit.Before | ||||||
| import org.junit.Test | import org.junit.Test | ||||||
| import org.junit.jupiter.api.Assertions.assertEquals | import org.junit.jupiter.api.Assertions.assertEquals | ||||||
|  | @ -118,7 +121,9 @@ class UploadRepositoryUnitTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testGetUploads() { |     fun testGetUploads() { | ||||||
|         assertEquals(repository.getUploads(), uploadModel.uploads) |         val result = listOf(uploadItem) | ||||||
|  |         whenever(uploadModel.uploads).thenReturn(result) | ||||||
|  |         assertSame(result, repository.getUploads()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|  | @ -136,10 +141,10 @@ class UploadRepositoryUnitTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testSearchAll() { |     fun testSearchAll() { | ||||||
|         assertEquals( |         val empty = Observable.empty<List<CategoryItem>>() | ||||||
|             repository.searchAll("", listOf(), listOf()), |         whenever(categoriesModel.searchAll(any(), any(), any())).thenReturn(empty) | ||||||
|             categoriesModel.searchAll("", listOf(), listOf()), |         assertSame(empty, repository.searchAll("", listOf(), listOf())) | ||||||
|         ) | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|  | @ -164,7 +169,9 @@ class UploadRepositoryUnitTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testGetLicenses() { |     fun testGetLicenses() { | ||||||
|         assertEquals(repository.getLicenses(), uploadModel.licenses) |         whenever(uploadModel.licenses).thenReturn(listOf()) | ||||||
|  |         repository.getLicenses() | ||||||
|  |         verify(uploadModel).licenses | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|  | @ -208,10 +215,10 @@ class UploadRepositoryUnitTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testGetUploadItemCaseNonNull() { |     fun testGetUploadItemCaseNonNull() { | ||||||
|         `when`(uploadModel.items).thenReturn(listOf(uploadItem)) |         `when`(uploadModel.items).thenReturn(mutableListOf(uploadItem)) | ||||||
|         assertEquals( |         assertEquals( | ||||||
|             repository.getUploadItem(0), |             repository.getUploadItem(0), | ||||||
|             uploadModel.items[0], |             uploadItem, | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -220,19 +227,6 @@ class UploadRepositoryUnitTest { | ||||||
|         assertEquals(repository.getUploadItem(-1), null) |         assertEquals(repository.getUploadItem(-1), null) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |  | ||||||
|     fun testSetSelectedLicense() { |  | ||||||
|         assertEquals(repository.setSelectedLicense(""), uploadModel.setSelectedLicense("")) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     fun testSetSelectedExistingDepictions() { |  | ||||||
|         assertEquals( |  | ||||||
|             repository.setSelectedExistingDepictions(listOf("")), |  | ||||||
|             uploadModel.setSelectedExistingDepictions(listOf("")), |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |     @Test | ||||||
|     fun testOnDepictItemClicked() { |     fun testOnDepictItemClicked() { | ||||||
|         assertEquals( |         assertEquals( | ||||||
|  | @ -243,12 +237,14 @@ class UploadRepositoryUnitTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testGetSelectedDepictions() { |     fun testGetSelectedDepictions() { | ||||||
|         assertEquals(repository.getSelectedDepictions(), uploadModel.selectedDepictions) |         repository.getSelectedDepictions() | ||||||
|  |         verify(uploadModel).selectedDepictions | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testGetSelectedExistingDepictions() { |     fun testGetSelectedExistingDepictions() { | ||||||
|         assertEquals(repository.getSelectedExistingDepictions(), uploadModel.selectedExistingDepictions) |         repository.getSelectedExistingDepictions() | ||||||
|  |         verify(uploadModel).selectedExistingDepictions | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|  | @ -324,8 +320,8 @@ class UploadRepositoryUnitTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testIsWMLSupportedForThisPlace() { |     fun testIsWMLSupportedForThisPlace() { | ||||||
|         `when`(uploadModel.items).thenReturn(listOf(uploadItem)) |         whenever(uploadModel.items).thenReturn(mutableListOf(uploadItem)) | ||||||
|         `when`(uploadItem.isWLMUpload).thenReturn(true) |         whenever(uploadItem.isWLMUpload).thenReturn(true) | ||||||
|         assertEquals( |         assertEquals( | ||||||
|             repository.isWMLSupportedForThisPlace(), |             repository.isWMLSupportedForThisPlace(), | ||||||
|             true, |             true, | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ import fr.free.nrw.commons.upload.ImageCoordinates | ||||||
| import fr.free.nrw.commons.upload.UploadActivity | import fr.free.nrw.commons.upload.UploadActivity | ||||||
| import fr.free.nrw.commons.upload.UploadItem | import fr.free.nrw.commons.upload.UploadItem | ||||||
| import fr.free.nrw.commons.upload.UploadMediaDetailAdapter | import fr.free.nrw.commons.upload.UploadMediaDetailAdapter | ||||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM | import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Companion.LAST_ZOOM | ||||||
| import org.junit.Assert | import org.junit.Assert | ||||||
| import org.junit.Before | import org.junit.Before | ||||||
| import org.junit.Test | import org.junit.Test | ||||||
|  | @ -100,7 +100,7 @@ class UploadMediaDetailFragmentUnitTest { | ||||||
|     private lateinit var place: Place |     private lateinit var place: Place | ||||||
| 
 | 
 | ||||||
|     @Mock |     @Mock | ||||||
|     private var location: fr.free.nrw.commons.location.LatLng? = null |     private lateinit var location: LatLng | ||||||
| 
 | 
 | ||||||
|     @Mock |     @Mock | ||||||
|     private lateinit var defaultKvStore: JsonKvStore |     private lateinit var defaultKvStore: JsonKvStore | ||||||
|  | @ -153,12 +153,6 @@ class UploadMediaDetailFragmentUnitTest { | ||||||
|         Assert.assertNotNull(fragment) |         Assert.assertNotNull(fragment) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testSetCallback() { |  | ||||||
|         fragment.setCallback(null) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |     @Test | ||||||
|     @Throws(Exception::class) |     @Throws(Exception::class) | ||||||
|     fun testOnCreate() { |     fun testOnCreate() { | ||||||
|  | @ -194,7 +188,7 @@ class UploadMediaDetailFragmentUnitTest { | ||||||
|         Whitebox.setInternalState(fragment, "presenter", presenter) |         Whitebox.setInternalState(fragment, "presenter", presenter) | ||||||
|         val method: Method = |         val method: Method = | ||||||
|             UploadMediaDetailFragment::class.java.getDeclaredMethod( |             UploadMediaDetailFragment::class.java.getDeclaredMethod( | ||||||
|                 "init", |                 "initializeFragment", | ||||||
|             ) |             ) | ||||||
|         method.isAccessible = true |         method.isAccessible = true | ||||||
|         method.invoke(fragment) |         method.invoke(fragment) | ||||||
|  | @ -209,7 +203,7 @@ class UploadMediaDetailFragmentUnitTest { | ||||||
|         `when`(callback.totalNumberOfSteps).thenReturn(5) |         `when`(callback.totalNumberOfSteps).thenReturn(5) | ||||||
|         val method: Method = |         val method: Method = | ||||||
|             UploadMediaDetailFragment::class.java.getDeclaredMethod( |             UploadMediaDetailFragment::class.java.getDeclaredMethod( | ||||||
|                 "init", |                 "initializeFragment", | ||||||
|             ) |             ) | ||||||
|         method.isAccessible = true |         method.isAccessible = true | ||||||
|         method.invoke(fragment) |         method.invoke(fragment) | ||||||
|  | @ -229,22 +223,6 @@ class UploadMediaDetailFragmentUnitTest { | ||||||
|         method.invoke(fragment, R.string.media_detail_step_title, R.string.media_details_tooltip) |         method.invoke(fragment, R.string.media_detail_step_title, R.string.media_details_tooltip) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnNextButtonClicked() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         Whitebox.setInternalState(fragment, "presenter", presenter) |  | ||||||
|         fragment.onNextButtonClicked() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnPreviousButtonClicked() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         Whitebox.setInternalState(fragment, "presenter", presenter) |  | ||||||
|         fragment.onPreviousButtonClicked() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |     @Test | ||||||
|     @Throws(Exception::class) |     @Throws(Exception::class) | ||||||
|     fun testShowSimilarImageFragment() { |     fun testShowSimilarImageFragment() { | ||||||
|  | @ -258,7 +236,7 @@ class UploadMediaDetailFragmentUnitTest { | ||||||
|     fun testOnImageProcessed() { |     fun testOnImageProcessed() { | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |         Shadows.shadowOf(Looper.getMainLooper()).idle() | ||||||
|         `when`(uploadItem.mediaUri).thenReturn(mediaUri) |         `when`(uploadItem.mediaUri).thenReturn(mediaUri) | ||||||
|         fragment.onImageProcessed(uploadItem, place) |         fragment.onImageProcessed(uploadItem) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|  | @ -366,7 +344,10 @@ class UploadMediaDetailFragmentUnitTest { | ||||||
|         `when`(uploadItem.gpsCoords).thenReturn(imageCoordinates) |         `when`(uploadItem.gpsCoords).thenReturn(imageCoordinates) | ||||||
|         val activityResult = ActivityResult(Activity.RESULT_OK, intent) |         val activityResult = ActivityResult(Activity.RESULT_OK, intent) | ||||||
| 
 | 
 | ||||||
|         val handleResultMethod = UploadMediaDetailFragment::class.java.getDeclaredMethod("onCameraPosition", ActivityResult::class.java) |         val handleResultMethod = UploadMediaDetailFragment::class.java.getDeclaredMethod( | ||||||
|  |             "onCameraPosition", | ||||||
|  |             ActivityResult::class.java | ||||||
|  |         ) | ||||||
|         handleResultMethod.isAccessible = true |         handleResultMethod.isAccessible = true | ||||||
| 
 | 
 | ||||||
|         handleResultMethod.invoke(fragment, activityResult) |         handleResultMethod.invoke(fragment, activityResult) | ||||||
|  | @ -382,7 +363,7 @@ class UploadMediaDetailFragmentUnitTest { | ||||||
|         val cameraPosition = Mockito.mock(CameraPosition::class.java) |         val cameraPosition = Mockito.mock(CameraPosition::class.java) | ||||||
|         val latLng = Mockito.mock(LatLng::class.java) |         val latLng = Mockito.mock(LatLng::class.java) | ||||||
| 
 | 
 | ||||||
|         Whitebox.setInternalState(fragment, "callback", callback) |         Whitebox.setInternalState(fragment, "fragmentCallback", callback) | ||||||
|         Whitebox.setInternalState(cameraPosition, "latitude", latLng.latitude) |         Whitebox.setInternalState(cameraPosition, "latitude", latLng.latitude) | ||||||
|         Whitebox.setInternalState(cameraPosition, "longitude", latLng.longitude) |         Whitebox.setInternalState(cameraPosition, "longitude", latLng.longitude) | ||||||
|         Whitebox.setInternalState(fragment, "editableUploadItem", uploadItem) |         Whitebox.setInternalState(fragment, "editableUploadItem", uploadItem) | ||||||
|  | @ -396,7 +377,10 @@ class UploadMediaDetailFragmentUnitTest { | ||||||
| 
 | 
 | ||||||
|         val activityResult = ActivityResult(Activity.RESULT_OK, intent) |         val activityResult = ActivityResult(Activity.RESULT_OK, intent) | ||||||
| 
 | 
 | ||||||
|         val handleResultMethod = UploadMediaDetailFragment::class.java.getDeclaredMethod("onCameraPosition", ActivityResult::class.java) |         val handleResultMethod = UploadMediaDetailFragment::class.java.getDeclaredMethod( | ||||||
|  |             "onCameraPosition", | ||||||
|  |             ActivityResult::class.java | ||||||
|  |         ) | ||||||
|         handleResultMethod.isAccessible = true |         handleResultMethod.isAccessible = true | ||||||
| 
 | 
 | ||||||
|         handleResultMethod.invoke(fragment, activityResult) |         handleResultMethod.invoke(fragment, activityResult) | ||||||
|  | @ -407,7 +391,7 @@ class UploadMediaDetailFragmentUnitTest { | ||||||
|     @Throws(Exception::class) |     @Throws(Exception::class) | ||||||
|     fun testUpdateMediaDetails() { |     fun testUpdateMediaDetails() { | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |         Shadows.shadowOf(Looper.getMainLooper()).idle() | ||||||
|         fragment.updateMediaDetails(null) |         fragment.updateMediaDetails(mock()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|  | @ -417,21 +401,6 @@ class UploadMediaDetailFragmentUnitTest { | ||||||
|         fragment.onDestroyView() |         fragment.onDestroyView() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnLlContainerTitleClicked() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         fragment.onLlContainerTitleClicked() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnIbMapClicked() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         Whitebox.setInternalState(fragment, "presenter", presenter) |  | ||||||
|         fragment.onIbMapClicked() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |     @Test | ||||||
|     @Throws(Exception::class) |     @Throws(Exception::class) | ||||||
|     fun testOnPrimaryCaptionTextChange() { |     fun testOnPrimaryCaptionTextChange() { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Nicolas Raoul
						Nicolas Raoul