diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index f9d73c482c77..39ae8d726163 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -1754,6 +1754,13 @@ class BrowserTabViewModelTest { assertFalse(browserViewState().canFireproofSite) } + @Test + fun whenFireproofWebsiteAddedThenPixelSent() { + loadUrl("http://example.com/", isBrowserShowing = true) + testee.onFireproofWebsiteClicked() + verify(mockPixel).fire(Pixel.PixelName.FIREPROOF_WEBSITE_ADDED) + } + @Test fun whenUserClicksOnFireproofWebsiteSnackbarUndoActionThenFireproofWebsiteIsRemoved() { loadUrl("http://example.com/", isBrowserShowing = true) @@ -1764,6 +1771,16 @@ class BrowserTabViewModelTest { assertTrue(browserViewState().canFireproofSite) } + @Test + fun whenUserClicksOnFireproofWebsiteSnackbarUndoActionThenPixelSent() { + loadUrl("http://example.com/", isBrowserShowing = true) + testee.onFireproofWebsiteClicked() + assertCommandIssued { + testee.onFireproofWebsiteSnackbarUndoClicked(this.fireproofWebsiteEntity) + } + verify(mockPixel).fire(Pixel.PixelName.FIREPROOF_WEBSITE_UNDO) + } + private inline fun assertCommandIssued(instanceAssertions: T.() -> Unit = {}) { verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) val issuedCommand = commandCaptor.allValues.find { it is T } diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModelTest.kt index 5b97b2c2c2bb..13cb15068311 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModelTest.kt @@ -26,6 +26,7 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.ui.FireproofWebsitesViewModel.Command.ConfirmDeleteFireproofWebsite import com.duckduckgo.app.global.db.AppDatabase +import com.duckduckgo.app.statistics.pixels.Pixel import com.nhaarman.mockitokotlin2.atLeastOnce import com.nhaarman.mockitokotlin2.mock import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -63,13 +64,15 @@ class FireproofWebsitesViewModelTest { private val mockViewStateObserver: Observer = mock() + private val mockPixel: Pixel = mock() + @Before fun before() { db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java) .allowMainThreadQueries() .build() fireproofWebsiteDao = db.fireproofWebsiteDao() - viewModel = FireproofWebsitesViewModel(fireproofWebsiteDao, coroutineRule.testDispatcherProvider) + viewModel = FireproofWebsitesViewModel(fireproofWebsiteDao, coroutineRule.testDispatcherProvider, mockPixel) viewModel.command.observeForever(mockCommandObserver) viewModel.viewState.observeForever(mockViewStateObserver) } @@ -101,6 +104,15 @@ class FireproofWebsitesViewModelTest { assertTrue(viewStateCaptor.value.fireproofWebsitesEntities.isEmpty()) } + @Test + fun whenUserConfirmsToDeleteThenPixelSent() { + givenFireproofWebsiteDomain("domain.com") + + viewModel.delete(FireproofWebsiteEntity("domain.com")) + + verify(mockPixel).fire(Pixel.PixelName.FIREPROOF_WEBSITE_DELETED) + } + @Test fun whenViewModelInitialisedThenViewStateShowsCurrentFireproofWebsites() { givenFireproofWebsiteDomain("domain.com") diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 975daa475d3b..0016e472031c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -722,16 +722,16 @@ class BrowserTabViewModel( fireproofWebsiteDao.insert(fireproofWebsiteEntity) } if (id >= 0) { + pixel.fire(PixelName.FIREPROOF_WEBSITE_ADDED) command.value = ShowFireproofWebSiteConfirmation(fireproofWebsiteEntity = fireproofWebsiteEntity) } } } fun onFireproofWebsiteSnackbarUndoClicked(fireproofWebsiteEntity: FireproofWebsiteEntity) { - viewModelScope.launch { - withContext(dispatchers.io()) { - fireproofWebsiteDao.delete(fireproofWebsiteEntity) - } + viewModelScope.launch(dispatchers.io()) { + fireproofWebsiteDao.delete(fireproofWebsiteEntity) + pixel.fire(PixelName.FIREPROOF_WEBSITE_UNDO) } } diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt index 97f5b5886a7b..42f83ba264cf 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsiteAdapter.kt @@ -22,26 +22,26 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.PopupMenu -import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView import com.duckduckgo.app.browser.R import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.website import com.duckduckgo.app.global.faviconLocation import com.duckduckgo.app.global.image.GlideApp -import kotlinx.android.synthetic.main.view_fireproof_website_description.view.* import kotlinx.android.synthetic.main.view_fireproof_website_entry.view.* import timber.log.Timber -import java.lang.IllegalArgumentException class FireproofWebsiteAdapter( - private val viewModel: FireproofWebsitesViewModel, - @StringRes private val listDescriptionStringRes: Int + private val viewModel: FireproofWebsitesViewModel ) : RecyclerView.Adapter() { - companion object Type { + companion object { const val FIREPROOF_WEBSITE_TYPE = 0 const val DESCRIPTION_TYPE = 1 + const val EMPTY_STATE_TYPE = 2 + + const val DESCRIPTION_ITEM_SIZE = 1 + const val EMPTY_HINT_ITEM_SIZE = 1 } var fireproofWebsites: List = emptyList() @@ -57,6 +57,10 @@ class FireproofWebsiteAdapter( val view = inflater.inflate(R.layout.view_fireproof_website_entry, parent, false) FireproofWebSiteViewHolder.FireproofWebsiteItemViewHolder(view, viewModel) } + EMPTY_STATE_TYPE -> { + val view = inflater.inflate(R.layout.view_fireproof_website_empty_hint, parent, false) + FireproofWebSiteViewHolder.FireproofWebsiteEmptyHintViewHolder(view) + } DESCRIPTION_TYPE -> { val view = inflater.inflate(R.layout.view_fireproof_website_description, parent, false) FireproofWebSiteViewHolder.FireproofWebsiteDescriptionViewHolder(view) @@ -66,32 +70,45 @@ class FireproofWebsiteAdapter( } override fun getItemViewType(position: Int): Int { - return if ((fireproofWebsites.size - 1) < position) { + return if (position == 0) { DESCRIPTION_TYPE } else { - FIREPROOF_WEBSITE_TYPE + getListItemType() } } override fun onBindViewHolder(holder: FireproofWebSiteViewHolder, position: Int) { when (holder) { - is FireproofWebSiteViewHolder.FireproofWebsiteDescriptionViewHolder -> holder.bind(listDescriptionStringRes) - is FireproofWebSiteViewHolder.FireproofWebsiteItemViewHolder -> holder.bind(fireproofWebsites[position]) + is FireproofWebSiteViewHolder.FireproofWebsiteItemViewHolder -> holder.bind(fireproofWebsites[getWebsiteItemPosition(position)]) } } override fun getItemCount(): Int { - return fireproofWebsites.size + 1 + return getItemsSize() + DESCRIPTION_ITEM_SIZE } -} -sealed class FireproofWebSiteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private fun getItemsSize() = if (fireproofWebsites.isEmpty()) { + EMPTY_HINT_ITEM_SIZE + } else { + fireproofWebsites.size + } + + private fun getWebsiteItemPosition(position: Int) = position - DESCRIPTION_ITEM_SIZE - class FireproofWebsiteDescriptionViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) { - fun bind(@StringRes text: Int) = with(itemView) { - fireproofWebsiteDescription.setText(text) + private fun getListItemType(): Int { + return if (fireproofWebsites.isEmpty()) { + EMPTY_STATE_TYPE + } else { + FIREPROOF_WEBSITE_TYPE } } +} + +sealed class FireproofWebSiteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + class FireproofWebsiteDescriptionViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) + + class FireproofWebsiteEmptyHintViewHolder(itemView: View) : FireproofWebSiteViewHolder(itemView) class FireproofWebsiteItemViewHolder(itemView: View, private val viewModel: FireproofWebsitesViewModel) : FireproofWebSiteViewHolder(itemView) { diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesActivity.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesActivity.kt index 5f9b4df37f4a..3c7b1ead0ac4 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesActivity.kt @@ -45,7 +45,7 @@ class FireproofWebsitesActivity : DuckDuckGoActivity() { } private fun setupFireproofWebsiteRecycler() { - adapter = FireproofWebsiteAdapter(viewModel, R.string.fireproofWebsiteFeatureDescription) + adapter = FireproofWebsiteAdapter(viewModel) recycler.adapter = adapter } diff --git a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt index 661f98033be3..21d31bada978 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/fireproofwebsite/ui/FireproofWebsitesViewModel.kt @@ -22,11 +22,14 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.ui.FireproofWebsitesViewModel.Command.ConfirmDeleteFireproofWebsite import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.SingleLiveEvent +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.FIREPROOF_WEBSITE_DELETED import kotlinx.coroutines.launch class FireproofWebsitesViewModel( private val dao: FireproofWebsiteDao, - private val dispatcherProvider: DispatcherProvider + private val dispatcherProvider: DispatcherProvider, + private val pixel: Pixel ) : ViewModel() { data class ViewState( @@ -66,6 +69,7 @@ class FireproofWebsitesViewModel( fun delete(entity: FireproofWebsiteEntity) { viewModelScope.launch(dispatcherProvider.io()) { dao.delete(entity) + pixel.fire(FIREPROOF_WEBSITE_DELETED) } } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index a2c578f742d9..c619547fe661 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -210,6 +210,7 @@ class ViewModelFactory @Inject constructor( private fun fireproofWebsiteViewModel() = FireproofWebsitesViewModel( dao = fireproofWebsiteDao, - dispatcherProvider = dispatcherProvider + dispatcherProvider = dispatcherProvider, + pixel = pixel ) } diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index e6b0c5e1428d..97e77d5e155d 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -175,9 +175,11 @@ interface Pixel { COOKIE_DATABASE_OPEN_ERROR("m_cdb_oe"), COOKIE_DATABASE_DELETE_ERROR("m_cdb_de"), COOKIE_DATABASE_CORRUPTED_ERROR("m_cdb_ce"), - COOKIE_DATABASE_EXCEPTION_OPEN_ERROR("m_cdb_e_oe"), - COOKIE_DATABASE_EXCEPTION_DELETE_ERROR("m_cdb_e_de") + COOKIE_DATABASE_EXCEPTION_DELETE_ERROR("m_cdb_e_de"), + FIREPROOF_WEBSITE_ADDED("m_fw_a"), + FIREPROOF_WEBSITE_DELETED("m_fw_d"), + FIREPROOF_WEBSITE_UNDO("m_fw_u") } object PixelParameter { diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt index fa1241e2ebba..025dc8ec97fc 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt @@ -85,7 +85,7 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine setContentView(R.layout.activity_tab_switcher) extractIntentExtras() configureViewReferences() - setupToolbar(toolbar) + setupToolbar(toolbar) configureRecycler() configureObservers() } diff --git a/app/src/main/res/layout/view_fireproof_website_description.xml b/app/src/main/res/layout/view_fireproof_website_description.xml index d5a923045d23..d71b535a5153 100644 --- a/app/src/main/res/layout/view_fireproof_website_description.xml +++ b/app/src/main/res/layout/view_fireproof_website_description.xml @@ -29,5 +29,7 @@ android:textColor="?attr/settingsMinorTextColor" android:textSize="14sp" android:textStyle="normal" + android:justificationMode="inter_word" + android:text="@string/fireproofWebsiteFeatureDescription" tools:text="Lorem ipsum dolor sit amet" /> \ No newline at end of file diff --git a/app/src/main/res/layout/view_fireproof_website_empty_hint.xml b/app/src/main/res/layout/view_fireproof_website_empty_hint.xml new file mode 100644 index 000000000000..82c3d0bdde5c --- /dev/null +++ b/app/src/main/res/layout/view_fireproof_website_empty_hint.xml @@ -0,0 +1,38 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 445b1d1c85a3..7c5debe22455 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -39,6 +39,7 @@ <b>%s</b> is now fireproof! Visit Settings to learn more. Undo Are you sure you want to delete <b>%s</b>? + No websites fireproofed yet Websites rely on cookies to keep you signed in. When you Fireproof a site, cookies won\'t be erased and you\'ll stay signed in, even after using the Fire Button. More options for fireproof website %s Confirm