Skip to content

Commit 11fd7e4

Browse files
authored
Add long press context menu for images with option to download image (#179)
* Add long press context menu for images with option to download image * Add extra log statements * Remove timber-style logging instances from project * Add external storage permission so images and PDFs downloaded externally
1 parent c480449 commit 11fd7e4

File tree

11 files changed

+347
-33
lines changed

11 files changed

+347
-33
lines changed

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import android.arch.lifecycle.Observer
2323
import android.arch.persistence.room.Room
2424
import android.net.Uri
2525
import android.support.test.InstrumentationRegistry
26+
import android.view.MenuItem
2627
import android.view.View
2728
import com.duckduckgo.app.autocomplete.api.AutoCompleteApi
2829
import com.duckduckgo.app.bookmarks.db.BookmarkEntity
@@ -51,7 +52,7 @@ import org.junit.Before
5152
import org.junit.Rule
5253
import org.junit.Test
5354
import org.mockito.*
54-
import org.mockito.ArgumentCaptor.forClass
55+
import org.mockito.ArgumentMatchers.anyString
5556
import org.mockito.Mockito.never
5657
import org.mockito.Mockito.verify
5758

@@ -91,11 +92,18 @@ class BrowserViewModelTest {
9192
@Mock
9293
private lateinit var bookmarksDao: BookmarksDao
9394

95+
@Mock
96+
private lateinit var mockLongPressHandler: LongPressHandler
97+
98+
@Mock
99+
private lateinit var mockOmnibarConverter: OmnibarEntryConverter
100+
101+
@Captor
102+
private lateinit var commandCaptor: ArgumentCaptor<Command>
103+
94104
private lateinit var db: AppDatabase
95105
private lateinit var appConfigurationDao: AppConfigurationDao
96106

97-
private val mockOmnibarConverter: OmnibarEntryConverter = mock()
98-
99107
private lateinit var testee: BrowserViewModel
100108

101109
@Before
@@ -117,6 +125,7 @@ class BrowserViewModelTest {
117125
autoCompleteApi = mockAutoCompleteApi,
118126
appSettingsPreferencesStore = mockSettingsStore,
119127
bookmarksDao = bookmarksDao,
128+
longPressHandler = mockLongPressHandler,
120129
appConfigurationDao = appConfigurationDao)
121130

122131
testee.url.observeForever(mockQueryObserver)
@@ -241,10 +250,9 @@ class BrowserViewModelTest {
241250
@Test
242251
fun whenSharedTextReceivedThenNavigationTriggered() {
243252
testee.onSharedTextReceived("http://example.com")
244-
val captor: ArgumentCaptor<Command> = forClass(Command::class.java)
245-
verify(mockCommandObserver, times(2)).onChanged(captor.capture())
246-
assertNotNull(captor.value)
247-
assertTrue(captor.value is Navigate)
253+
verify(mockCommandObserver, times(2)).onChanged(commandCaptor.capture())
254+
assertNotNull(commandCaptor.value)
255+
assertTrue(commandCaptor.value is Navigate)
248256
}
249257

250258
@Test
@@ -395,10 +403,9 @@ class BrowserViewModelTest {
395403

396404
@Test
397405
fun whenEnteringNonEmptyQueryThenHideKeyboardCommandIssued() {
398-
val captor = ArgumentCaptor.forClass(BrowserViewModel.Command::class.java)
399406
testee.onUserSubmittedQuery("foo")
400-
verify(mockCommandObserver, Mockito.atLeastOnce()).onChanged(captor.capture())
401-
assertTrue(captor.value == Command.HideKeyboard)
407+
verify(mockCommandObserver, Mockito.atLeastOnce()).onChanged(commandCaptor.capture())
408+
assertTrue(commandCaptor.value == Command.HideKeyboard)
402409
}
403410

404411
@Test
@@ -410,11 +417,10 @@ class BrowserViewModelTest {
410417

411418
@Test
412419
fun whenNotifiedEnteringFullScreenThenEnterFullScreenCommandIssued() {
413-
val captor = ArgumentCaptor.forClass(BrowserViewModel.Command::class.java)
414420
val stubView = View(InstrumentationRegistry.getTargetContext())
415421
testee.goFullScreen(stubView)
416-
verify(mockCommandObserver, Mockito.atLeastOnce()).onChanged(captor.capture())
417-
assertTrue(captor.lastValue is Command.ShowFullScreen)
422+
verify(mockCommandObserver, Mockito.atLeastOnce()).onChanged(commandCaptor.capture())
423+
assertTrue(commandCaptor.lastValue is Command.ShowFullScreen)
418424
}
419425

420426
@Test
@@ -427,4 +433,18 @@ class BrowserViewModelTest {
427433
fun whenViewModelInitialisedThenFullScreenFlagIsDisabled() {
428434
assertFalse(testee.viewState.value!!.isFullScreen)
429435
}
436+
437+
@Test
438+
fun whenUserSelectsDownloadImageOptionFromContextMenuThenDownloadFileCommandIssued() {
439+
whenever(mockLongPressHandler.userSelectedMenuItem(anyString(), any()))
440+
.thenReturn(LongPressHandler.RequiredAction.DownloadFile("example.com"))
441+
442+
val mockMenuItem : MenuItem = mock()
443+
testee.userSelectedItemFromLongPressMenu("example.com", mockMenuItem)
444+
verify(mockCommandObserver, Mockito.atLeastOnce()).onChanged(commandCaptor.capture())
445+
assertTrue(commandCaptor.lastValue is Command.DownloadImage)
446+
447+
val lastCommand = commandCaptor.lastValue as Command.DownloadImage
448+
assertEquals("example.com", lastCommand.url)
449+
}
430450
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright (c) 2018 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser
18+
19+
import android.view.ContextMenu
20+
import android.view.MenuItem
21+
import android.webkit.WebView.HitTestResult
22+
import com.nhaarman.mockito_kotlin.eq
23+
import com.nhaarman.mockito_kotlin.never
24+
import com.nhaarman.mockito_kotlin.verify
25+
import com.nhaarman.mockito_kotlin.whenever
26+
import org.junit.Assert.assertEquals
27+
import org.junit.Assert.assertTrue
28+
import org.junit.Before
29+
import org.junit.Test
30+
import org.mockito.ArgumentMatchers.anyInt
31+
import org.mockito.ArgumentMatchers.anyString
32+
import org.mockito.Mock
33+
import org.mockito.MockitoAnnotations
34+
35+
class WebViewLongPressHandlerTest {
36+
37+
private lateinit var testee: WebViewLongPressHandler
38+
39+
@Mock
40+
private lateinit var mockMenu: ContextMenu
41+
42+
@Mock
43+
private lateinit var mockMenuItem: MenuItem
44+
45+
@Before
46+
fun setup() {
47+
MockitoAnnotations.initMocks(this)
48+
testee = WebViewLongPressHandler()
49+
}
50+
51+
@Test
52+
fun whenLongPressedWithImageTypeThenImageOptionsHeaderAddedToMenu() {
53+
testee.handleLongPress(HitTestResult.IMAGE_TYPE, mockMenu)
54+
verify(mockMenu).setHeaderTitle(R.string.imageOptions)
55+
}
56+
57+
@Test
58+
fun whenLongPressedWithAnchorImageTypeThenImageOptionsHeaderAddedToMenu() {
59+
testee.handleLongPress(HitTestResult.SRC_IMAGE_ANCHOR_TYPE, mockMenu)
60+
verify(mockMenu).setHeaderTitle(R.string.imageOptions)
61+
}
62+
63+
@Test
64+
fun whenLongPressedWithImageTypeThenDownloadImageMenuAdded() {
65+
testee.handleLongPress(HitTestResult.IMAGE_TYPE, mockMenu)
66+
verify(mockMenu).add(anyInt(), eq(WebViewLongPressHandler.CONTEXT_MENU_ID_DOWNLOAD_IMAGE), anyInt(), eq(R.string.downloadImage))
67+
}
68+
69+
@Test
70+
fun whenLongPressedWithAnchorImageTypeThenDownloadImageMenuAdded() {
71+
testee.handleLongPress(HitTestResult.SRC_IMAGE_ANCHOR_TYPE, mockMenu)
72+
verify(mockMenu).add(anyInt(), eq(WebViewLongPressHandler.CONTEXT_MENU_ID_DOWNLOAD_IMAGE), anyInt(), eq(R.string.downloadImage))
73+
}
74+
75+
@Test
76+
fun whenLongPressedWithOtherImageTypeThenMenuNotAltered() {
77+
testee.handleLongPress(HitTestResult.UNKNOWN_TYPE, mockMenu)
78+
verify(mockMenu, never()).setHeaderTitle(anyString())
79+
verify(mockMenu, never()).setHeaderTitle(anyInt())
80+
verify(mockMenu, never()).add(anyInt())
81+
verify(mockMenu, never()).add(anyString())
82+
verify(mockMenu, never()).add(anyInt(), anyInt(), anyInt(), anyInt())
83+
verify(mockMenu, never()).add(anyInt(), anyInt(), anyInt(), anyString())
84+
}
85+
86+
@Test
87+
fun whenUserSelectedDownloadImageOptionThenActionIsDownloadFileActionRequired() {
88+
whenever(mockMenuItem.itemId).thenReturn(WebViewLongPressHandler.CONTEXT_MENU_ID_DOWNLOAD_IMAGE)
89+
val action = testee.userSelectedMenuItem("example.com", mockMenuItem)
90+
assertTrue(action is LongPressHandler.RequiredAction.DownloadFile)
91+
}
92+
93+
@Test
94+
fun whenUserSelectedDownloadImageOptionThenDownloadFileWithCorrectUrlReturned() {
95+
whenever(mockMenuItem.itemId).thenReturn(WebViewLongPressHandler.CONTEXT_MENU_ID_DOWNLOAD_IMAGE)
96+
val action = testee.userSelectedMenuItem("example.com", mockMenuItem) as LongPressHandler.RequiredAction.DownloadFile
97+
assertEquals("example.com", action.url)
98+
}
99+
100+
@Test
101+
fun whenUserSelectedUnknownOptionThenNoActionRequiredReturned() {
102+
val unknownMenuId = 123
103+
whenever(mockMenuItem.itemId).thenReturn(unknownMenuId)
104+
val action = testee.userSelectedMenuItem("example.com", mockMenuItem)
105+
assertTrue(action == LongPressHandler.RequiredAction.None)
106+
}
107+
}

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
<uses-permission android:name="android.permission.INTERNET" />
77
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
8+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
89

910
<application
1011
android:name="com.duckduckgo.app.global.DuckDuckGoApplication"

0 commit comments

Comments
 (0)