Skip to content

Commit 60764f6

Browse files
authored
#3101: Add image upload limit of 20 to custom selector (#5369)
* Add basic image limit warning to custom selector * Block upload button when image limit is exceeded * Complete basic functionality for upload limit: Disabled button, warning sign & toast * Consolidate functionality between upload limit and not for upload marking * Upload Limit: write unit tests and optimize control flow * Upload Limit Tests: improve logic coverage and alter to stay valid if limit is changed * Upload Limit: polish javadocs and add explanation to warning toast. * Upload Limit: refactor variable names * Upload Limit: change warning toast to dialog box, repurposing welcome dialog code & layout * Upload Limit: remove error icon when the mark as not for upload button is pressed
1 parent 3118a83 commit 60764f6

File tree

5 files changed

+216
-51
lines changed

5 files changed

+216
-51
lines changed

app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ import fr.free.nrw.commons.media.ZoomableActivity
2828
import fr.free.nrw.commons.theme.BaseActivity
2929
import fr.free.nrw.commons.upload.FileUtilsWrapper
3030
import fr.free.nrw.commons.utils.CustomSelectorUtils
31-
import kotlinx.android.synthetic.main.activity_login.view.title
3231
import kotlinx.coroutines.*
3332
import java.io.File
33+
import java.lang.Integer.max
3434
import javax.inject.Inject
3535

3636

@@ -67,6 +67,22 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
6767
*/
6868
private lateinit var prefs: SharedPreferences
6969

70+
/**
71+
* Maximum number of images that can be selected.
72+
*/
73+
private val uploadLimit: Int = 20
74+
75+
/**
76+
* Flag that is marked true when the amount
77+
* of selected images is greater than the upload limit.
78+
*/
79+
private var uploadLimitExceeded: Boolean = false
80+
81+
/**
82+
* Tracks the amount by which the upload limit has been exceeded.
83+
*/
84+
private var uploadLimitExceededBy: Int = 0
85+
7086
/**
7187
* View Model Factory.
7288
*/
@@ -201,6 +217,7 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
201217
i++
202218
}
203219
markAsNotForUpload(selectedImages)
220+
toolbarBinding.imageLimitError.visibility = View.INVISIBLE
204221
}
205222

206223
/**
@@ -314,6 +331,10 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
314331
private fun setUpToolbar() {
315332
val back: ImageButton = findViewById(R.id.back)
316333
back.setOnClickListener { onBackPressed() }
334+
335+
val limitError: ImageButton = findViewById(R.id.image_limit_error)
336+
limitError.visibility = View.INVISIBLE
337+
limitError.setOnClickListener { displayUploadLimitWarning() }
317338
}
318339

319340
/**
@@ -342,7 +363,19 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
342363
viewModel.selectedImages.value = selectedImages
343364
changeTitle(bucketName, selectedImages.size)
344365

345-
if (selectedNotForUploadImages > 0) {
366+
uploadLimitExceeded = selectedImages.size > uploadLimit
367+
uploadLimitExceededBy = max(selectedImages.size - uploadLimit,0)
368+
369+
if (uploadLimitExceeded && selectedNotForUploadImages == 0) {
370+
toolbarBinding.imageLimitError.visibility = View.VISIBLE
371+
bottomSheetBinding.upload.text = resources.getString(
372+
R.string.custom_selector_button_limit_text, uploadLimit)
373+
} else {
374+
toolbarBinding.imageLimitError.visibility = View.INVISIBLE
375+
bottomSheetBinding.upload.text = resources.getString(R.string.upload)
376+
}
377+
378+
if (uploadLimitExceeded || selectedNotForUploadImages > 0) {
346379
bottomSheetBinding.upload.isEnabled = false
347380
bottomSheetBinding.upload.alpha = 0.5f
348381
} else {
@@ -390,22 +423,22 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
390423
* Get the selected images. Remove any non existent file, forward the data to finish selector.
391424
*/
392425
fun onDone() {
393-
val selectedImages = viewModel.selectedImages.value
394-
if (selectedImages.isNullOrEmpty()) {
395-
finishPickImages(arrayListOf())
396-
return
397-
}
398-
var i = 0
399-
while (i < selectedImages.size) {
400-
val path = selectedImages[i].path
401-
val file = File(path)
402-
if (!file.exists()) {
403-
selectedImages.removeAt(i)
404-
i--
426+
val selectedImages = viewModel.selectedImages.value
427+
if (selectedImages.isNullOrEmpty()) {
428+
finishPickImages(arrayListOf())
429+
return
405430
}
406-
i++
407-
}
408-
finishPickImages(selectedImages)
431+
var i = 0
432+
while (i < selectedImages.size) {
433+
val path = selectedImages[i].path
434+
val file = File(path)
435+
if (!file.exists()) {
436+
selectedImages.removeAt(i)
437+
i--
438+
}
439+
i++
440+
}
441+
finishPickImages(selectedImages)
409442
}
410443

411444
/**
@@ -432,6 +465,20 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
432465
}
433466
}
434467

468+
/**
469+
* Displays a dialog explaining the upload limit warning.
470+
*/
471+
private fun displayUploadLimitWarning() {
472+
val dialog = Dialog(this)
473+
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
474+
dialog.setContentView(R.layout.custom_selector_limit_dialog)
475+
(dialog.findViewById(R.id.btn_dismiss_limit_warning) as Button).setOnClickListener()
476+
{ dialog.dismiss() }
477+
(dialog.findViewById(R.id.upload_limit_warning) as TextView).text = resources.getString(
478+
R.string.custom_selector_over_limit_warning, uploadLimit, uploadLimitExceededBy)
479+
dialog.show()
480+
}
481+
435482
/**
436483
* On activity destroy
437484
* If image fragment is open, overwrite its attributes otherwise discard the values.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<androidx.constraintlayout.widget.ConstraintLayout
3+
xmlns:android="http://schemas.android.com/apk/res/android"
4+
android:layout_height="wrap_content"
5+
android:layout_width="match_parent"
6+
android:layout_margin="@dimen/dimen_10">
7+
<ScrollView
8+
android:layout_height="match_parent"
9+
android:layout_width="match_parent">
10+
11+
<LinearLayout
12+
android:layout_width="match_parent"
13+
android:layout_height="wrap_content"
14+
android:orientation="vertical" >
15+
16+
<TextView
17+
android:id="@+id/upload_limit_warning"
18+
android:layout_width="wrap_content"
19+
android:layout_height="wrap_content"
20+
android:layout_gravity="center"
21+
android:layout_margin="@dimen/dimen_10"
22+
android:textSize="@dimen/description_text_size"
23+
android:textStyle="bold" />
24+
25+
<Button
26+
android:id="@+id/btn_dismiss_limit_warning"
27+
android:layout_width="match_parent"
28+
android:layout_height="wrap_content"
29+
android:text="@string/custom_selector_dismiss_limit_warning_button_text"
30+
android:textColor="@color/primaryTextColor" />
31+
32+
</LinearLayout>
33+
</ScrollView>
34+
</androidx.constraintlayout.widget.ConstraintLayout>
Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,56 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<merge xmlns:android="http://schemas.android.com/apk/res/android"
2+
<merge xmlns:tools="http://schemas.android.com/tools"
3+
xmlns:android="http://schemas.android.com/apk/res/android"
34
xmlns:app="http://schemas.android.com/apk/res-auto"
45
>
5-
<androidx.constraintlayout.widget.ConstraintLayout
6-
android:id="@+id/toolbar_layout"
7-
android:layout_width="match_parent"
8-
android:layout_height="?attr/actionBarSize"
6+
7+
<androidx.constraintlayout.widget.ConstraintLayout
8+
android:id="@+id/toolbar_layout"
9+
android:layout_width="match_parent"
10+
android:layout_height="?attr/actionBarSize"
11+
android:background="?attr/mainBackground"
12+
app:layout_constraintTop_toTopOf="parent">
13+
14+
<ImageButton
15+
android:id="@+id/back"
16+
android:layout_width="wrap_content"
17+
android:layout_height="match_parent"
18+
android:layout_centerVertical="true"
19+
android:background="?attr/selectableItemBackgroundBorderless"
20+
android:clickable="true"
21+
android:contentDescription="@string/back"
22+
android:focusable="true"
23+
android:padding="@dimen/standard_gap"
24+
app:layout_constraintBottom_toBottomOf="parent"
25+
app:layout_constraintStart_toStartOf="parent"
926
app:layout_constraintTop_toTopOf="parent"
10-
android:background="?attr/mainBackground">
27+
app:srcCompat="?attr/custom_selector_back" />
1128

12-
<ImageButton
13-
android:id="@+id/back"
14-
android:layout_width="wrap_content"
15-
android:layout_height="match_parent"
16-
app:layout_constraintBottom_toBottomOf="parent"
17-
app:layout_constraintStart_toStartOf="parent"
18-
app:layout_constraintTop_toTopOf="parent"
19-
android:layout_centerVertical="true"
20-
android:background="?attr/selectableItemBackgroundBorderless"
21-
android:padding="@dimen/standard_gap"
22-
android:clickable="true"
23-
android:focusable="true"
24-
app:srcCompat="?attr/custom_selector_back"
25-
android:contentDescription="@string/back" />
29+
<TextView
30+
android:id="@+id/title"
31+
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
32+
android:layout_width="0dp"
33+
android:layout_height="29dp"
34+
android:ellipsize="end"
35+
android:singleLine="true"
36+
android:text="@string/custom_selector_title"
37+
android:textAlignment="center"
38+
app:layout_constraintBottom_toBottomOf="parent"
39+
app:layout_constraintEnd_toStartOf="@+id/image_limit_error"
40+
app:layout_constraintStart_toEndOf="@+id/back"
41+
app:layout_constraintTop_toTopOf="parent" />
2642

27-
<TextView
28-
android:id="@+id/title"
29-
android:textAlignment="center"
30-
android:layout_width="0dp"
31-
android:layout_height="wrap_content"
32-
android:layout_marginEnd="@dimen/dimen_20"
33-
app:layout_constraintStart_toEndOf="@id/back"
34-
app:layout_constraintEnd_toEndOf="parent"
35-
app:layout_constraintTop_toTopOf="parent"
36-
app:layout_constraintBottom_toBottomOf="parent"
37-
android:singleLine="true"
38-
android:ellipsize="end"
39-
android:text="@string/custom_selector_title"
40-
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
43+
<ImageButton
44+
android:id="@+id/image_limit_error"
45+
android:layout_width="48dp"
46+
android:layout_height="0dp"
47+
android:background="#00FFFFFF"
48+
android:contentDescription="@string/custom_selector_limit_error_desc"
49+
app:layout_constraintBottom_toBottomOf="parent"
50+
app:layout_constraintEnd_toEndOf="parent"
51+
app:layout_constraintTop_toTopOf="parent"
52+
app:layout_constraintVertical_bias="1.0"
53+
app:srcCompat="@drawable/ic_error_red_24dp" />
4154

4255
</androidx.constraintlayout.widget.ConstraintLayout>
4356
</merge>

app/src/main/res/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,10 @@ Upload your first media by tapping on the add button.</string>
716716
<string name="custom_selector_info_text2">Unlike the picture on the left, the picture on the right has the Commons logo indicating it is already uploaded. \n Touch and hold for image preview.</string>
717717
<string name="welcome_custom_selector_ok">Awesome</string>
718718
<string name="custom_selector_already_uploaded_image_text">This image has already been uploaded to Commons.</string>
719+
<string name="custom_selector_over_limit_warning">For technical reasons, the app can\'t reliably upload more than %1$d pictures at once. The upload limit of %1$d has been exceeded by %2$d.</string>
720+
<string name="custom_selector_dismiss_limit_warning_button_text">Dismiss</string>
721+
<string name="custom_selector_button_limit_text">Max: %1$d</string>
722+
<string name="custom_selector_limit_error_desc">Error: Upload Limit Exceeded</string>
719723
<string name="place_state_wlm">WLM</string>
720724
<string name="wlm_upload_info">This image will be entered into the Wiki Loves Monuments contest</string>
721725
<string name="display_monuments">Display monuments</string>

app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import fr.free.nrw.commons.TestAppAdapter
77
import fr.free.nrw.commons.TestCommonsApplication
88
import fr.free.nrw.commons.customselector.model.Image
99
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter
10+
import kotlinx.android.synthetic.main.bottom_sheet_nearby.bottom_sheet
1011
import org.junit.Before
1112
import org.junit.Test
13+
import org.junit.jupiter.api.Assertions.assertEquals
1214
import org.junit.jupiter.api.Assertions.assertNotNull
1315
import org.junit.runner.RunWith
1416
import org.mockito.Mockito
@@ -18,6 +20,7 @@ import org.robolectric.Robolectric
1820
import org.robolectric.RobolectricTestRunner
1921
import org.robolectric.annotation.Config
2022
import org.wikipedia.AppAdapter
23+
import java.lang.reflect.Field
2124
import java.lang.reflect.Method
2225

2326
/**
@@ -210,4 +213,68 @@ class CustomSelectorActivityTest {
210213
method.isAccessible = true
211214
method.invoke(activity)
212215
}
216+
217+
/**
218+
* Test displayUploadLimitWarning Function.
219+
*/
220+
@Test
221+
@Throws(Exception::class)
222+
fun testDisplayUploadLimitWarning() {
223+
val method: Method = CustomSelectorActivity::class.java.getDeclaredMethod(
224+
"displayUploadLimitWarning"
225+
)
226+
method.isAccessible = true
227+
method.invoke(activity)
228+
}
229+
230+
/**
231+
* Logic tests for the upload limit functionality.
232+
*/
233+
@Test
234+
@Throws(Exception::class)
235+
fun testUploadLimit() {
236+
val overLimit: Field =
237+
CustomSelectorActivity::class.java.getDeclaredField("uploadLimitExceeded")
238+
overLimit.isAccessible = true
239+
val exceededBy: Field =
240+
CustomSelectorActivity::class.java.getDeclaredField("uploadLimitExceededBy")
241+
exceededBy.isAccessible = true
242+
val limit: Field =
243+
CustomSelectorActivity::class.java.getDeclaredField("uploadLimit")
244+
limit.isAccessible = true
245+
246+
// all tests check integration with not for upload marking
247+
248+
// test with list size limit
249+
for (i in 1..limit.getInt(activity)) {
250+
images.add(Image(i.toLong(), i.toString(), uri,
251+
"abc/abc", 1, "bucket1"))
252+
}
253+
activity.onFolderClick(1, "test", 0)
254+
activity.onSelectedImagesChanged(images,0)
255+
assertEquals(false, overLimit.getBoolean(activity))
256+
assertEquals(0, exceededBy.getInt(activity))
257+
activity.onSelectedImagesChanged(images, 1)
258+
assertEquals(false, overLimit.getBoolean(activity))
259+
assertEquals(0, exceededBy.getInt(activity))
260+
261+
// test with list size limit+1
262+
images.add(image)
263+
activity.onSelectedImagesChanged(images,0)
264+
assertEquals(true, overLimit.getBoolean(activity))
265+
assertEquals(1, exceededBy.getInt(activity))
266+
activity.onSelectedImagesChanged(images,1)
267+
assertEquals(true, overLimit.getBoolean(activity))
268+
assertEquals(1, exceededBy.getInt(activity))
269+
270+
//test with list size 1
271+
images.clear()
272+
images.add(image)
273+
activity.onSelectedImagesChanged(images,0)
274+
assertEquals(false, overLimit.getBoolean(activity))
275+
assertEquals(0, exceededBy.getInt(activity))
276+
activity.onSelectedImagesChanged(images,1)
277+
assertEquals(false, overLimit.getBoolean(activity))
278+
assertEquals(0, exceededBy.getInt(activity))
279+
}
213280
}

0 commit comments

Comments
 (0)