From ed3303f6b09d2edd8913b9fcb40a7c33b85b809d Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Mon, 15 Jan 2018 10:39:05 +0000 Subject: [PATCH 01/16] Make browser singleTask to fix up behavior --- app/src/main/AndroidManifest.xml | 7 ++++++- .../app/privacymonitor/ui/PrivacyDashboardActivity.kt | 10 ---------- app/src/main/res/menu/menu_browser_activity.xml | 2 +- app/src/main/res/values/strings.xml | 1 + 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3092911ba647..cb46717e210a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,8 +53,8 @@ @@ -63,26 +63,31 @@ android:label="@string/privacyDashboardActivityTitle" android:parentActivityName=".BrowserActivity" android:screenOrientation="fullSensor" /> + + + + + { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - override fun onBackPressed() { if (viewModel.shouldReloadPage) { setResult(RESULT_RELOAD) diff --git a/app/src/main/res/menu/menu_browser_activity.xml b/app/src/main/res/menu/menu_browser_activity.xml index e5c346e235c8..99829c904476 100644 --- a/app/src/main/res/menu/menu_browser_activity.xml +++ b/app/src/main/res/menu/menu_browser_activity.xml @@ -27,7 +27,7 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7263d1f0998..da2792793dbc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -86,6 +86,7 @@ We\'re still collecting data to show how many trackers we\'ve blocked. + Clear Data Clear All Data Cancel Data cleared From 111ec0111252867a0cf4b9d06dbb2d73f1118dd3 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Mon, 15 Jan 2018 16:52:47 +0000 Subject: [PATCH 02/16] bookmarks (#110) --- app/build.gradle | 5 + .../6.json | 156 +++++++++++++++++ .../app/bookmarks/db/BookmarksDaoTest.kt | 69 ++++++++ .../bookmarks/ui/BookmarksViewModelTest.kt | 93 ++++++++++ .../app/browser/BrowserViewModelTest.kt | 46 ++++- .../db/HttpsUpgradePerformanceTest.kt | 1 - app/src/main/AndroidManifest.xml | 6 + .../app/about/AboutDuckDuckGoActivity.kt | 2 +- .../app/bookmarks/db/BookmarkEntity.kt | 23 +++ .../app/bookmarks/db/BookmarksDao.kt | 37 ++++ .../app/bookmarks/ui/BookmarksActivity.kt | 159 ++++++++++++++++++ .../app/bookmarks/ui/BookmarksViewModel.kt | 74 ++++++++ .../duckduckgo/app/browser/BrowserActivity.kt | 71 +++++--- .../app/browser/BrowserViewModel.kt | 48 ++++-- .../duckduckgo/app/di/AndroidBindingModule.kt | 4 + .../com/duckduckgo/app/di/DatabaseModule.kt | 6 + .../duckduckgo/app/global/ViewModelFactory.kt | 12 +- .../duckduckgo/app/global/db/AppDatabase.kt | 8 +- .../app/global/view/ActivityExtension.kt | 4 +- .../com/duckduckgo/app/home/HomeActivity.kt | 24 ++- .../ui/NetworkTrackerPillView.kt | 7 +- .../ui/PrivacyDashboardActivity.kt | 41 ++--- .../ui/PrivacyPracticesActivity.kt | 23 +-- .../privacymonitor/ui/ScorecardActivity.kt | 3 +- .../ui/TrackerNetworksActivity.kt | 3 +- .../app/settings/SettingsActivity.kt | 2 +- app/src/main/res/drawable-hdpi/ic_remove.png | Bin 0 -> 714 bytes app/src/main/res/drawable-mdpi/ic_remove.png | Bin 0 -> 478 bytes app/src/main/res/drawable-xhdpi/ic_remove.png | Bin 0 -> 1049 bytes .../main/res/drawable-xxhdpi/ic_remove.png | Bin 0 -> 1677 bytes .../main/res/drawable-xxxhdpi/ic_remove.png | Bin 0 -> 2364 bytes .../layout/activity_about_duck_duck_go.xml | 15 +- .../main/res/layout/activity_bookmarks.xml | 29 ++++ .../res/layout/activity_privacy_dashboard.xml | 15 +- .../res/layout/activity_privacy_practices.xml | 15 +- .../res/layout/activity_privacy_scorecard.xml | 15 +- app/src/main/res/layout/activity_settings.xml | 15 +- .../res/layout/activity_tracker_networks.xml | 15 +- app/src/main/res/layout/content_bookmarks.xml | 23 +++ app/src/main/res/layout/include_toolbar.xml | 37 ++++ .../main/res/layout/view_bookmark_entry.xml | 54 ++++++ .../main/res/menu/menu_browser_activity.xml | 10 ++ app/src/main/res/menu/menu_home_activity.xml | 5 + app/src/main/res/values/strings.xml | 9 + 44 files changed, 1016 insertions(+), 168 deletions(-) create mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/6.json create mode 100644 app/src/androidTest/java/com/duckduckgo/app/bookmarks/db/BookmarksDaoTest.kt create mode 100644 app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt create mode 100644 app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarkEntity.kt create mode 100644 app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt create mode 100644 app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt create mode 100644 app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt create mode 100755 app/src/main/res/drawable-hdpi/ic_remove.png create mode 100755 app/src/main/res/drawable-mdpi/ic_remove.png create mode 100755 app/src/main/res/drawable-xhdpi/ic_remove.png create mode 100755 app/src/main/res/drawable-xxhdpi/ic_remove.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_remove.png create mode 100644 app/src/main/res/layout/activity_bookmarks.xml create mode 100644 app/src/main/res/layout/content_bookmarks.xml create mode 100644 app/src/main/res/layout/include_toolbar.xml create mode 100644 app/src/main/res/layout/view_bookmark_entry.xml diff --git a/app/build.gradle b/app/build.gradle index d7b82a634a38..ed95c6ca89e8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -75,6 +75,7 @@ ext { architectureComponents = "1.0.0" dagger = "2.14.1" retrofit = "2.3.0" + ankoVersion = "0.10.4" } @@ -95,6 +96,10 @@ dependencies { implementation "com.google.dagger:dagger-android-support:$dagger" releaseImplementation 'com.faendir:acra:4.10.0' + // Anko + compile "org.jetbrains.anko:anko-commons:$ankoVersion" + compile "org.jetbrains.anko:anko-design:$ankoVersion" + // ViewModel and LiveData implementation "android.arch.lifecycle:extensions:$architectureComponents" kapt "android.arch.lifecycle:compiler:$architectureComponents" diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/6.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/6.json new file mode 100644 index 000000000000..888cf1a4b60b --- /dev/null +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/6.json @@ -0,0 +1,156 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "cf29693b9b6c4b791ee9c6cc434e0ee6", + "entities": [ + { + "tableName": "https_upgrade_domain", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "disconnect_tracker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `category` TEXT NOT NULL, `networkName` TEXT NOT NULL, `networkUrl` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "networkName", + "columnName": "networkName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "networkUrl", + "columnName": "networkUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "network_leaderboard", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkName` TEXT NOT NULL, `domainVisited` TEXT NOT NULL, PRIMARY KEY(`networkName`, `domainVisited`))", + "fields": [ + { + "fieldPath": "networkName", + "columnName": "networkName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "domainVisited", + "columnName": "domainVisited", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "networkName", + "domainVisited" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app_configuration", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `appConfigurationDownloaded` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appConfigurationDownloaded", + "columnName": "appConfigurationDownloaded", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"cf29693b9b6c4b791ee9c6cc434e0ee6\")" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/db/BookmarksDaoTest.kt b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/db/BookmarksDaoTest.kt new file mode 100644 index 000000000000..b96a0457ff6a --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/db/BookmarksDaoTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.bookmarks.db + +import android.arch.persistence.room.Room +import android.support.test.InstrumentationRegistry +import com.duckduckgo.app.blockingObserve +import com.duckduckgo.app.global.db.AppDatabase +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class BookmarksDaoTest { + + private lateinit var db: AppDatabase + private lateinit var dao: BookmarksDao + + @Before + fun before() { + db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), AppDatabase::class.java) + .build() + dao = db.bookmarksDao() + } + + @After + fun after() { + db.close() + } + + @Test + fun whenBookmarkDeleteThenItIsNoLongerInTheList() { + val bookmark = BookmarkEntity(id = 1, title = "title", url = "www.example.com") + dao.insert(bookmark) + dao.delete(bookmark) + val list = dao.bookmarks().blockingObserve() + assertTrue(list!!.isEmpty()) + } + + @Test + fun whenBookmarkAddedThenItIsInList() { + val bookmark = BookmarkEntity(id = 1, title = "title", url = "www.example.com") + dao.insert(bookmark) + val list = dao.bookmarks().blockingObserve() + assertEquals(listOf(bookmark), list) + } + + @Test + fun whenInInitialStateThenTheBookmarksAreEmpty() { + val list = dao.bookmarks().blockingObserve() + assertNotNull(list) + assertTrue(list!!.isEmpty()) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt new file mode 100644 index 000000000000..4536a20689ef --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.bookmarks.ui + +import android.arch.core.executor.testing.InstantTaskExecutorRule +import android.arch.lifecycle.MutableLiveData +import android.arch.lifecycle.Observer +import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentCaptor + +class BookmarksViewModelTest { + + @get:Rule + @Suppress("unused") + var instantTaskExecutorRule = InstantTaskExecutorRule() + + private val liveData = MutableLiveData>() + private val viewStateObserver: Observer = mock() + private val commandObserver: Observer = mock() + private val bookmarksDao: BookmarksDao = mock() + + private val bookmark = BookmarkEntity(title = "title", url = "www.example.com") + + private val testee: BookmarksViewModel by lazy { + val model = BookmarksViewModel(bookmarksDao) + model.viewState.observeForever(viewStateObserver) + model.command.observeForever(commandObserver) + model + } + + @Before + fun before() { + liveData.value = emptyList() + whenever(bookmarksDao.bookmarks()).thenReturn(liveData) + } + + @Test + fun whenBookmarkDeletedThenDaoUpdated() { + testee.delete(bookmark) + verify(bookmarksDao).delete(bookmark) + } + + @Test + fun whenBookmarkSelectedThenOpenCommand() { + testee.onSelected(bookmark) + val captor: ArgumentCaptor = ArgumentCaptor.forClass(BookmarksViewModel.Command::class.java) + verify(commandObserver).onChanged(captor.capture()) + assertNotNull(captor.value) + assertTrue(captor.value is BookmarksViewModel.Command.OpenBookmark) + } + + @Test + fun whenDeleteRequestedThenConfirmCommand() { + testee.onDeleteRequested(bookmark) + val captor: ArgumentCaptor = ArgumentCaptor.forClass(BookmarksViewModel.Command::class.java) + verify(commandObserver).onChanged(captor.capture()) + assertNotNull(captor.value) + assertTrue(captor.value is BookmarksViewModel.Command.ConfirmDeleteBookmark) + } + + @Test + fun whenBookmarksChangedThenObserverNotified() { + testee + val captor: ArgumentCaptor = ArgumentCaptor.forClass(BookmarksViewModel.ViewState::class.java) + verify(viewStateObserver).onChanged(captor.capture()) + assertNotNull(captor.value) + assertNotNull(captor.value.bookmarks) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index 013e3365aa25..9e33eb3f7a65 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -23,6 +23,9 @@ import android.arch.lifecycle.Observer import android.arch.persistence.room.Room import android.net.Uri import android.support.test.InstrumentationRegistry +import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.bookmarks.ui.BookmarksActivity import com.duckduckgo.app.browser.BrowserViewModel.Command import com.duckduckgo.app.browser.BrowserViewModel.Command.LandingPage import com.duckduckgo.app.browser.BrowserViewModel.Command.Navigate @@ -57,13 +60,13 @@ class BrowserViewModelTest { @Suppress("unused") var instantTaskExecutorRule = InstantTaskExecutorRule() - private var lastEntry: NetworkLeaderboardEntry? = null + private var lastNetworkLeaderboardEntry: NetworkLeaderboardEntry? = null private val testStringResolver: StringResolver = object : StringResolver {} private val testNetworkLeaderboardDao: NetworkLeaderboardDao = object : NetworkLeaderboardDao { override fun insert(leaderboardEntry: NetworkLeaderboardEntry) { - lastEntry = leaderboardEntry + lastNetworkLeaderboardEntry = leaderboardEntry } override fun networkPercents(): LiveData> { @@ -76,6 +79,7 @@ class BrowserViewModelTest { private lateinit var mockStringResolver: StringResolver private lateinit var db: AppDatabase private lateinit var appConfigurationDao: AppConfigurationDao + private lateinit var bookmarksDao: BookmarksDao private val testOmnibarConverter: OmnibarEntryConverter = object : OmnibarEntryConverter { override fun convertUri(input: String): String = "duckduckgo.com" @@ -96,6 +100,7 @@ class BrowserViewModelTest { queryObserver = mock() navigationObserver = mock() termsOfServiceStore = mock() + bookmarksDao = mock() testee = BrowserViewModel( testOmnibarConverter, @@ -104,7 +109,9 @@ class BrowserViewModelTest { TrackerNetworks(), PrivacyMonitorRepository(), testStringResolver, - testNetworkLeaderboardDao, appConfigurationDao) + testNetworkLeaderboardDao, + bookmarksDao, + appConfigurationDao) testee.url.observeForever(queryObserver) testee.command.observeForever(navigationObserver) @@ -118,12 +125,39 @@ class BrowserViewModelTest { testee.command.removeObserver(navigationObserver) } + @Test + fun whenBookmarksResultCodeIsOpenUrlThenNavigate() { + testee.receivedBookmarksResult(BookmarksActivity.OPEN_URL_RESULT_CODE, "www.example.com") + val captor: ArgumentCaptor = ArgumentCaptor.forClass(Command::class.java) + verify(navigationObserver).onChanged(captor.capture()) + assertNotNull(captor.value) + assertTrue(captor.value is Navigate) + } + + @Test + fun whenUrlPresentThenAddBookmarkButtonEnabled() { + testee.urlChanged("www.example.com") + assertTrue(testee.viewState.value!!.canAddBookmarks) + } + + @Test + fun whenNoUrlThenAddBookmarkButtonDisabled() { + testee.urlChanged(null) + assertFalse(testee.viewState.value!!.canAddBookmarks) + } + + @Test + fun whenBookmarkAddedThenDaoIsUpdated() { + testee.addBookmark("A title", "www.example.com") + verify(bookmarksDao).insert(BookmarkEntity(title = "A title", url = "www.example.com")) + } + @Test fun whenTrackerDetectedThenNetworkLeaderbardUpdated() { testee.trackerDetected(TrackingEvent("http://www.example.com", "http://www.tracker.com/tracker.js", TrackerNetwork("Network1", "www.tracker.com"), false)) - assertNotNull(lastEntry) - assertEquals(lastEntry!!.domainVisited, "www.example.com") - assertEquals(lastEntry!!.networkName, "Network1") + assertNotNull(lastNetworkLeaderboardEntry) + assertEquals(lastNetworkLeaderboardEntry!!.domainVisited, "www.example.com") + assertEquals(lastNetworkLeaderboardEntry!!.networkName, "Network1") } @Test diff --git a/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/db/HttpsUpgradePerformanceTest.kt b/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/db/HttpsUpgradePerformanceTest.kt index bdfcbc508d76..2a726fe5536e 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/db/HttpsUpgradePerformanceTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/db/HttpsUpgradePerformanceTest.kt @@ -33,7 +33,6 @@ package com.duckduckgo.app.httpsupgrade.db */ import android.arch.persistence.room.Room -import android.content.Context import android.net.Uri import android.support.test.InstrumentationRegistry import com.duckduckgo.app.global.db.AppDatabase diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cb46717e210a..1d4eafa4b457 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -95,6 +95,12 @@ android:screenOrientation="fullSensor" android:theme="@style/AppTheme" /> + + diff --git a/app/src/main/java/com/duckduckgo/app/about/AboutDuckDuckGoActivity.kt b/app/src/main/java/com/duckduckgo/app/about/AboutDuckDuckGoActivity.kt index 9aa2630ac1ff..cc07514530cb 100644 --- a/app/src/main/java/com/duckduckgo/app/about/AboutDuckDuckGoActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/about/AboutDuckDuckGoActivity.kt @@ -21,8 +21,8 @@ import android.content.Intent import android.os.Bundle import android.support.v7.app.AppCompatActivity import com.duckduckgo.app.browser.R -import kotlinx.android.synthetic.main.activity_about_duck_duck_go.* import kotlinx.android.synthetic.main.content_about_duck_duck_go.* +import kotlinx.android.synthetic.main.include_toolbar.* class AboutDuckDuckGoActivity : AppCompatActivity() { diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarkEntity.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarkEntity.kt new file mode 100644 index 000000000000..4b0161a1db7b --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarkEntity.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.bookmarks.db + +import android.arch.persistence.room.Entity +import android.arch.persistence.room.PrimaryKey + +@Entity(tableName = "bookmarks") +data class BookmarkEntity(@PrimaryKey(autoGenerate = true) var id: Int = 0, var title: String?, var url: String) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt new file mode 100644 index 000000000000..d0a5024ec417 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.bookmarks.db + +import android.arch.lifecycle.LiveData +import android.arch.persistence.room.Dao +import android.arch.persistence.room.Delete +import android.arch.persistence.room.Insert +import android.arch.persistence.room.Query + +@Dao +interface BookmarksDao { + + @Insert + fun insert(bookmark: BookmarkEntity) + + @Query("select * from bookmarks") + fun bookmarks(): LiveData> + + @Delete + fun delete(bookmark: BookmarkEntity) + +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt new file mode 100644 index 000000000000..a43299b0967e --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.bookmarks.ui + +import android.app.Activity +import android.arch.lifecycle.Observer +import android.arch.lifecycle.ViewModelProviders +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.v7.widget.RecyclerView.Adapter +import android.support.v7.widget.RecyclerView.ViewHolder +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.global.DuckDuckGoActivity +import com.duckduckgo.app.global.ViewModelFactory +import kotlinx.android.synthetic.main.content_bookmarks.* +import kotlinx.android.synthetic.main.include_toolbar.* +import kotlinx.android.synthetic.main.view_bookmark_entry.view.* +import org.jetbrains.anko.alert +import org.jetbrains.anko.doAsync +import javax.inject.Inject + +class BookmarksActivity: DuckDuckGoActivity() { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + lateinit var adapter: BookmarksAdapter + + private val viewModel: BookmarksViewModel by lazy { + ViewModelProviders.of(this, viewModelFactory).get(BookmarksViewModel::class.java) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_bookmarks) + setupActionBar() + setupBookmarksRecycler() + observeViewModel() + } + + private fun setupBookmarksRecycler() { + adapter = BookmarksAdapter(applicationContext, viewModel) + recycler.adapter = adapter + } + + private fun setupActionBar() { + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + private fun observeViewModel() { + viewModel.viewState.observe(this, Observer { viewState -> + viewState?.let { + adapter.bookmarks = it.bookmarks + } + }) + + viewModel.command.observe(this, Observer { + when(it) { + is BookmarksViewModel.Command.ConfirmDeleteBookmark -> confirmDeleteBookmark(it.bookmark) + is BookmarksViewModel.Command.OpenBookmark -> openBookmark(it.bookmark) + } + }) + } + + private fun openBookmark(bookmark: BookmarkEntity) { + val intent = Intent(bookmark.url) + setResult(OPEN_URL_RESULT_CODE, intent) + finish() + } + + private fun confirmDeleteBookmark(bookmark: BookmarkEntity) { + val message = getString(R.string.bookmarkDeleteConfirmMessage, bookmark.title) + val title = getString(R.string.bookmarkDeleteConfirmTitle) + alert(message, title) { + positiveButton(android.R.string.yes) { delete(bookmark) } + negativeButton(android.R.string.no) { } + }.show() + } + + private fun delete(bookmark: BookmarkEntity) { + doAsync { + viewModel.delete(bookmark) + } + } + + companion object { + fun intent(context: Context): Intent { + return Intent(context, BookmarksActivity::class.java) + } + + val OPEN_URL_RESULT_CODE = Activity.RESULT_FIRST_USER + } + + class BookmarksAdapter(val context: Context, val viewModel: BookmarksViewModel): Adapter() { + + var bookmarks: List = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } + + + override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): BookmarksViewHolder { + val inflater = LayoutInflater.from(context) + val view = inflater.inflate(R.layout.view_bookmark_entry, parent, false) + return BookmarksViewHolder(view, viewModel) + } + + override fun onBindViewHolder(holder: BookmarksViewHolder?, position: Int) { + holder?.update(bookmarks[position]) + } + + override fun getItemCount(): Int { + return bookmarks.size + } + + } + + class BookmarksViewHolder(itemView: View?, val viewModel: BookmarksViewModel) : ViewHolder(itemView) { + + lateinit var bookmark: BookmarkEntity + + fun update(bookmark: BookmarkEntity) { + this.bookmark = bookmark + + itemView.delete.contentDescription = itemView.context.getString(R.string.deleteBookmarkContentDescription, bookmark.title) + itemView.title.text = bookmark.title + + itemView.delete.setOnClickListener { + viewModel.onDeleteRequested(bookmark) + } + + itemView.setOnClickListener { + viewModel.onSelected(bookmark) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt new file mode 100644 index 000000000000..95af7dc86145 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.bookmarks.ui + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import android.arch.lifecycle.Observer +import android.arch.lifecycle.ViewModel +import android.support.annotation.WorkerThread +import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.bookmarks.ui.BookmarksViewModel.Command.ConfirmDeleteBookmark +import com.duckduckgo.app.bookmarks.ui.BookmarksViewModel.Command.OpenBookmark +import com.duckduckgo.app.global.SingleLiveEvent + +class BookmarksViewModel(val dao: BookmarksDao): ViewModel() { + + data class ViewState(val bookmarks: List = emptyList()) + + sealed class Command { + + class OpenBookmark(val bookmark: BookmarkEntity) : Command() + class ConfirmDeleteBookmark(val bookmark: BookmarkEntity) : Command() + + } + + val viewState: MutableLiveData = MutableLiveData() + val command: SingleLiveEvent = SingleLiveEvent() + + private val bookmarks: LiveData> = dao.bookmarks() + private val bookmarksObserver = Observer> { onBookmarksChanged(it!!) } + + init { + viewState.value = ViewState() + bookmarks.observeForever(bookmarksObserver) + } + + override fun onCleared() { + super.onCleared() + bookmarks.removeObserver(bookmarksObserver) + } + + private fun onBookmarksChanged(bookmarks: List) { + viewState.value = viewState.value?.copy(bookmarks) + } + + fun onSelected(bookmark: BookmarkEntity) { + command.value = OpenBookmark(bookmark) + } + + fun onDeleteRequested(bookmark: BookmarkEntity) { + command.value = ConfirmDeleteBookmark(bookmark) + } + + @WorkerThread + fun delete(bookmark: BookmarkEntity) { + dao.delete(bookmark) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index b3e9945961e9..18f871535018 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -31,7 +31,7 @@ import android.view.View import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.webkit.WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE import android.widget.TextView -import android.widget.Toast +import com.duckduckgo.app.bookmarks.ui.BookmarksActivity import com.duckduckgo.app.browser.omnibar.OnBackKeyListener import com.duckduckgo.app.global.DuckDuckGoActivity import com.duckduckgo.app.global.ViewModelFactory @@ -39,9 +39,11 @@ import com.duckduckgo.app.global.view.* import com.duckduckgo.app.privacymonitor.model.PrivacyGrade import com.duckduckgo.app.privacymonitor.renderer.icon import com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardActivity -import com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardActivity.Companion.REQUEST_DASHBOARD import com.duckduckgo.app.settings.SettingsActivity import kotlinx.android.synthetic.main.activity_browser.* +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.toast +import org.jetbrains.anko.uiThread import javax.inject.Inject class BrowserActivity : DuckDuckGoActivity() { @@ -51,23 +53,12 @@ class BrowserActivity : DuckDuckGoActivity() { @Inject lateinit var viewModelFactory: ViewModelFactory private var acceptingRenderUpdates = true + private var addBookmarkMenuItem: MenuItem? = null private val viewModel: BrowserViewModel by lazy { ViewModelProviders.of(this, viewModelFactory).get(BrowserViewModel::class.java) } - companion object { - - fun intent(context: Context, sharedText: String? = null): Intent { - val intent = Intent(context, BrowserActivity::class.java) - intent.putExtra(SHARED_TEXT_EXTRA, sharedText) - return intent - } - - private const val SHARED_TEXT_EXTRA = "SHARED_TEXT_EXTRA" - private const val REQUEST_SETTINGS = 1001 - } - private val privacyGradeMenu: MenuItem? get() = toolbar.menu.findItem(R.id.privacy_dashboard_menu_item) @@ -148,6 +139,7 @@ class BrowserActivity : DuckDuckGoActivity() { false -> pageLoadingIndicator.hide() } + addBookmarkMenuItem?.setEnabled(viewState.canAddBookmarks) if (shouldUpdateOmnibarTextInput(viewState, viewState.omnibarText)) { omnibarTextInput.setText(viewState.omnibarText) appBarLayout.setExpanded(true, true) @@ -269,6 +261,8 @@ class BrowserActivity : DuckDuckGoActivity() { override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_browser_activity, menu) + addBookmarkMenuItem = menu?.findItem(R.id.add_bookmark_menu_item) + addBookmarkMenuItem?.setEnabled(false) return true } @@ -294,6 +288,14 @@ class BrowserActivity : DuckDuckGoActivity() { webView.goForward() return true } + R.id.add_bookmark_menu_item -> { + addBookmark() + return true + } + R.id.bookmarks_menu_item -> { + launchBookmarksView() + return true + } R.id.settings_menu_item -> { launchSettingsView() return true @@ -302,31 +304,47 @@ class BrowserActivity : DuckDuckGoActivity() { return false } + private fun addBookmark() { + val title = webView.title + val url = webView.url + doAsync { + viewModel.addBookmark(title, url) + uiThread { + toast(R.string.bookmarkAddedFeedback) + } + } + } + private fun finishActivityAnimated() { clearViewPriorToAnimation() supportFinishAfterTransition() } private fun launchPrivacyDashboard() { - startActivityForResult(PrivacyDashboardActivity.intent(this), REQUEST_DASHBOARD) + startActivityForResult(PrivacyDashboardActivity.intent(this), DASHBOARD_REQUEST_CODE) } private fun launchFire() { FireDialog(this, { finishActivityAnimated() }, { - Toast.makeText(this, R.string.fireDataCleared, Toast.LENGTH_SHORT).show() + applicationContext.toast(R.string.fireDataCleared) }).show() } private fun launchSettingsView() { - startActivityForResult(SettingsActivity.intent(this), REQUEST_SETTINGS) + startActivityForResult(SettingsActivity.intent(this), SETTINGS_REQUEST_CODE) + } + + private fun launchBookmarksView() { + startActivityForResult(BookmarksActivity.intent(this), BOOKMARKS_REQUEST_CODE) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { - REQUEST_DASHBOARD -> viewModel.receivedDashboardResult(resultCode) - REQUEST_SETTINGS -> viewModel.receivedSettingsResult(resultCode) + DASHBOARD_REQUEST_CODE -> viewModel.receivedDashboardResult(resultCode) + SETTINGS_REQUEST_CODE -> viewModel.receivedSettingsResult(resultCode) + BOOKMARKS_REQUEST_CODE -> viewModel.receivedBookmarksResult(resultCode, data?.action) else -> super.onActivityResult(requestCode, resultCode, data) } } @@ -346,4 +364,19 @@ class BrowserActivity : DuckDuckGoActivity() { omnibarTextInput.hideKeyboard() webView.hide() } + + companion object { + + fun intent(context: Context, sharedText: String? = null): Intent { + val intent = Intent(context, BrowserActivity::class.java) + intent.putExtra(SHARED_TEXT_EXTRA, sharedText) + return intent + } + + private const val SHARED_TEXT_EXTRA = "SHARED_TEXT_EXTRA" + private const val SETTINGS_REQUEST_CODE = 100 + private const val DASHBOARD_REQUEST_CODE = 101 + private const val BOOKMARKS_REQUEST_CODE = 102 + } + } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 7d8dd76d0d65..717484ade6a3 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -22,7 +22,11 @@ import android.arch.lifecycle.ViewModel import android.net.Uri import android.support.annotation.AnyThread import android.support.annotation.VisibleForTesting +import android.support.annotation.WorkerThread import com.duckduckgo.app.about.AboutDuckDuckGoActivity.Companion.RESULT_CODE_LOAD_ABOUT_DDG_WEB_PAGE +import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.bookmarks.ui.BookmarksActivity.Companion.OPEN_URL_RESULT_CODE import com.duckduckgo.app.browser.BrowserViewModel.Command.Navigate import com.duckduckgo.app.browser.BrowserViewModel.Command.Refresh import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter @@ -36,8 +40,8 @@ import com.duckduckgo.app.privacymonitor.model.TermsOfService import com.duckduckgo.app.privacymonitor.model.improvedGrade import com.duckduckgo.app.privacymonitor.store.PrivacyMonitorRepository import com.duckduckgo.app.privacymonitor.store.TermsOfServiceStore -import com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardActivity.Companion.RESULT_RELOAD -import com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardActivity.Companion.RESULT_TOSDR +import com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardActivity.Companion.RELOAD_RESULT_CODE +import com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardActivity.Companion.TOSDR_RESULT_CODE import com.duckduckgo.app.settings.db.AppConfigurationDao import com.duckduckgo.app.settings.db.AppConfigurationEntity import com.duckduckgo.app.trackerdetection.model.TrackerNetworks @@ -52,6 +56,7 @@ class BrowserViewModel( private val privacyMonitorRepository: PrivacyMonitorRepository, private val stringResolver: StringResolver, private val networkLeaderboardDao: NetworkLeaderboardDao, + private val bookmarksDao: BookmarksDao, appConfigurationDao: AppConfigurationDao) : WebViewClientListener, ViewModel() { data class ViewState( @@ -62,7 +67,8 @@ class BrowserViewModel( val browserShowing: Boolean = false, val showClearButton: Boolean = false, val showPrivacyGrade: Boolean = false, - val showFireButton: Boolean = true + val showFireButton: Boolean = true, + val canAddBookmarks: Boolean = false ) sealed class Command { @@ -94,7 +100,7 @@ class BrowserViewModel( private var appConfigurationDownloaded = false init { - viewState.value = ViewState() + viewState.value = ViewState(canAddBookmarks = false) privacyMonitorRepository.privacyMonitor = MutableLiveData() appConfigurationObservable.observeForever(appConfigurationObserver) } @@ -159,9 +165,13 @@ class BrowserViewModel( override fun urlChanged(url: String?) { Timber.v("Url changed: $url") - if (url == null) return + if (url == null) { + viewState.value = viewState.value?.copy(canAddBookmarks = false) + return + } var newViewState = currentViewState().copy( + canAddBookmarks = true, omnibarText = url, browserShowing = true, showFireButton = true, @@ -216,7 +226,7 @@ class BrowserViewModel( } fun onSharedTextReceived(input: String) { - command.value = Navigate(buildUrl(input)) + openUrl(buildUrl(input)) } /** @@ -234,10 +244,10 @@ class BrowserViewModel( fun receivedDashboardResult(resultCode: Int) { when (resultCode) { - RESULT_RELOAD -> command.value = Refresh() - RESULT_TOSDR -> { + RELOAD_RESULT_CODE -> command.value = Refresh() + TOSDR_RESULT_CODE -> { val url = stringResolver.getString(R.string.tosdrUrl) - command.value = Navigate(url) + openUrl(url) } } } @@ -246,10 +256,28 @@ class BrowserViewModel( when (resultCode) { RESULT_CODE_LOAD_ABOUT_DDG_WEB_PAGE -> { val url = stringResolver.getString(R.string.aboutUrl) - command.value = Navigate(url) + openUrl(url) + } + } + } + + @WorkerThread + fun addBookmark(title: String?, url: String?) { + bookmarksDao.insert(BookmarkEntity(title = title, url = url!!)) + } + + fun receivedBookmarksResult(resultCode: Int, action: String?) { + when (resultCode) { + OPEN_URL_RESULT_CODE -> { + openUrl(action ?: return) } } } + + private fun openUrl(url: String) { + command.value = Navigate(url) + } + } diff --git a/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt b/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt index c7cfdc734162..fae389ca37c0 100644 --- a/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt @@ -16,6 +16,7 @@ package com.duckduckgo.app.di +import com.duckduckgo.app.bookmarks.ui.BookmarksActivity import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.job.AppConfigurationJobService import com.duckduckgo.app.onboarding.ui.OnboardingActivity @@ -61,6 +62,9 @@ abstract class AndroidBindingModule { @ContributesAndroidInjector abstract fun settingsActivity(): SettingsActivity + @ActivityScoped + @ContributesAndroidInjector + abstract fun bookmarksActivity(): BookmarksActivity /* Services */ diff --git a/app/src/main/java/com/duckduckgo/app/di/DatabaseModule.kt b/app/src/main/java/com/duckduckgo/app/di/DatabaseModule.kt index 55822cd3b1e3..539d071aa47c 100644 --- a/app/src/main/java/com/duckduckgo/app/di/DatabaseModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/DatabaseModule.kt @@ -43,4 +43,10 @@ class DatabaseModule { @Provides fun providesNetworkLeaderboardDao(database: AppDatabase) = database.networkLeaderboardDao() + @Provides + fun providesBookmarksDao(database: AppDatabase) = database.bookmarksDao() + + @Provides + fun appConfigurationDao(database: AppDatabase) = database.appConfigurationDao() + } \ 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 9ec8d1856b65..cbefa965434c 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -18,10 +18,11 @@ package com.duckduckgo.app.global import android.arch.lifecycle.ViewModel import android.arch.lifecycle.ViewModelProvider +import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.bookmarks.ui.BookmarksViewModel import com.duckduckgo.app.browser.BrowserViewModel import com.duckduckgo.app.browser.DuckDuckGoUrlDetector import com.duckduckgo.app.browser.omnibar.QueryUrlConverter -import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.ui.OnboardingViewModel import com.duckduckgo.app.privacymonitor.db.NetworkLeaderboardDao @@ -33,6 +34,7 @@ import com.duckduckgo.app.privacymonitor.ui.PrivacyPracticesViewModel import com.duckduckgo.app.privacymonitor.ui.ScorecardViewModel import com.duckduckgo.app.privacymonitor.ui.TrackerNetworksViewModel import com.duckduckgo.app.settings.SettingsViewModel +import com.duckduckgo.app.settings.db.AppConfigurationDao import com.duckduckgo.app.trackerdetection.model.TrackerNetworks import javax.inject.Inject @@ -47,20 +49,22 @@ class ViewModelFactory @Inject constructor( private val termsOfServiceStore: TermsOfServiceStore, private val trackerNetworks: TrackerNetworks, private val stringResolver: StringResolver, - private val appDatabase: AppDatabase, - private val networkLeaderboardDao: NetworkLeaderboardDao + private val appConfigurationDao: AppConfigurationDao, + private val networkLeaderboardDao: NetworkLeaderboardDao, + private val bookmarksDao: BookmarksDao ) : ViewModelProvider.NewInstanceFactory() { override fun create(modelClass: Class) = with(modelClass) { when { isAssignableFrom(OnboardingViewModel::class.java) -> OnboardingViewModel(onboaringStore) - isAssignableFrom(BrowserViewModel::class.java) -> BrowserViewModel(queryUrlConverter, duckDuckGoUrlDetector, termsOfServiceStore, trackerNetworks, privacyMonitorRepository, stringResolver, networkLeaderboardDao, appDatabase.appConfigurationDao()) + isAssignableFrom(BrowserViewModel::class.java) -> BrowserViewModel(queryUrlConverter, duckDuckGoUrlDetector, termsOfServiceStore, trackerNetworks, privacyMonitorRepository, stringResolver, networkLeaderboardDao, bookmarksDao, appConfigurationDao) isAssignableFrom(PrivacyDashboardViewModel::class.java) -> PrivacyDashboardViewModel(privacySettingsStore, networkLeaderboardDao) isAssignableFrom(ScorecardViewModel::class.java) -> ScorecardViewModel(privacySettingsStore) isAssignableFrom(TrackerNetworksViewModel::class.java) -> TrackerNetworksViewModel() isAssignableFrom(PrivacyPracticesViewModel::class.java) -> PrivacyPracticesViewModel() isAssignableFrom(SettingsViewModel::class.java) -> SettingsViewModel(stringResolver) + isAssignableFrom(BookmarksViewModel::class.java) -> BookmarksViewModel(bookmarksDao) else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") } } as T diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 8527f3abaca3..3d8499fc73ab 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -18,6 +18,8 @@ package com.duckduckgo.app.global.db import android.arch.persistence.room.Database import android.arch.persistence.room.RoomDatabase +import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.duckduckgo.app.httpsupgrade.db.HttpsUpgradeDomain import com.duckduckgo.app.httpsupgrade.db.HttpsUpgradeDomainDao import com.duckduckgo.app.privacymonitor.db.NetworkLeaderboardDao @@ -27,11 +29,12 @@ import com.duckduckgo.app.settings.db.AppConfigurationEntity import com.duckduckgo.app.trackerdetection.db.TrackerDataDao import com.duckduckgo.app.trackerdetection.model.DisconnectTracker -@Database(exportSchema = true, version = 5, entities = [ +@Database(exportSchema = true, version = 6, entities = [ HttpsUpgradeDomain::class, DisconnectTracker::class, NetworkLeaderboardEntry::class, - AppConfigurationEntity::class + AppConfigurationEntity::class, + BookmarkEntity::class ]) abstract class AppDatabase : RoomDatabase() { @@ -39,5 +42,6 @@ abstract class AppDatabase : RoomDatabase() { abstract fun trackerDataDao(): TrackerDataDao abstract fun networkLeaderboardDao(): NetworkLeaderboardDao abstract fun appConfigurationDao(): AppConfigurationDao + abstract fun bookmarksDao(): BookmarksDao } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/global/view/ActivityExtension.kt b/app/src/main/java/com/duckduckgo/app/global/view/ActivityExtension.kt index d888efd250be..8bc964ef9239 100644 --- a/app/src/main/java/com/duckduckgo/app/global/view/ActivityExtension.kt +++ b/app/src/main/java/com/duckduckgo/app/global/view/ActivityExtension.kt @@ -18,13 +18,13 @@ package com.duckduckgo.app.global.view import android.content.Intent import android.support.v7.app.AppCompatActivity -import android.widget.Toast import com.duckduckgo.app.browser.R +import org.jetbrains.anko.toast fun AppCompatActivity.launchExternalActivity(intent: Intent) { if (intent.resolveActivity(packageManager) != null) { startActivity(intent) } else { - Toast.makeText(this, R.string.no_compatible_third_party_app_installed, Toast.LENGTH_SHORT).show() + toast(R.string.no_compatible_third_party_app_installed) } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/home/HomeActivity.kt b/app/src/main/java/com/duckduckgo/app/home/HomeActivity.kt index 36b50595fa77..33b1afb4497c 100644 --- a/app/src/main/java/com/duckduckgo/app/home/HomeActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/home/HomeActivity.kt @@ -23,8 +23,8 @@ import android.support.v4.app.ActivityOptionsCompat import android.support.v7.app.AppCompatActivity import android.view.Menu import android.view.MenuItem -import android.widget.Toast import com.duckduckgo.app.about.AboutDuckDuckGoActivity +import com.duckduckgo.app.bookmarks.ui.BookmarksActivity import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.intentText @@ -32,6 +32,7 @@ import com.duckduckgo.app.global.view.FireDialog import com.duckduckgo.app.settings.SettingsActivity import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.content_home.* +import org.jetbrains.anko.toast class HomeActivity : AppCompatActivity() { @@ -80,6 +81,10 @@ class HomeActivity : AppCompatActivity() { startActivityForResult(SettingsActivity.intent(this), SETTINGS_REQUEST_CODE) true } + R.id.bookmarks_menu_item -> { + startActivityForResult(BookmarksActivity.intent(this), BOOKMARKS_REQUEST_CODE) + true + } R.id.fire_menu_item -> { launchFire() true @@ -90,29 +95,42 @@ class HomeActivity : AppCompatActivity() { private fun launchFire() { FireDialog(this, {}, { - Toast.makeText(this, R.string.fireDataCleared, Toast.LENGTH_SHORT).show() + toast(R.string.fireDataCleared) }).show() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { SETTINGS_REQUEST_CODE -> onHandleSettingsResult(resultCode) + BOOKMARKS_REQUEST_CODE -> onHandleBookmarksResult(resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data) } } + private fun onHandleBookmarksResult(resultCode: Int, data: Intent?) { + when(resultCode) { + BookmarksActivity.OPEN_URL_RESULT_CODE -> { + openUrl(data?.action ?: return) + } + } + } + private fun onHandleSettingsResult(resultCode: Int) { when (resultCode) { AboutDuckDuckGoActivity.RESULT_CODE_LOAD_ABOUT_DDG_WEB_PAGE -> { - startActivity(BrowserActivity.intent(this, getString(R.string.aboutUrl))) + openUrl(getString(R.string.aboutUrl)) } } } + private fun openUrl(url: String) { + startActivity(BrowserActivity.intent(this, url)) + } companion object { private const val SETTINGS_REQUEST_CODE = 100 + private const val BOOKMARKS_REQUEST_CODE = 101 fun intent(context: Context): Intent { return Intent(context, HomeActivity::class.java) diff --git a/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/NetworkTrackerPillView.kt b/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/NetworkTrackerPillView.kt index ead2384721de..29be588cf98c 100644 --- a/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/NetworkTrackerPillView.kt +++ b/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/NetworkTrackerPillView.kt @@ -40,13 +40,16 @@ class NetworkTrackerPillView: FrameLayout { initLayout() } - fun initLayout() { + private fun initLayout() { View.inflate(context, R.layout.view_network_tracker_pill, this) } fun render(networkName: String?, percent: Float?) { icon.setImageResource(renderer.networkPillIcon(context, networkName ?: "") ?: R.drawable.network_pill_generic) - percentage.text = renderer.percentage(percent) + + val percentText = renderer.percentage(percent) + icon.contentDescription = "$networkName $percentText" + percentage.text = percentText } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/PrivacyDashboardActivity.kt b/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/PrivacyDashboardActivity.kt index f83c9a064b27..b34fb0d08d74 100644 --- a/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/PrivacyDashboardActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/PrivacyDashboardActivity.kt @@ -32,25 +32,13 @@ import com.duckduckgo.app.privacymonitor.PrivacyMonitor import com.duckduckgo.app.privacymonitor.renderer.* import com.duckduckgo.app.privacymonitor.store.PrivacyMonitorRepository import com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardViewModel.ViewState -import kotlinx.android.synthetic.main.activity_privacy_dashboard.* import kotlinx.android.synthetic.main.content_privacy_dashboard.* import kotlinx.android.synthetic.main.include_privacy_dashboard_header.* +import kotlinx.android.synthetic.main.include_toolbar.* import javax.inject.Inject - class PrivacyDashboardActivity : DuckDuckGoActivity() { - companion object { - - val REQUEST_DASHBOARD = 1000 - val RESULT_RELOAD = 1000 - val RESULT_TOSDR = 1001 - - fun intent(context: Context): Intent { - return Intent(context, PrivacyDashboardActivity::class.java) - } - } - @Inject lateinit var viewModelFactory: ViewModelFactory @Inject lateinit var repository: PrivacyMonitorRepository private val trackersRenderer = TrackersRenderer() @@ -89,7 +77,7 @@ class PrivacyDashboardActivity : DuckDuckGoActivity() { override fun onBackPressed() { if (viewModel.shouldReloadPage) { - setResult(RESULT_RELOAD) + setResult(RELOAD_RESULT_CODE) } super.onBackPressed() } @@ -128,18 +116,33 @@ class PrivacyDashboardActivity : DuckDuckGoActivity() { startActivity(ScorecardActivity.intent(this)) } - fun onNetworksClicked(view: View) { + fun onNetworksClicked(@Suppress("UNUSED_PARAMETER") view: View) { startActivity(TrackerNetworksActivity.intent(this)) } - fun onPracticesClicked(view: View) { - startActivityForResult(PrivacyPracticesActivity.intent(this), REQUEST_DASHBOARD) + + fun onPracticesClicked(@Suppress("UNUSED_PARAMETER") view: View) { + startActivityForResult(PrivacyPracticesActivity.intent(this), REQUEST_CODE_PRIVACY_PRACTICES) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == REQUEST_DASHBOARD && resultCode == RESULT_TOSDR) { - setResult(RESULT_TOSDR) + if (requestCode == REQUEST_CODE_PRIVACY_PRACTICES && resultCode == PrivacyPracticesActivity.TOSDR_RESULT_CODE) { + setResult(TOSDR_RESULT_CODE) finish() } } + + companion object { + + private const val REQUEST_CODE_PRIVACY_PRACTICES = 100 + + const val RELOAD_RESULT_CODE = 100 + const val TOSDR_RESULT_CODE = 101 + + fun intent(context: Context): Intent { + return Intent(context, PrivacyDashboardActivity::class.java) + } + + } + } diff --git a/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/PrivacyPracticesActivity.kt b/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/PrivacyPracticesActivity.kt index 9b554afd7369..1a6a5352b1a0 100644 --- a/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/PrivacyPracticesActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/PrivacyPracticesActivity.kt @@ -30,11 +30,10 @@ import com.duckduckgo.app.privacymonitor.PrivacyMonitor import com.duckduckgo.app.privacymonitor.renderer.banner import com.duckduckgo.app.privacymonitor.renderer.text import com.duckduckgo.app.privacymonitor.store.PrivacyMonitorRepository -import kotlinx.android.synthetic.main.activity_privacy_dashboard.* import kotlinx.android.synthetic.main.content_privacy_practices.* +import kotlinx.android.synthetic.main.include_toolbar.* import javax.inject.Inject - class PrivacyPracticesActivity : DuckDuckGoActivity() { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -42,12 +41,6 @@ class PrivacyPracticesActivity : DuckDuckGoActivity() { private val practicesAdapter = PrivacyPracticesAdapter() - companion object { - fun intent(context: Context): Intent { - return Intent(context, PrivacyPracticesActivity::class.java) - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_privacy_practices) @@ -84,8 +77,18 @@ class PrivacyPracticesActivity : DuckDuckGoActivity() { practicesAdapter.updateData(viewState.goodTerms, viewState.badTerms) } - fun onTosdrLinkClicked(view: View) { - setResult(PrivacyDashboardActivity.RESULT_TOSDR) + fun onTosdrLinkClicked(@Suppress("UNUSED_PARAMETER") view: View) { + setResult(TOSDR_RESULT_CODE) finish() } + + companion object { + + val TOSDR_RESULT_CODE = 100 + + fun intent(context: Context): Intent { + return Intent(context, PrivacyPracticesActivity::class.java) + } + } + } diff --git a/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/ScorecardActivity.kt b/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/ScorecardActivity.kt index d44af8c31e1d..9c964286368b 100644 --- a/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/ScorecardActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/ScorecardActivity.kt @@ -31,12 +31,11 @@ import com.duckduckgo.app.global.view.html import com.duckduckgo.app.privacymonitor.PrivacyMonitor import com.duckduckgo.app.privacymonitor.renderer.* import com.duckduckgo.app.privacymonitor.store.PrivacyMonitorRepository -import kotlinx.android.synthetic.main.activity_privacy_scorecard.* import kotlinx.android.synthetic.main.content_privacy_scorecard.* import kotlinx.android.synthetic.main.include_privacy_dashboard_header.* +import kotlinx.android.synthetic.main.include_toolbar.* import javax.inject.Inject - class ScorecardActivity : DuckDuckGoActivity() { @Inject lateinit var viewModelFactory: ViewModelFactory diff --git a/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/TrackerNetworksActivity.kt b/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/TrackerNetworksActivity.kt index 0df1c523b977..9687b46f7da8 100644 --- a/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/TrackerNetworksActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/TrackerNetworksActivity.kt @@ -28,11 +28,10 @@ import com.duckduckgo.app.global.ViewModelFactory import com.duckduckgo.app.privacymonitor.PrivacyMonitor import com.duckduckgo.app.privacymonitor.renderer.TrackersRenderer import com.duckduckgo.app.privacymonitor.store.PrivacyMonitorRepository -import kotlinx.android.synthetic.main.activity_tracker_networks.* import kotlinx.android.synthetic.main.content_tracker_networks.* +import kotlinx.android.synthetic.main.include_toolbar.* import javax.inject.Inject - class TrackerNetworksActivity : DuckDuckGoActivity() { @Inject lateinit var viewModelFactory: ViewModelFactory diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt index 995aba1766f6..1491cbb0389b 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt @@ -28,8 +28,8 @@ import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.DuckDuckGoActivity import com.duckduckgo.app.global.ViewModelFactory import com.duckduckgo.app.global.view.launchExternalActivity -import kotlinx.android.synthetic.main.activity_settings.* import kotlinx.android.synthetic.main.content_settings.* +import kotlinx.android.synthetic.main.include_toolbar.* import javax.inject.Inject class SettingsActivity : DuckDuckGoActivity() { diff --git a/app/src/main/res/drawable-hdpi/ic_remove.png b/app/src/main/res/drawable-hdpi/ic_remove.png new file mode 100755 index 0000000000000000000000000000000000000000..ccf8afa52114cbf2260597eded39835f6fd7652a GIT binary patch literal 714 zcmV;*0yX`KP)Px%gh@m}R7efIRljRgK@gr-9Ivr&(Xo?`J@~D`=#??C= zzn>Z(-6bHtkO|L8SfM1_=}=?-G;ePH6|9gcbEeiU-r;MujyH60<}A^@#{s9SmkGJ~ zQOsn1B^ek_DbpT*8eF#p{Q`W#OtKj1>z{#0Tgu7sq#c5E(y-j9W-Mqm0Bh2`zgFOs z4`Y*(o=&hHPE#VxKE`7aUE}{A9|9> z7RvUwz?HscYr)a}NQ<@-L7d=^<@veF1a<>d{Mq3M*J}=&9}e?4eU_M?UKzJ^%m! literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_remove.png b/app/src/main/res/drawable-mdpi/ic_remove.png new file mode 100755 index 0000000000000000000000000000000000000000..156be55fd64f6fb69da9f0d23ed171d134ea14a4 GIT binary patch literal 478 zcmV<40U`d0P)Px$m`OxIR5%f(R69?@Kn%7|;RnFV3VYgtg*L4O{{cF{Ru=vX6D)-Z24G|cq`oGG zmJuXYCJ+p)RgE9Hkfzs%#7W7W?e9Kc?C%1=gDUZl%(|e@52Edu%;6Urt(JCxPESZ6 zNR?6>h-#>zJPxtrNU-_ANHsZEi|Jp)z${OSbUNSN1IWW;3Ne{&#ci6SR<6Qm+|@n# z7s5C8pV#=XaSI5V`B-u77?2d5McdL<;x7iT7K3X0Gl`*97}FUbth0H|XE{<3zveSV zF2T;A=d8*zQl(!!RV1Gxn-R{V%t%X{**8~wvP&@=iL3sR>em6$0c-OKR_9eKQVl4wt_zFktjweZg9E+F2DNPx&(@8`@R9FeMSTSo9K@^@hxim&Fu@FsR7e#GyF@<=05){wEMui}VR;h&u;*Su) zh;@pf2og{m3r~!}yOR{gq*6jUiy#&nF|o;I$M@deOlEiIcK7bqfxVe|@6Gq#zI`+A z%?Z(J${StNW1xX5jVeu`UK)wnXF||}%`M3+>jxUEEGU%c4I#{XUK&%tD{SznL|7Zh zM*%9Xi%M$cq!~!7P+LDW(bY>KwqCdxHZ82*=IF5$GkVCq@``i#!hckG?554oNN zu^Bw>9~^?k#Z9To<0Yp6N$qU@q7K@nkx*jNlbRB;Y9Z{U;%67n8t8Ua^5;b+(K3 zyu`!^e@fgE9RmS;X&Xlbrpl+U2<6XH@W;V1j2kN#qI45SQ3m@?`BO+A$7cg~=8_Nt z(@?MTK8?!9fG`}={Ur+?)IK$AFax9}CI3v z!Oq@TZ))TY`%3HF!<0i`sF&6{DKG^Tcif-319aGbQ1%SM`nG4{0oTf?iT2P*9ct=K z9O(FokVvM95=Y`dk>eo~5QyarDIxRn`Iq_#@UE`N?=LkUg4lpKEonIbIdWK3Dm1n@g4r2w`n}a zngW$gLT&j~l;;GB3elW5x9{J^BJy13ZAFTe_#mWw>{9O1)7Bt9>%Ol)5WbjpE$o^` z0djccE|m)|jF>@uayNg|@qf~_AU7HXTj4;-{_ccrf`}%{8=K(_@V|{=W%$Ba$g1+_ zLz?PlJd`OZ>Q#C7RvF12;@!|gMboo=s5{t^RQA)2B8OV7XQK!}%3GKjx%kNiuV)an zh4L9yV#zH_nVgIeaMN?;I2nXxp*WU^xArxWog;Gt4Wb%VB^wm0IdU{11O-dMOx#Z84uYiIKl30K1W)`rsp`WivGdt(ZovlPJ?X4eA`jG_c7?ZzAQ5y6{*m7 z&-R9`M~_nt(t^6@|Igs>Wi;?29v4TEKHPdpgw8a4;2YMmZ-v_Ty4{tRgk9|)TZm3K TVSI2800000NkvXXu0mjfpFrv% literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_remove.png b/app/src/main/res/drawable-xxhdpi/ic_remove.png new file mode 100755 index 0000000000000000000000000000000000000000..780f416d47c2239dca2842786e0ab23a25cb205b GIT binary patch literal 1677 zcmV;826Fj{P)Px*M@d9MRA>d=8ar$pMfAPBGoc9;AOu25BqEvw<($Mq_#B%eNEnHRm~==4@sXS$ zs#KIzsSv?Z!Y2hCI5bEEi4^hY93dS05TxNFLWDp_Nktlhb342@vooGq&+P5oZEWL} z?q=rA`|kYSd$R!6Bbxhu6~lwaH3+#PhW7~#&?PQ>hrv!psuR^UiGAulO$2c9EBQku zBR##^vsx=;D?1rCuM{g}c!)#$_ln`hO#`4xkS$Xtd3Kso2!W(+_5y5EPQEE|Ry#F` zRVG&5sRm!4He=Nmq9aqjr2lb;h1JkR49d(bCa`E_RjR`;4LY6g^PSz=b?Bs*9Hds; zX`05lvjFGLm}YQ942G{#!%_2c%)s~%Etq~mhEVtM@IteZ>b5Ks9K-qctON5kM6+>* zG`LNR=8|NTLvo}QsycA*E}Z%1H>NjJrjUR!SN^#sMjCPnNl9G^m?(SHfHxaIfe^bj zuuPf65-`)c!`WU$P6suzMa5v3=&6xBs<&OZ8od+*G_O2_;DblMa=`l2N)jfo`#Abw z*t#-frV)+yD5PS>hbUo)H?h@zfa``xBhzI4h{o;}-3G)Pg#4gs`-tpLIm1T=e=I)% zlB2k={F%+euCLLMJ87r;m;MKA#OOM`Uu-RZVADCmDiLCH=eo6NNMYWcB~R2>8y9h* zJ*0%`1FA2EU5F7sPI$}ff>up8&D1*0w3mb2LT5~nfI4#LkBwTV6>}K`jM+d_Z#GP| ziVEKS$&sl^i#$PcU}%0GtwQr69=w~FbtcB1+{q_Y5kXIV)*ff}Z9Y5Y5uf1uJu&qW zJ-oZFH&gC5micgLNBNevt%JV4AdJof-rjxvg~Bv}nne320n-;i;9gQ-JE{2x>H0M! z$rR2l{h)D+%0gk}sJ{r1ept~rrK2JEb z3B2WM!L6mKgqHP@YPtHk@u8aFTzk&nT_sexifYWrNvV=`o|g^~r7-CYk$;y*s-#UR z7R|@ZM%%yBoqd-aeP+2F=4FIQ#^hA?EkH9MXSXoS)p!)M-JnqFe%+2PtB%rPoSjqA0Zu@PFc=pBYbQfN zQJ5}b#Ik8}zOMyQq)E3v-ro?8lu9rYufh=%6B9M~>z8-QFRv`p=%#{dZ?Zpk7a$2*pB~Q`%wuAO5#-z?kCN~An@APx9 zlvSuIJl2(OP56AW9%GnHc0?as7N?D_lI|*DqZE--@$O^@Hpo)G{39P&#qE6BZNU=^V{ZT)D}4(X$2*T*aO;srHEptq zc16l3Z@EWo=Lqai`t6Le?f2^dm*=BMdPZhhu@5EwQQpo8u}L0g5zkVRC7J4x zeDji#O_b1~I~tBkx5`7duuaPI;E!vfz8Kh~Z^63PlD?u0!|9@n!n4Iqs&WanNO$Y9 z&N#tr9Di6q#$71EWs{-E%fKN!Pa>ycUQ6W^!$msd`uo--&$#k7u2A50*q1Kw8m8rw zfu}cJT0kqunalb13Ej+>g@$Qll_n5$-@nl#NF#)JJfDiydfH_v!!&_7G_(skD{SDn zhOJLY>waL{WIp^ak{xB3%}6FnoBJmK#XFQri24-K=HspNwk_~&Xe~|K8-f=%0PNW? zA3v29KClNmVw4QtLnBK^2<#vw@1uI=Y~wBJ`Px-_DMuRRCodHTuY1;MHsHXXBH4dFo*&d14_6F%fk!1fJzW9py6>LhC~vhCZLFV zF_I0&gAY_BQDZcQC>KBlE+C=;yLgbtf;WRgJP0I6!Xp@9ce?!k>Zzu>yL!5NW_osJ zLnpiS`s=T+zOJgSs=sQ42tBf+{gj;DzRQdz9gwr9N$odXI&zYfP@I4kcC@FGTVxDE zmP>m5&7kbNZ>cbOAmx%^YA{qkOsOac6fQfa&j1}aWmi|A)*hz0GaeKDrJn;i5MUxX z&dXsD;6zO_D6qc{7Sc~qdqkK>PPk2GvZg>Se61l*BIABj;f%HlXbODB%@S-G%yck# ziH`K%XU6%8WDM!1u+1Mpl#IZp!iXb!mAGvP=^itptMV41b8w;qk{+g&9s1`OdJti zp`PB+TA8mXlakxHZ&J_6VH{Lr1eCZsEt4z7P}w9RgLK5RMHk9Jsv4VK=l%2vUd}QmTBES+0cyEP3gZTg&~uu zyZ2kI$lAv$js!&KQG*Fufp)EmbYj?*bEdBpuH2fbTXCxxIc^E<-Fq{v&lau-NG@MK zM4UT)0~ssI*%Zgdk+T^?|I2ad)`BpT#< zSY^#=K9-d=c&VYOv@HvUYD^28=;2w>vlw~5Y!*z-)u>wR6@M&C8y4DrXdyLXE> zZ#a1HhQLZ-z(5iGuD|^LNUg!3Ou&K6!uzlc-Hu7v@r1Ii$*BR!dGm&e{wuox;z_F} zTO?6QIJAlmpV($gw#6j^0+6EUyK(_FuYSXYOLm-BsQbi+HV|f9ML-w=HiMD1W`-L5 zQcRdUjCSm}6{jZ_P_E-qvG6z@I{u3dl)o@D6S1BuG$?LWzxMp|RM-a^zb%LkUQ9@RoysN^1yf1gw+&RdXw_l?hmN}G_S|vF% zZ}+m)x$+crS48)QZRMreFSg|i$65B2gCevr-79O@QkpI`*(;MHn+%Y!*cXB?)eEKA zwXkK&9!IB|iy^{>6;=wDv)3f29P<}%Wpl@doYZGlOI=;AlXW4Nxsio{KuX-bu^ zk%e8^F>8{LeSd}HYYzv_QDJ?NrF2*TOC&zK_DDFss<2b+pzdQwD&kfA8ZoC$9@(*y_HgfRjg)$)UJ{&eNwl9!3wjANS-QC;l-puCP{Nu1j#v%P86lS z?HHaKzulHpBWpU2*c94Um?kHym?bh3K^PF1qNQwW=%%Ozbk$M#pkqO^(q;_U?7wmxnd>uM4Hek5LH_C$N6j$ThKw~pkebI_zxa2} zX4R%;Aqf~=5+lYwXwIYjE+!W(8miV4wc4DMFf?^K?Cn@nDK5%5eS*Zb3%k*+8aw-K zMzWX#=E5YXyx>cJY(nQAtS*j_C{9p6?;_5DU6*QJL=HBxcu_q6sWDA=a5VH^Z{a21SC8{T(TmhawR6fTg3Q>U#oP9u2R``shBc0maeYBPky{H@HREr zwj6p}6z_#L@OaCk10dvUG2(%p`aE$x zOUi|yIDqBr!T5nQ-sY84aFlKeI_E?Xcn^B%NW|ffAKt{{pX;Kl_ - - - - - + diff --git a/app/src/main/res/layout/activity_bookmarks.xml b/app/src/main/res/layout/activity_bookmarks.xml new file mode 100644 index 000000000000..2573bc162c3c --- /dev/null +++ b/app/src/main/res/layout/activity_bookmarks.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_privacy_dashboard.xml b/app/src/main/res/layout/activity_privacy_dashboard.xml index c11e5b8039ff..9ad7ac6df252 100644 --- a/app/src/main/res/layout/activity_privacy_dashboard.xml +++ b/app/src/main/res/layout/activity_privacy_dashboard.xml @@ -15,25 +15,12 @@ --> - - - - - + diff --git a/app/src/main/res/layout/activity_privacy_practices.xml b/app/src/main/res/layout/activity_privacy_practices.xml index 6b20277358c8..409647503d81 100644 --- a/app/src/main/res/layout/activity_privacy_practices.xml +++ b/app/src/main/res/layout/activity_privacy_practices.xml @@ -15,25 +15,12 @@ --> - - - - - + diff --git a/app/src/main/res/layout/activity_privacy_scorecard.xml b/app/src/main/res/layout/activity_privacy_scorecard.xml index ded5d3df8605..2b5f033841fb 100644 --- a/app/src/main/res/layout/activity_privacy_scorecard.xml +++ b/app/src/main/res/layout/activity_privacy_scorecard.xml @@ -15,25 +15,12 @@ --> - - - - - + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 8be39ee0b52c..ab49116869e8 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -15,25 +15,12 @@ --> - - - - - + diff --git a/app/src/main/res/layout/activity_tracker_networks.xml b/app/src/main/res/layout/activity_tracker_networks.xml index f10db0b8c57c..7d46d02e1b0d 100644 --- a/app/src/main/res/layout/activity_tracker_networks.xml +++ b/app/src/main/res/layout/activity_tracker_networks.xml @@ -15,25 +15,12 @@ --> - - - - - + diff --git a/app/src/main/res/layout/content_bookmarks.xml b/app/src/main/res/layout/content_bookmarks.xml new file mode 100644 index 000000000000..0486193a7560 --- /dev/null +++ b/app/src/main/res/layout/content_bookmarks.xml @@ -0,0 +1,23 @@ + + + + diff --git a/app/src/main/res/layout/include_toolbar.xml b/app/src/main/res/layout/include_toolbar.xml new file mode 100644 index 000000000000..a027f3ccbfe8 --- /dev/null +++ b/app/src/main/res/layout/include_toolbar.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_bookmark_entry.xml b/app/src/main/res/layout/view_bookmark_entry.xml new file mode 100644 index 000000000000..aa7275be636a --- /dev/null +++ b/app/src/main/res/layout/view_bookmark_entry.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_browser_activity.xml b/app/src/main/res/menu/menu_browser_activity.xml index 99829c904476..d8ada3234692 100644 --- a/app/src/main/res/menu/menu_browser_activity.xml +++ b/app/src/main/res/menu/menu_browser_activity.xml @@ -42,6 +42,16 @@ android:title="@string/refresh" app:showAsAction="never" /> + + + + + + Search or type URL Clear search input No compatible app installed + Add Bookmark Privacy Dashboard @@ -109,4 +110,12 @@ After all, the internet shouldn\'t feel so creepy, and getting the privacy you deserve online should be as simple closing the blinds. More at duckduckgo.com/about + + Bookmarks + Bookmarks + Delete bookmark for %s + Are you sure you want to delete this bookmark?\n\n\'%s\' + Confirm + Bookmark added + From dda4c22bbda3d61ff68f9b33920908c346b869c5 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Tue, 16 Jan 2018 15:26:39 +0000 Subject: [PATCH 03/16] Update app name and remove no longer needed db upgrade schemas --- app/build.gradle | 2 +- .../{6.json => 1.json} | 2 +- .../2.json | 71 ---------- .../3.json | 98 -------------- .../4.json | 124 ------------------ .../5.json | 124 ------------------ .../duckduckgo/app/global/db/AppDatabase.kt | 2 +- 7 files changed, 3 insertions(+), 420 deletions(-) rename app/schemas/com.duckduckgo.app.global.db.AppDatabase/{6.json => 1.json} (99%) delete mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/2.json delete mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/3.json delete mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/4.json delete mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/5.json diff --git a/app/build.gradle b/app/build.gradle index ed95c6ca89e8..f9a39ecd90bf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,7 +13,7 @@ android { compileSdkVersion 27 buildToolsVersion "26.0.2" defaultConfig { - applicationId "com.duckduckgo.app" + applicationId "com.duckduckgo.mobile.android" minSdkVersion 21 targetSdkVersion 27 versionCode buildVersionCode() diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/6.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/1.json similarity index 99% rename from app/schemas/com.duckduckgo.app.global.db.AppDatabase/6.json rename to app/schemas/com.duckduckgo.app.global.db.AppDatabase/1.json index 888cf1a4b60b..61a2f0502d0c 100644 --- a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/6.json +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/1.json @@ -1,7 +1,7 @@ { "formatVersion": 1, "database": { - "version": 6, + "version": 1, "identityHash": "cf29693b9b6c4b791ee9c6cc434e0ee6", "entities": [ { diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/2.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/2.json deleted file mode 100644 index 6c1b64c69a90..000000000000 --- a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/2.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 2, - "identityHash": "e91e82a2b5c652807026be5f72a6c56e", - "entities": [ - { - "tableName": "https_upgrade_domain", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", - "fields": [ - { - "fieldPath": "domain", - "columnName": "domain", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "domain" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "disconnect_tracker", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `category` TEXT NOT NULL, `networkName` TEXT NOT NULL, `networkUrl` TEXT NOT NULL, PRIMARY KEY(`url`))", - "fields": [ - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "category", - "columnName": "category", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "networkName", - "columnName": "networkName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "networkUrl", - "columnName": "networkUrl", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "url" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"e91e82a2b5c652807026be5f72a6c56e\")" - ] - } -} \ No newline at end of file diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/3.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/3.json deleted file mode 100644 index 514f797ca94c..000000000000 --- a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/3.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 3, - "identityHash": "edcb0f51325e54c85c931334898d7916", - "entities": [ - { - "tableName": "https_upgrade_domain", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", - "fields": [ - { - "fieldPath": "domain", - "columnName": "domain", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "domain" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "disconnect_tracker", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `category` TEXT NOT NULL, `networkName` TEXT NOT NULL, `networkUrl` TEXT NOT NULL, PRIMARY KEY(`url`))", - "fields": [ - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "category", - "columnName": "category", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "networkName", - "columnName": "networkName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "networkUrl", - "columnName": "networkUrl", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "url" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "network_leaderboard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkName` TEXT NOT NULL, `domainVisited` TEXT NOT NULL, PRIMARY KEY(`networkName`, `domainVisited`))", - "fields": [ - { - "fieldPath": "networkName", - "columnName": "networkName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "domainVisited", - "columnName": "domainVisited", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "networkName", - "domainVisited" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"edcb0f51325e54c85c931334898d7916\")" - ] - } -} \ No newline at end of file diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/4.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/4.json deleted file mode 100644 index 79e31ee03cb3..000000000000 --- a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/4.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 4, - "identityHash": "df2202389ba5b11018b57a417457b11b", - "entities": [ - { - "tableName": "https_upgrade_domain", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", - "fields": [ - { - "fieldPath": "domain", - "columnName": "domain", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "domain" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "disconnect_tracker", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `category` TEXT NOT NULL, `networkName` TEXT NOT NULL, `networkUrl` TEXT NOT NULL, PRIMARY KEY(`url`))", - "fields": [ - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "category", - "columnName": "category", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "networkName", - "columnName": "networkName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "networkUrl", - "columnName": "networkUrl", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "url" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "network_leaderboard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkName` TEXT NOT NULL, `domainVisited` TEXT NOT NULL, PRIMARY KEY(`networkName`, `domainVisited`))", - "fields": [ - { - "fieldPath": "networkName", - "columnName": "networkName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "domainVisited", - "columnName": "domainVisited", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "networkName", - "domainVisited" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "app_configuration", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `appConfigurationDownloaded` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appConfigurationDownloaded", - "columnName": "appConfigurationDownloaded", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"df2202389ba5b11018b57a417457b11b\")" - ] - } -} \ No newline at end of file diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/5.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/5.json deleted file mode 100644 index 0e3bb84f8bc3..000000000000 --- a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/5.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 5, - "identityHash": "df2202389ba5b11018b57a417457b11b", - "entities": [ - { - "tableName": "https_upgrade_domain", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", - "fields": [ - { - "fieldPath": "domain", - "columnName": "domain", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "domain" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "disconnect_tracker", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `category` TEXT NOT NULL, `networkName` TEXT NOT NULL, `networkUrl` TEXT NOT NULL, PRIMARY KEY(`url`))", - "fields": [ - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "category", - "columnName": "category", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "networkName", - "columnName": "networkName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "networkUrl", - "columnName": "networkUrl", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "url" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "network_leaderboard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkName` TEXT NOT NULL, `domainVisited` TEXT NOT NULL, PRIMARY KEY(`networkName`, `domainVisited`))", - "fields": [ - { - "fieldPath": "networkName", - "columnName": "networkName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "domainVisited", - "columnName": "domainVisited", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "networkName", - "domainVisited" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "app_configuration", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `appConfigurationDownloaded` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appConfigurationDownloaded", - "columnName": "appConfigurationDownloaded", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"df2202389ba5b11018b57a417457b11b\")" - ] - } -} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 3d8499fc73ab..c961ef2a1616 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -29,7 +29,7 @@ import com.duckduckgo.app.settings.db.AppConfigurationEntity import com.duckduckgo.app.trackerdetection.db.TrackerDataDao import com.duckduckgo.app.trackerdetection.model.DisconnectTracker -@Database(exportSchema = true, version = 6, entities = [ +@Database(exportSchema = true, version = 1, entities = [ HttpsUpgradeDomain::class, DisconnectTracker::class, NetworkLeaderboardEntry::class, From abd397633d0259f0003530aeb150f63557589d81 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Wed, 17 Jan 2018 11:51:35 +0000 Subject: [PATCH 04/16] Update placeholder colors --- .../duckduckgo/app/onboarding/ui/OnboardingActivity.kt | 4 ++-- app/src/main/res/layout/content_onboarding_no_trace.xml | 2 +- .../main/res/layout/content_onboarding_protect_data.xml | 2 +- app/src/main/res/values/colors.xml | 8 +++++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingActivity.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingActivity.kt index 6dc69620a331..dad74c0753c7 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingActivity.kt @@ -149,10 +149,10 @@ class OnboardingActivity : DuckDuckGoActivity() { val pageCount = 2 @ColorRes - val firstColor = R.color.lighMuddyGreen + val firstColor = R.color.lighOliveGreen @ColorRes - val secondColor = R.color.lightWindowsBlue + val secondColor = R.color.powderBlue } } diff --git a/app/src/main/res/layout/content_onboarding_no_trace.xml b/app/src/main/res/layout/content_onboarding_no_trace.xml index 8a46ee102749..43ad83b588f6 100644 --- a/app/src/main/res/layout/content_onboarding_no_trace.xml +++ b/app/src/main/res/layout/content_onboarding_no_trace.xml @@ -22,7 +22,7 @@ android:layout_height="match_parent" android:paddingEnd="44dp" android:paddingStart="44dp" - tools:background="@color/lightWindowsBlue" + tools:background="@color/skyBlue" tools:context="com.duckduckgo.app.onboarding.ui.OnboardingActivity"> #de5833 #d03a10 + #50BFFF - #93B358 + #3c90c0 + + #93c04d #58732e #3fa140 - #6FBDF9 - #3c90c0 + From ae514557eb994a810e60e31ae2abe32ffdb97e1a Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Wed, 17 Jan 2018 11:52:32 +0000 Subject: [PATCH 05/16] Update tosdr text --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1c2afdb97749..425353e87c0f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -78,7 +78,7 @@ "Poor Privacy Practices" Unknown Privacy Practices Privacy practices indicate how much the personal information that you share with a website is protected. - Privacy Practices results from TOSDR + Privacy Practices results from ToS;DR Good Practice Icon Bad Practice Icon Privacy Protection From b01146f2bc84609a298b8aa164818897311c5b59 Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Thu, 18 Jan 2018 05:01:39 +0000 Subject: [PATCH 06/16] Opt out of web view metrics (#114) --- app/src/main/AndroidManifest.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d4eafa4b457..f25cb8495f99 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,10 @@ android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> + + Date: Thu, 18 Jan 2018 11:12:34 +0000 Subject: [PATCH 07/16] Add latest tosdr data --- app/src/main/res/raw/tosdr.json | 643 +++++++++++++++++++------------- 1 file changed, 376 insertions(+), 267 deletions(-) diff --git a/app/src/main/res/raw/tosdr.json b/app/src/main/res/raw/tosdr.json index a22751a8d33e..f37c6719bf38 100644 --- a/app/src/main/res/raw/tosdr.json +++ b/app/src/main/res/raw/tosdr.json @@ -3,13 +3,13 @@ "score": 0, "all": { "bad": [ - "broader than necessary", - "user needs to check tosback.org", - "reduction of legal period for cause of action" - ], + "broader than necessary", + "user needs to check tosback.org", + "reduction of legal period for cause of action" + ], "good": [ - "help you deal with take-down notices" - ] + "help you deal with take-down notices" + ] }, "match": { "bad": [], @@ -21,13 +21,13 @@ "score": 0, "all": { "bad": [ - "user needs to check tosback.org", - "pseudonym not allowed (not because of user-to-user trust)" - ], + "user needs to check tosback.org", + "pseudonym not allowed (not because of user-to-user trust)" + ], "good": [ - "limited for purpose of same service", - "limited for purpose of same service" - ] + "limited for purpose of same service", + "limited for purpose of same service" + ] }, "match": { "bad": [], @@ -39,8 +39,8 @@ "score": 0, "all": { "bad": [ - "pseudonym not allowed (not because of user-to-user trust)" - ], + "pseudonym not allowed (not because of user-to-user trust)" + ], "good": [] }, "match": { @@ -53,11 +53,11 @@ "score": 0, "all": { "bad": [ - "user needs to check tosback.org" - ], + "user needs to check tosback.org" + ], "good": [ - "limited for purpose of same service" - ] + "limited for purpose of same service" + ] }, "match": { "bad": [], @@ -70,11 +70,11 @@ "all": { "bad": [], "good": [ - "suspension will be fair and proportionate", - "you publish under a free license, not a bilateral one", - "user feedback is invited", - "only temporary session cookies" - ] + "suspension will be fair and proportionate", + "you publish under a free license, not a bilateral one", + "user feedback is invited", + "only temporary session cookies" + ] }, "match": { "bad": [], @@ -98,16 +98,16 @@ "score": 0, "all": { "bad": [ - "little involvement", - "sets third-party cookies and/or ads", - "your content stays licensed", - "very broad" - ], + "little involvement", + "sets third-party cookies and/or ads", + "your content stays licensed", + "very broad" + ], "good": [ - "you can get your data back", - "tracking data deleted after 10 days and opt-out", - "archives provided" - ] + "you can get your data back", + "tracking data deleted after 10 days and opt-out", + "archives provided" + ] }, "match": { "bad": [], @@ -119,32 +119,32 @@ "score": 85, "all": { "bad": [ - "they can license to third parties", - "reduction of legal period for cause of action", - "responsible and indemnify" - ], + "they can license to third parties", + "reduction of legal period for cause of action", + "responsible and indemnify" + ], "good": [] }, "match": { "bad": [ - "they can license to third parties" - ], + "they can license to third parties" + ], "good": [] }, - "class": "E" + "class": false }, "tumblr.com": { "score": 0, "all": { "bad": [ - "sets third-party cookies and/or ads", - "keep a license even after you close your account" - ], + "sets third-party cookies and/or ads", + "keep a license even after you close your account" + ], "good": [ - "archives provided", - "third parties are bound by confidentiality obligations", - "they state that you own your data" - ] + "archives provided", + "third parties are bound by confidentiality obligations", + "they state that you own your data" + ] }, "match": { "bad": [], @@ -156,27 +156,55 @@ "score": -65, "all": { "bad": [ - "they can delete your account without prior notice and without a reason", - "class action waiver", - "personal data is given to third parties", - "defend, indemnify, hold harmless; survives termination" - ], + "they can delete your account without prior notice and without a reason", + "class action waiver", + "personal data is given to third parties", + "defend, indemnify, hold harmless; survives termination" + ], "good": [ - "pseudonyms allowed", - "you can leave at any time", - "personal data is not sold", - "you can request access and deletion of personal data", - "user is notified a month or more in advance" - ] + "pseudonyms allowed", + "you can leave at any time", + "personal data is not sold", + "you can request access and deletion of personal data", + "user is notified a month or more in advance" + ] }, "match": { "bad": [ - "personal data is given to third parties" - ], + "personal data is given to third parties" + ], "good": [ - "personal data is not sold", - "you can request access and deletion of personal data" - ] + "personal data is not sold", + "you can request access and deletion of personal data" + ] + }, + "class": false + }, + "store.steampowered.com": { + "score": -65, + "all": { + "bad": [ + "they can delete your account without prior notice and without a reason", + "class action waiver", + "personal data is given to third parties", + "defend, indemnify, hold harmless; survives termination" + ], + "good": [ + "pseudonyms allowed", + "you can leave at any time", + "personal data is not sold", + "you can request access and deletion of personal data", + "user is notified a month or more in advance" + ] + }, + "match": { + "bad": [ + "personal data is given to third parties" + ], + "good": [ + "personal data is not sold", + "you can request access and deletion of personal data" + ] }, "class": false }, @@ -184,25 +212,27 @@ "score": 10, "all": { "bad": [ - "no promise to inform/notify", - "they can delete your account without prior notice and without a reason", - "no quality guarantee", - "no quality guarantee", - "third parties may be involved in operating the service", - "personal data is given to third parties" - ], + "no promise to inform/notify", + "they can delete your account without prior notice and without a reason", + "Spotify may transfer and process your data to somewhere outside of your country-bad-50", + "You grant perpetual license to anything you publish-bad-80", + "no quality guarantee", + "no quality guarantee", + "third parties may be involved in operating the service", + "personal data is given to third parties" + ], "good": [ - "you can leave at any time", - "info given about what personal data they collect", - "info given about intended use of your information", - "info given about risk of publishing your info online", - "they educate you about the risks" - ] + "you can leave at any time", + "info given about what personal data they collect", + "info given about intended use of your information", + "info given about risk of publishing your info online", + "they educate you about the risks" + ] }, "match": { "bad": [ - "personal data is given to third parties" - ], + "personal data is given to third parties" + ], "good": [] }, "class": false @@ -211,23 +241,23 @@ "score": 20, "all": { "bad": [ - "may sell your data in merger", - "responsible and indemnify", - "third-party cookies, but with opt-out instructions" - ], + "may sell your data in merger", + "responsible and indemnify", + "third-party cookies, but with opt-out instructions" + ], "good": [ - "user is notified a month or more in advance", - "your personal data is used for limited purposes", - "you have control over licensing options", - "easy to read", - "you can leave at any time", - "pseudonyms allowed" - ] + "user is notified a month or more in advance", + "your personal data is used for limited purposes", + "you have control over licensing options", + "easy to read", + "you can leave at any time", + "pseudonyms allowed" + ] }, "match": { "bad": [ - "may sell your data in merger" - ], + "may sell your data in merger" + ], "good": [] }, "class": "B" @@ -237,8 +267,8 @@ "all": { "bad": [], "good": [ - "logs are deleted after two weeks" - ] + "logs are deleted after two weeks" + ] }, "match": { "bad": [], @@ -250,9 +280,9 @@ "score": 0, "all": { "bad": [ - "you may not express negative opinions about them", - "user needs to check tosback.org" - ], + "you may not express negative opinions about them", + "user needs to check tosback.org" + ], "good": [] }, "match": { @@ -266,10 +296,10 @@ "all": { "bad": [], "good": [ - "you can get your data back", - "you can leave at any time", - "you have control over licensing options" - ] + "you can get your data back", + "you can leave at any time", + "you have control over licensing options" + ] }, "match": { "bad": [], @@ -293,17 +323,17 @@ "score": -25, "all": { "bad": [ - "user needs to check tosback.org" - ], + "user needs to check tosback.org" + ], "good": [ - "personal data is not sold" - ] + "personal data is not sold" + ] }, "match": { "bad": [], "good": [ - "personal data is not sold" - ] + "personal data is not sold" + ] }, "class": false }, @@ -311,26 +341,26 @@ "score": -20, "all": { "bad": [ - "they can delete your account without prior notice and without a reason", - "class action waiver", - "targeted third-party advertising", - "no promise to inform/notify", - "sets third-party cookies and/or ads", - "no liability for unauthorized access", - "user needs to check tosback.org" - ], + "they can delete your account without prior notice and without a reason", + "class action waiver", + "targeted third-party advertising", + "no promise to inform/notify", + "sets third-party cookies and/or ads", + "no liability for unauthorized access", + "user needs to check tosback.org" + ], "good": [ - "easy to read", - "you can request access and deletion of personal data" - ] + "easy to read", + "you can request access and deletion of personal data" + ] }, "match": { "bad": [ - "targeted third-party advertising" - ], + "targeted third-party advertising" + ], "good": [ - "you can request access and deletion of personal data" - ] + "you can request access and deletion of personal data" + ] }, "class": false }, @@ -338,20 +368,20 @@ "score": 60, "all": { "bad": [ - "tracks you on other websites", - "your data may be stored anywhere in the world", - "no promise to inform/notify", - "class action waiver", - "user needs to check tosback.org" - ], + "tracks you on other websites", + "your data may be stored anywhere in the world", + "no promise to inform/notify", + "class action waiver", + "user needs to check tosback.org" + ], "good": [ - "personalized ads are opt-out" - ] + "personalized ads are opt-out" + ] }, "match": { "bad": [ - "tracks you on other websites" - ], + "tracks you on other websites" + ], "good": [] }, "class": false @@ -360,12 +390,12 @@ "score": 0, "all": { "bad": [ - "class action waiver", - "very broad" - ], + "class action waiver", + "very broad" + ], "good": [ - "user is notified a week or more in advance" - ] + "user is notified a week or more in advance" + ] }, "match": { "bad": [], @@ -377,8 +407,8 @@ "score": 0, "all": { "bad": [ - "user needs to check tosback.org" - ], + "user needs to check tosback.org" + ], "good": [] }, "match": { @@ -392,8 +422,8 @@ "all": { "bad": [], "good": [ - "you publish under a free license, not a bilateral one" - ] + "you publish under a free license, not a bilateral one" + ] }, "match": { "bad": [], @@ -429,8 +459,8 @@ "score": 0, "all": { "bad": [ - "broader than necessary" - ], + "broader than necessary" + ], "good": [] }, "match": { @@ -443,17 +473,17 @@ "score": 20, "all": { "bad": [ - "no promise to inform/notify", - "no pricing info given before you sign up", - "may sell your data in merger", - "your use is throttled" - ], + "no promise to inform/notify", + "no pricing info given before you sign up", + "may sell your data in merger", + "your use is throttled" + ], "good": [] }, "match": { "bad": [ - "may sell your data in merger" - ], + "may sell your data in merger" + ], "good": [] }, "class": false @@ -462,26 +492,26 @@ "score": 220, "all": { "bad": [ - "your content stays licensed", - "tracks you on other websites", - "third-party access without a warrant", - "they can use your content for all their existing and future services", - "they may stop providing the service at any time", - "logs are kept forever" - ], + "your content stays licensed", + "tracks you on other websites", + "third-party access without a warrant", + "they can use your content for all their existing and future services", + "they may stop providing the service at any time", + "logs are kept forever" + ], "good": [ - "user is notified a week or more in advance", - "they provide a way to export your data", - "archives provided", - "limited for purpose across broad platform" - ] + "user is notified a week or more in advance", + "they provide a way to export your data", + "archives provided", + "limited for purpose across broad platform" + ] }, "match": { "bad": [ - "tracks you on other websites", - "they can use your content for all their existing and future services", - "logs are kept forever" - ], + "tracks you on other websites", + "they can use your content for all their existing and future services", + "logs are kept forever" + ], "good": [] }, "class": "C" @@ -490,17 +520,17 @@ "score": 0, "all": { "bad": [ - "user needs to check tosback.org", - "defend, indemnify, hold harmless", - "they can delete your account without prior notice and without a reason", - "pseudonym not allowed (not because of user-to-user trust)" - ], + "user needs to check tosback.org", + "defend, indemnify, hold harmless", + "they can delete your account without prior notice and without a reason", + "pseudonym not allowed (not because of user-to-user trust)" + ], "good": [ - "info given about security practices", - "will notify before merger", - "you publish under a free license, not a bilateral one", - "your personal data is used for limited purposes" - ] + "info given about security practices", + "will notify before merger", + "you publish under a free license, not a bilateral one", + "your personal data is used for limited purposes" + ] }, "match": { "bad": [], @@ -512,8 +542,8 @@ "score": 0, "all": { "bad": [ - "user needs to check tosback.org" - ], + "user needs to check tosback.org" + ], "good": [] }, "match": { @@ -527,10 +557,10 @@ "all": { "bad": [], "good": [ - "you can choose the copyright license", - "limited for purpose of same service", - "you can choose with whom you share content" - ] + "you can choose the copyright license", + "limited for purpose of same service", + "you can choose with whom you share content" + ] }, "match": { "bad": [], @@ -542,8 +572,8 @@ "score": 0, "all": { "bad": [ - "sets third-party cookies and/or ads" - ], + "sets third-party cookies and/or ads" + ], "good": [] }, "match": { @@ -556,23 +586,22 @@ "score": 100, "all": { "bad": [ - "tracks you on other websites", - "the (mobile) app of this service requires very broad permissions at install-time", - "your data is used for many purposes", - "very broad", - "pseudonym not allowed (not because of user-to-user trust)", - "many third parties are involved in operating the service" - ], + "tracks you on other websites", + "your data is used for many purposes", + "very broad", + "pseudonym not allowed (not because of user-to-user trust)", + "many third parties are involved in operating the service" + ], "good": [ - "user feedback is invited", - "they state that you own your data" - ] + "user feedback is invited", + "they state that you own your data" + ] }, "match": { "bad": [ - "tracks you on other websites", - "your data is used for many purposes" - ], + "tracks you on other websites", + "your data is used for many purposes" + ], "good": [] }, "class": false @@ -606,14 +635,94 @@ "all": { "bad": [], "good": [ - "no tracking" - ] + "no tracking" + ] + }, + "match": { + "bad": [], + "good": [ + "no tracking" + ] + }, + "class": "A" + }, + "donttrack.us": { + "score": -100, + "all": { + "bad": [], + "good": [ + "no tracking" + ] + }, + "match": { + "bad": [], + "good": [ + "no tracking" + ] + }, + "class": "A" + }, + "spreadprivacy.com": { + "score": -100, + "all": { + "bad": [], + "good": [ + "no tracking" + ] + }, + "match": { + "bad": [], + "good": [ + "no tracking" + ] + }, + "class": "A" + }, + "duckduckhack.com": { + "score": -100, + "all": { + "bad": [], + "good": [ + "no tracking" + ] + }, + "match": { + "bad": [], + "good": [ + "no tracking" + ] + }, + "class": "A" + }, + "privatebrowsingmyths.com": { + "score": -100, + "all": { + "bad": [], + "good": [ + "no tracking" + ] + }, + "match": { + "bad": [], + "good": [ + "no tracking" + ] + }, + "class": "A" + }, + "duck.co": { + "score": -100, + "all": { + "bad": [], + "good": [ + "no tracking" + ] }, "match": { "bad": [], "good": [ - "no tracking" - ] + "no tracking" + ] }, "class": "A" }, @@ -633,11 +742,11 @@ "score": 0, "all": { "bad": [ - "user needs to check tosback.org" - ], + "user needs to check tosback.org" + ], "good": [ - "they will help you react to others infringing on your copyright" - ] + "they will help you react to others infringing on your copyright" + ] }, "match": { "bad": [], @@ -649,19 +758,19 @@ "score": 20, "all": { "bad": [ - "only for your individual and non-commercial use", - "sets third-party cookies and/or ads", - "may sell your data in merger", - "broad license including right to distribute through any media" - ], + "only for your individual and non-commercial use", + "sets third-party cookies and/or ads", + "may sell your data in merger", + "broad license including right to distribute through any media" + ], "good": [ - "third parties are bound by confidentiality obligations" - ] + "third parties are bound by confidentiality obligations" + ] }, "match": { "bad": [ - "may sell your data in merger" - ], + "may sell your data in merger" + ], "good": [] }, "class": "D" @@ -670,21 +779,21 @@ "score": 20, "all": { "bad": [ - "keep a license even after you close your account", - "they can delete your account without prior notice and without a reason", - "they become the owner of ideas you give them", - "user needs to check tosback.org", - "third-party cookies, but with opt-out instructions", - "broader than necessary", - "your content stays licensed", - "may sell your data in merger" - ], + "keep a license even after you close your account", + "they can delete your account without prior notice and without a reason", + "they become the owner of ideas you give them", + "user needs to check tosback.org", + "third-party cookies, but with opt-out instructions", + "broader than necessary", + "your content stays licensed", + "may sell your data in merger" + ], "good": [] }, "match": { "bad": [ - "may sell your data in merger" - ], + "may sell your data in merger" + ], "good": [] }, "class": false @@ -693,25 +802,25 @@ "score": 20, "all": { "bad": [ - "sets third-party cookies and/or ads", - "no liability for unauthorized access", - "user needs to check tosback.org", - "may sell your data in merger", - "defend, indemnify, hold harmless" - ], + "sets third-party cookies and/or ads", + "no liability for unauthorized access", + "user needs to check tosback.org", + "may sell your data in merger", + "defend, indemnify, hold harmless" + ], "good": [ - "refund policy", - "they provide a way to export your data", - "you publish under a free license, not a bilateral one", - "limited for purpose of same service", - "will warn about maintenance", - "they give 30 days notice before closing your account" - ] + "refund policy", + "they provide a way to export your data", + "you publish under a free license, not a bilateral one", + "limited for purpose of same service", + "will warn about maintenance", + "they give 30 days notice before closing your account" + ] }, "match": { "bad": [ - "may sell your data in merger" - ], + "may sell your data in merger" + ], "good": [] }, "class": "B" @@ -756,8 +865,8 @@ "score": 0, "all": { "bad": [ - "user needs to check tosback.org" - ], + "user needs to check tosback.org" + ], "good": [] }, "match": { @@ -770,17 +879,17 @@ "score": 0, "all": { "bad": [ - "defend, indemnify, hold harmless", - "you may not scrape", - "user needs to rely on tosback.org" - ], + "defend, indemnify, hold harmless", + "you may not scrape", + "user needs to rely on tosback.org" + ], "good": [ - "archives provided", - "user feedback is invited", - "easy to read", - "you can delete your content", - "pseudonyms allowed" - ] + "archives provided", + "user feedback is invited", + "easy to read", + "you can delete your content", + "pseudonyms allowed" + ] }, "match": { "bad": [], @@ -792,19 +901,19 @@ "score": 110, "all": { "bad": [ - "may sell your data in merger", - "tracks you on other websites", - "targeted third-party advertising", - "user needs to check tosback.org" - ], + "may sell your data in merger", + "tracks you on other websites", + "targeted third-party advertising", + "user needs to check tosback.org" + ], "good": [] }, "match": { "bad": [ - "may sell your data in merger", - "tracks you on other websites", - "targeted third-party advertising" - ], + "may sell your data in merger", + "tracks you on other websites", + "targeted third-party advertising" + ], "good": [] }, "class": false @@ -813,15 +922,15 @@ "score": 0, "all": { "bad": [ - "class action waiver", - "broader than necessary", - "they can delete your account without prior notice and without a reason", - "responsible and indemnify" - ], + "class action waiver", + "broader than necessary", + "they can delete your account without prior notice and without a reason", + "responsible and indemnify" + ], "good": [ - "pseudonyms allowed", - "easy to read" - ] + "pseudonyms allowed", + "easy to read" + ] }, "match": { "bad": [], From 89bcb480513d4d5f18da565cd2b977aef13de8d9 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Thu, 18 Jan 2018 12:55:09 +0000 Subject: [PATCH 08/16] Allow files such as pdfs to be downloaded --- .../com/duckduckgo/app/browser/BrowserActivity.kt | 12 ++++++++++++ app/src/main/res/values/strings.xml | 1 + 2 files changed, 13 insertions(+) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index 18f871535018..20ca61fae751 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -17,6 +17,8 @@ package com.duckduckgo.app.browser import android.annotation.SuppressLint +import android.app.DownloadManager +import android.app.DownloadManager.Request.* import android.arch.lifecycle.Observer import android.arch.lifecycle.ViewModelProviders import android.content.Context @@ -31,6 +33,7 @@ import android.view.View import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.webkit.WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE import android.widget.TextView +import android.widget.Toast import com.duckduckgo.app.bookmarks.ui.BookmarksActivity import com.duckduckgo.app.browser.omnibar.OnBackKeyListener import com.duckduckgo.app.global.DuckDuckGoActivity @@ -223,6 +226,15 @@ class BrowserActivity : DuckDuckGoActivity() { setSupportZoom(true) } + webView.setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> + val request = DownloadManager.Request(Uri.parse(url)) + request.allowScanningByMediaScanner() + request.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED ) + val manager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + manager.enqueue(request) + Toast.makeText(applicationContext, getString(R.string.webviewDownload), Toast.LENGTH_LONG).show() + } + webView.setOnTouchListener { _, _ -> focusDummy.requestFocus() false diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 425353e87c0f..2283cb06f95b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ Refresh Back Forward + Downloading file Search or type URL Clear search input No compatible app installed From 0b122bc61a5c0ed01782e4a1d7528bcb10cadf10 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Thu, 18 Jan 2018 14:22:44 +0000 Subject: [PATCH 09/16] Tracking improvements * Skip trusted site evaluation * Get url for resource interceptions directly from webview * Add url check to privacy monitor updates * Improve network matching by matching on name (as same network may appear in two categories) * Improve logging and tidy up https upgrade order * Remove gradle coverage config to fix kotlin unit test introspection --- app/build.gradle | 1 - .../app/browser/BrowserViewModelTest.kt | 4 +- .../privacymonitor/model/TrustedSitesTest.kt | 35 ++++++++++++++++ .../trackerdetection/TrackerDetectorTest.kt | 9 ++-- .../app/browser/BrowserViewModel.kt | 12 ++++-- .../app/browser/BrowserWebViewClient.kt | 41 +++++++++++++++---- .../app/browser/WebViewClientListener.kt | 2 +- .../app/privacymonitor/model/TrustedSites.kt | 38 +++++++++++++++++ .../app/trackerdetection/TrackerDetector.kt | 11 +++-- .../layout/content_onboarding_no_trace.xml | 2 +- 10 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/privacymonitor/model/TrustedSitesTest.kt create mode 100644 app/src/main/java/com/duckduckgo/app/privacymonitor/model/TrustedSites.kt diff --git a/app/build.gradle b/app/build.gradle index f9a39ecd90bf..56e3894f56a7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,7 +32,6 @@ android { } buildTypes { debug { - testCoverageEnabled = true } release { minifyEnabled false diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index 9e33eb3f7a65..f07048c5b274 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -153,7 +153,7 @@ class BrowserViewModelTest { } @Test - fun whenTrackerDetectedThenNetworkLeaderbardUpdated() { + fun whenTrackerDetectedThenNetworkLeaderboardUpdated() { testee.trackerDetected(TrackingEvent("http://www.example.com", "http://www.tracker.com/tracker.js", TrackerNetwork("Network1", "www.tracker.com"), false)) assertNotNull(lastNetworkLeaderboardEntry) assertEquals(lastNetworkLeaderboardEntry!!.domainVisited, "www.example.com") @@ -290,7 +290,7 @@ class BrowserViewModelTest { @Test fun whenTrackerDetectedThenPrivacyGradeIsUpdated() { testee.urlChanged("https://example.com") - testee.trackerDetected(TrackingEvent("", "", null, false)) + testee.trackerDetected(TrackingEvent("https://example.com", "", null, false)) assertEquals(PrivacyGrade.C, testee.privacyGrade.value) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/privacymonitor/model/TrustedSitesTest.kt b/app/src/androidTest/java/com/duckduckgo/app/privacymonitor/model/TrustedSitesTest.kt new file mode 100644 index 000000000000..483e8422432a --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/privacymonitor/model/TrustedSitesTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.privacymonitor.model + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + + +class TrustedSitesTest { + + @Test + fun whenSiteIsInTrustedListThenIsTrustedIsTrue() { + assertTrue(TrustedSites.isTrusted("https://www.duckduckgo.com")) + } + + @Test + fun whenSiteIsNotInTrustedListThenIsTrustedIsFalse() { + assertFalse(TrustedSites.isTrusted("https://www.example.com")) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorTest.kt b/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorTest.kt index 9e25e685ff01..39463055e20e 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorTest.kt @@ -157,6 +157,7 @@ class TrackerDetectorTest { fun whenUrlIsNetworkOfDocumentThenEvaluateReturnsNull() { val networks = arrayListOf(DisconnectTracker("example.com", "", network, "http://thirdparty.com/")) networkTrackers.updateData(networks) + trackerDetector.addClient(alwaysMatchingClient(EASYLIST)) assertNull(trackerDetector.evaluate("http://thirdparty.com/update.js", "http://example.com/index.com", resourceType)) } @@ -164,16 +165,18 @@ class TrackerDetectorTest { fun whenDocumentIsNetworkOfUrlThenEvaluateReturnsNull() { val networks = arrayListOf(DisconnectTracker("thirdparty.com", "", network, "http://example.com")) networkTrackers.updateData(networks) + trackerDetector.addClient(alwaysMatchingClient(EASYLIST)) assertNull(trackerDetector.evaluate("http://thirdparty.com/update.js", "http://example.com/index.com", resourceType)) } @Test - fun whenUrlSharesSameNetworkAsDocumentThenEvaluateReturnsNull() { + fun whenUrlSharesSameNetworkNameAsDocumentThenEvaluateReturnsNull() { val networks = arrayListOf( - DisconnectTracker("thirdparty.com", "", network, "http://network.com"), - DisconnectTracker("example.com", "", network, "http://network.com") + DisconnectTracker("thirdparty.com", "Social", network, "http://network.com"), + DisconnectTracker("example.com", "Advertising", network, "http://network.com") ) networkTrackers.updateData(networks) + trackerDetector.addClient(alwaysMatchingClient(EASYLIST)) assertNull(trackerDetector.evaluate("http://thirdparty.com/update.js", "http://example.com/index.com", resourceType)) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 717484ade6a3..27e8c6099542 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -188,7 +188,9 @@ class BrowserViewModel( } override fun trackerDetected(event: TrackingEvent) { - updateSiteMonitor(event) + if (event.documentUrl == siteMonitor?.url) { + updateSiteMonitor(event) + } updateNetworkLeaderboard(event) } @@ -203,9 +205,11 @@ class BrowserViewModel( networkLeaderboardDao.insert(NetworkLeaderboardEntry(networkName, domainVisited)) } - override fun pageHasHttpResources() { - siteMonitor?.hasHttpResources = true - onSiteMonitorChanged() + override fun pageHasHttpResources(page: String?) { + if (page == siteMonitor?.url) { + siteMonitor?.hasHttpResources = true + onSiteMonitorChanged() + } } private fun onSiteMonitorChanged() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index 3273c869df00..33c217712d09 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.browser import android.graphics.Bitmap import android.net.Uri +import android.support.annotation.AnyThread import android.support.annotation.WorkerThread import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse @@ -25,9 +26,11 @@ import android.webkit.WebView import android.webkit.WebViewClient import com.duckduckgo.app.global.isHttp import com.duckduckgo.app.httpsupgrade.HttpsUpgrader +import com.duckduckgo.app.privacymonitor.model.TrustedSites import com.duckduckgo.app.trackerdetection.TrackerDetector import com.duckduckgo.app.trackerdetection.model.ResourceType import timber.log.Timber +import java.util.concurrent.CountDownLatch import javax.inject.Inject @@ -39,7 +42,6 @@ class BrowserWebViewClient @Inject constructor( ) : WebViewClient() { var webViewClientListener: WebViewClientListener? = null - private var currentUrl: String? = null /** @@ -82,7 +84,6 @@ class BrowserWebViewClient @Inject constructor( } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { - currentUrl = url webViewClientListener?.loadingStarted() webViewClientListener?.urlChanged(url) } @@ -93,19 +94,25 @@ class BrowserWebViewClient @Inject constructor( @WorkerThread override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { - Timber.v("Intercepting resource ${request.url}") - - if (request.url != null && request.url.isHttp) { - webViewClientListener?.pageHasHttpResources() - } + Timber.v("Intercepting resource ${request.url} on page ${view.urlFromAnyThread()}}") if (shouldUpgrade(request)) { val newUri = httpsUpgrader.upgrade(request.url) - view.post({ view.loadUrl(newUri.toString()) }) + view.post { view.loadUrl(newUri.toString()) } return WebResourceResponse(null, null, null) } - if (shouldBlock(request, currentUrl)) { + val documentUrl = view.urlFromAnyThread() ?: return null + + if (TrustedSites.isTrusted(documentUrl)) { + return null + } + + if (request.url != null && request.url.isHttp) { + webViewClientListener?.pageHasHttpResources(documentUrl) + } + + if (shouldBlock(request, documentUrl)) { return WebResourceResponse(null, null, null) } @@ -137,4 +144,20 @@ class BrowserWebViewClient @Inject constructor( return true } + /** + * Access WebView.url from any thread. If you are on the main thread it is more efficient to use + * WebView.url directly. + */ + @AnyThread + private fun WebView.urlFromAnyThread(): String? { + val latch = CountDownLatch(1) + var safeUrl: String? = null + post { + safeUrl = url + latch.countDown() + } + latch.await() + return safeUrl + } + } diff --git a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt index 701e15b796fa..b526995c29dd 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt @@ -24,7 +24,7 @@ interface WebViewClientListener { fun progressChanged(newProgress: Int) fun urlChanged(url: String?) fun trackerDetected(event: TrackingEvent) - fun pageHasHttpResources() + fun pageHasHttpResources(page: String?) fun sendEmailRequested(emailAddress: String) fun sendSmsRequested(telephoneNumber: String) diff --git a/app/src/main/java/com/duckduckgo/app/privacymonitor/model/TrustedSites.kt b/app/src/main/java/com/duckduckgo/app/privacymonitor/model/TrustedSites.kt new file mode 100644 index 000000000000..ec8dc90d1f11 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/privacymonitor/model/TrustedSites.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.privacymonitor.model + +import com.duckduckgo.app.global.UriString + +class TrustedSites { + + companion object { + + private val trusted = listOf( + "duckduckgo.com", + "donttrack.us", + "spreadprivacy.com", + "duckduckhack.com", + "privatebrowsingmyths.com", + "duck.co" + ) + + fun isTrusted(url: String): Boolean { + return trusted.any { UriString.sameOrSubdomain(url, it) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDetector.kt b/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDetector.kt index 07820496176d..99b0d4deb109 100644 --- a/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDetector.kt @@ -48,21 +48,20 @@ class TrackerDetector @Inject constructor(private val networkTrackers: TrackerNe val matches = clients.any { it.matches(url, documentUrl, resourceType) } if (matches) { - val matchText = if (matches) "WAS" else "was not" - Timber.v("$documentUrl resource $url $matchText identified as a tracker") + Timber.v("$documentUrl resource $url WAS identified as a tracker") return TrackingEvent(documentUrl, url, networkTrackers.network(url), settings.privacyOn) } Timber.v("$documentUrl resource $url was not identified as a tracker") - return null } private fun firstParty(firstUrl: String, secondUrl: String): Boolean = - sameOrSubdomain(firstUrl, secondUrl) || sameOrSubdomain(secondUrl, firstUrl) || sameNetwork(firstUrl, secondUrl) + sameOrSubdomain(firstUrl, secondUrl) || sameOrSubdomain(secondUrl, firstUrl) || sameNetworkName(firstUrl, secondUrl) + + private fun sameNetworkName(firstUrl: String, secondUrl: String): Boolean = + networkTrackers.network(firstUrl) != null && networkTrackers.network(firstUrl)?.name == networkTrackers.network(secondUrl)?.name - private fun sameNetwork(firstUrl: String, secondUrl: String): Boolean = - networkTrackers.network(firstUrl) != null && networkTrackers.network(firstUrl) == networkTrackers.network(secondUrl) val clientCount get() = clients.count() } \ No newline at end of file diff --git a/app/src/main/res/layout/content_onboarding_no_trace.xml b/app/src/main/res/layout/content_onboarding_no_trace.xml index 43ad83b588f6..0de7141d284a 100644 --- a/app/src/main/res/layout/content_onboarding_no_trace.xml +++ b/app/src/main/res/layout/content_onboarding_no_trace.xml @@ -22,7 +22,7 @@ android:layout_height="match_parent" android:paddingEnd="44dp" android:paddingStart="44dp" - tools:background="@color/skyBlue" + tools:background="@color/powderBlue" tools:context="com.duckduckgo.app.onboarding.ui.OnboardingActivity"> Date: Thu, 18 Jan 2018 15:53:37 +0000 Subject: [PATCH 10/16] Removing "fullSensor" to give user back control over rotation locking (#119) --- app/src/main/AndroidManifest.xml | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f25cb8495f99..2e2c8395bf7b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,8 +21,7 @@ + android:label="@string/appName"> @@ -32,8 +31,7 @@ + android:launchMode="singleTask"> @@ -59,51 +57,44 @@ android:name=".BrowserActivity" android:configChanges="keyboardHidden|orientation|screenSize" android:launchMode="singleTask" - android:parentActivityName="com.duckduckgo.app.home.HomeActivity" - android:screenOrientation="fullSensor" /> + android:parentActivityName="com.duckduckgo.app.home.HomeActivity" /> + android:parentActivityName=".BrowserActivity" /> + android:parentActivityName="com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardActivity" /> + android:parentActivityName="com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardActivity" /> + android:parentActivityName="com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardActivity" /> + android:parentActivityName=".BrowserActivity" /> + android:parentActivityName=".BrowserActivity" /> Date: Thu, 18 Jan 2018 17:56:09 +0000 Subject: [PATCH 11/16] tracker whitelist (#121) --- .../TrackerDetectorListTest.kt | 34 ++++++++++++++----- .../app/job/AppConfigurationDownloader.kt | 2 ++ .../duckduckgo/app/trackerdetection/Client.kt | 18 +++++++--- .../app/trackerdetection/TrackerDataLoader.kt | 1 + .../app/trackerdetection/TrackerDetector.kt | 8 ++++- .../api/TrackerDataDownloader.kt | 9 +++-- .../api/TrackerListService.kt | 4 +++ 7 files changed, 59 insertions(+), 17 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorListTest.kt b/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorListTest.kt index cc79e449b285..13ae4a6496db 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorListTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorListTest.kt @@ -19,8 +19,7 @@ package com.duckduckgo.app.trackerdetection import android.support.test.runner.AndroidJUnit4 import com.duckduckgo.app.privacymonitor.store.PrivacySettingsStore -import com.duckduckgo.app.trackerdetection.Client.ClientName.EASYLIST -import com.duckduckgo.app.trackerdetection.Client.ClientName.EASYPRIVACY +import com.duckduckgo.app.trackerdetection.Client.ClientName.* import com.duckduckgo.app.trackerdetection.model.ResourceType import com.duckduckgo.app.trackerdetection.model.TrackerNetworks import com.duckduckgo.app.trackerdetection.model.TrackingEvent @@ -41,7 +40,8 @@ class TrackerDetectorListTest { private val resourceType = ResourceType.UNKNOWN } - private lateinit var testee: TrackerDetector + private lateinit var blockingOnlyTestee: TrackerDetector + private lateinit var testeeWithWhitelist: TrackerDetector private lateinit var settingStore: PrivacySettingsStore @@ -49,31 +49,47 @@ class TrackerDetectorListTest { fun before() { val easylistAdblock = adblockClient(EASYLIST, "binary/easylist_sample") val easyprivacyAdblock = adblockClient(EASYPRIVACY, "binary/easyprivacy_sample") + + // re-using blocking sample as a whitelist to hammer the point home + val trackersWhitelistAdblocks = adblockClient(TRACKERSWHITELIST, "binary/easylist_sample") + settingStore = mock() whenever(settingStore.privacyOn).thenReturn(true) - testee = TrackerDetector(TrackerNetworks(), settingStore) - testee.addClient(easyprivacyAdblock) - testee.addClient(easylistAdblock) + + blockingOnlyTestee = TrackerDetector(TrackerNetworks(), settingStore) + blockingOnlyTestee.addClient(easyprivacyAdblock) + blockingOnlyTestee.addClient(easylistAdblock) + + testeeWithWhitelist = TrackerDetector(TrackerNetworks(), settingStore) + testeeWithWhitelist.addClient(trackersWhitelistAdblocks) + testeeWithWhitelist.addClient(easyprivacyAdblock) + testeeWithWhitelist.addClient(easylistAdblock) + } + + @Test + fun whenUrlIsInWhitelistThenEvaluateReturnsNull() { + val url = "http://imasdk.googleapis.com/js/sdkloader/ima3.js" + assertNull(testeeWithWhitelist.evaluate(url, documentUrl, resourceType)) } @Test fun whenUrlIsInEasyListThenEvaluateReturnsTrackingEvent() { val url = "http://imasdk.googleapis.com/js/sdkloader/ima3.js" val expected = TrackingEvent(documentUrl, url, null, true) - assertEquals(expected, testee.evaluate(url, documentUrl, resourceType)) + assertEquals(expected, blockingOnlyTestee.evaluate(url, documentUrl, resourceType)) } @Test fun whenUrlIsInEasyPrivacyListThenEvaluateReturnsTrackingEvent() { val url = "http://cdn.tagcommander.com/1705/tc_catalog.css" val expected = TrackingEvent(documentUrl, url, null, true) - assertEquals(expected, testee.evaluate(url, documentUrl, resourceType)) + assertEquals(expected, blockingOnlyTestee.evaluate(url, documentUrl, resourceType)) } @Test fun whenUrlIsNotInAnyTrackerListsThenEvaluateReturnsNull() { val url = "https://duckduckgo.com/index.html" - assertNull(testee.evaluate(url, documentUrl, resourceType)) + assertNull(blockingOnlyTestee.evaluate(url, documentUrl, resourceType)) } private fun adblockClient(name: Client.ClientName, dataFile: String): Client { diff --git a/app/src/main/java/com/duckduckgo/app/job/AppConfigurationDownloader.kt b/app/src/main/java/com/duckduckgo/app/job/AppConfigurationDownloader.kt index 74592aee9d34..dfa219a13139 100644 --- a/app/src/main/java/com/duckduckgo/app/job/AppConfigurationDownloader.kt +++ b/app/src/main/java/com/duckduckgo/app/job/AppConfigurationDownloader.kt @@ -34,12 +34,14 @@ class AppConfigurationDownloader @Inject constructor( fun downloadTask(): Completable { val easyListDownload = trackerDataDownloader.downloadList(Client.ClientName.EASYLIST) val easyPrivacyDownload = trackerDataDownloader.downloadList(Client.ClientName.EASYPRIVACY) + val trackersWhitelist = trackerDataDownloader.downloadList(Client.ClientName.TRACKERSWHITELIST) val disconnectDownload = trackerDataDownloader.downloadList(Client.ClientName.DISCONNECT) val httpsUpgradeDownload = httpsUpgradeListDownloader.downloadList() return Completable.merge(listOf( easyListDownload.subscribeOn(Schedulers.io()), easyPrivacyDownload.subscribeOn(Schedulers.io()), + trackersWhitelist.subscribeOn(Schedulers.io()), disconnectDownload.subscribeOn(Schedulers.io()), httpsUpgradeDownload.subscribeOn(Schedulers.io()) )).doOnComplete { diff --git a/app/src/main/java/com/duckduckgo/app/trackerdetection/Client.kt b/app/src/main/java/com/duckduckgo/app/trackerdetection/Client.kt index 33fa027951b0..976b1643c918 100644 --- a/app/src/main/java/com/duckduckgo/app/trackerdetection/Client.kt +++ b/app/src/main/java/com/duckduckgo/app/trackerdetection/Client.kt @@ -20,10 +20,20 @@ import com.duckduckgo.app.trackerdetection.model.ResourceType interface Client { - enum class ClientName { - EASYLIST, - EASYPRIVACY, - DISCONNECT + enum class ClientType { + + BLOCKING, + WHITELIST + + } + + enum class ClientName(val type: ClientType) { + + EASYLIST(ClientType.BLOCKING), + EASYPRIVACY(ClientType.BLOCKING), + TRACKERSWHITELIST(ClientType.WHITELIST), + DISCONNECT(ClientType.BLOCKING) + } val name: ClientName diff --git a/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDataLoader.kt b/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDataLoader.kt index 25aa6ef48c76..14110137fd8c 100644 --- a/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDataLoader.kt +++ b/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDataLoader.kt @@ -37,6 +37,7 @@ class TrackerDataLoader @Inject constructor( // these are stored to disk, then fed to the C++ adblock module loadAdblockData(Client.ClientName.EASYLIST) loadAdblockData(Client.ClientName.EASYPRIVACY) + loadAdblockData(Client.ClientName.TRACKERSWHITELIST) // stored in DB, then read into memory loadDisconnectData() diff --git a/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDetector.kt b/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDetector.kt index 99b0d4deb109..080eb179a914 100644 --- a/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDetector.kt @@ -41,12 +41,18 @@ class TrackerDetector @Inject constructor(private val networkTrackers: TrackerNe fun evaluate(url: String, documentUrl: String, resourceType: ResourceType): TrackingEvent? { + val whitelisted = clients.any { it.name.type == Client.ClientType.WHITELIST && it.matches(url, documentUrl, resourceType) } + if (whitelisted) { + Timber.v("$documentUrl resource $url is whitelisted") + return null + } + if (firstParty(url, documentUrl)) { Timber.v("$url is a first party url") return null } - val matches = clients.any { it.matches(url, documentUrl, resourceType) } + val matches = clients.any { it.name.type == Client.ClientType.BLOCKING && it.matches(url, documentUrl, resourceType) } if (matches) { Timber.v("$documentUrl resource $url WAS identified as a tracker") return TrackingEvent(documentUrl, url, networkTrackers.network(url), settings.privacyOn) diff --git a/app/src/main/java/com/duckduckgo/app/trackerdetection/api/TrackerDataDownloader.kt b/app/src/main/java/com/duckduckgo/app/trackerdetection/api/TrackerDataDownloader.kt index fc58eb22f8d2..2039a38eecb2 100644 --- a/app/src/main/java/com/duckduckgo/app/trackerdetection/api/TrackerDataDownloader.kt +++ b/app/src/main/java/com/duckduckgo/app/trackerdetection/api/TrackerDataDownloader.kt @@ -24,6 +24,8 @@ import com.duckduckgo.app.trackerdetection.TrackerDataLoader import com.duckduckgo.app.trackerdetection.db.TrackerDataDao import com.duckduckgo.app.trackerdetection.store.TrackerDataStore import io.reactivex.Completable +import okhttp3.ResponseBody +import retrofit2.Call import timber.log.Timber import java.io.IOException import javax.inject.Inject @@ -39,7 +41,8 @@ class TrackerDataDownloader @Inject constructor( return when (clientName) { DISCONNECT -> disconnectDownload() - EASYLIST, EASYPRIVACY -> easyDownload(clientName) + EASYLIST, EASYPRIVACY -> easyDownload(clientName, { trackerListService.list(it.name.toLowerCase()) }) + TRACKERSWHITELIST -> easyDownload(clientName, { trackerListService.trackersWhitelist() }) } } @@ -68,11 +71,11 @@ class TrackerDataDownloader @Inject constructor( } } - private fun easyDownload(clientName: Client.ClientName): Completable { + private fun easyDownload(clientName: Client.ClientName, callFactory: (clientName: Client.ClientName) -> Call): Completable { return Completable.fromAction { Timber.i("Downloading ${clientName.name} data") - val call = trackerListService.list(clientName.name.toLowerCase()) + val call = callFactory(clientName) val response = call.execute() if (response.isCached && trackerDataStore.hasData(clientName)) { diff --git a/app/src/main/java/com/duckduckgo/app/trackerdetection/api/TrackerListService.kt b/app/src/main/java/com/duckduckgo/app/trackerdetection/api/TrackerListService.kt index ee77a9f46cc6..347d5e538008 100644 --- a/app/src/main/java/com/duckduckgo/app/trackerdetection/api/TrackerListService.kt +++ b/app/src/main/java/com/duckduckgo/app/trackerdetection/api/TrackerListService.kt @@ -26,6 +26,10 @@ interface TrackerListService { @GET("/contentblocking.js") fun list(@Query("l") list: String): Call + @GET("/contentblocking/trackers-whitelist.txt") + fun trackersWhitelist(): Call + @GET("/contentblocking.js?l=disconnect") fun disconnect(): Call + } From bb7d802234fe8f56075b35c6d5a0f10ee30e7aef Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Thu, 18 Jan 2018 17:59:10 +0000 Subject: [PATCH 12/16] Feature/autocomplete (#117) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add network definitions for autocomplete call * Add network security config defintion to enable DEBUG-only charles use * Add RxRelay as I'll be using a publish subject * Implementation of autocomplete - does not yet update the UI with results * Declare api class as `open` to allow it to be easily mocked Implicitly opening classes only works on pure JVM tests; not instrumentation tests. Choice here is either to add an otherwise useless interface, or declare as open. * Tidy up view model tests • make it more obvious what is mocked and what isn't • make BrowserViewModel constructor call use named params for easier reading * Mostly functional but ugly autocomplete suggestions; work in progress * Adding placeholder globe image - this needs replaced with real asset * Handling clicking of autosuggestions * Add "no suggestions" recycler item * Update asset placeholder for globe with proper asset * Wire autocomplete suggestions UI up to omnibar and data store * Add some tests for auto complete suggestions * This appears to be missing from our build.gradle, and suggested on docs * Add explanatory comment and remove noisy logging * Inhibit sending autocomplete suggestions when user has opted out * Use `@+id` instead of `@id` to help tools render layout * Update globe and search assets to have consistent sizing * Reduce fetches to autocomplete which should also solve a small UI glitch After viewing a website, if you start a new query, briefly you might see the autocomplete result from the current URL. This commit is part of that fix. --- app/build.gradle | 4 + .../app/browser/BrowserViewModelTest.kt | 111 ++++++++++++------ .../app/settings/SettingsViewModelTest.kt | 7 +- app/src/main/AndroidManifest.xml | 1 + .../app/autocomplete/api/AutoCompleteApi.kt | 49 ++++++++ .../autocomplete/api/AutoCompleteService.kt | 30 +++++ .../duckduckgo/app/browser/BrowserActivity.kt | 39 +++++- .../app/browser/BrowserViewModel.kt | 66 ++++++++++- .../autoComplete/BrowserAutoCompleteModule.kt | 32 +++++ .../BrowserAutoCompleteSuggestionsAdapter.kt | 111 ++++++++++++++++++ .../com/duckduckgo/app/di/AppComponent.kt | 4 +- .../com/duckduckgo/app/di/NetworkModule.kt | 5 + .../duckduckgo/app/global/ViewModelFactory.kt | 25 +++- .../app/settings/SettingsActivity.kt | 2 + .../app/settings/SettingsViewModel.kt | 20 +++- .../app/settings/db/SettingsDataStore.kt | 45 +++++++ .../main/res/drawable/ic_add_white_24dp.xml | 25 ++++ .../main/res/drawable/ic_globe_white_24dp.xml | 26 ++++ .../res/drawable/ic_search_white_24dp.xml | 10 +- app/src/main/res/layout/activity_browser.xml | 12 ++ app/src/main/res/layout/content_settings.xml | 7 ++ .../item_autocomplete_no_suggestions.xml | 35 ++++++ .../layout/item_autocomplete_suggestion.xml | 69 +++++++++++ app/src/main/res/values/strings.xml | 5 + app/src/main/res/values/styles.xml | 15 +++ .../main/res/xml/network_security_config.xml | 25 ++++ 26 files changed, 728 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApi.kt create mode 100644 app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteService.kt create mode 100644 app/src/main/java/com/duckduckgo/app/browser/autoComplete/BrowserAutoCompleteModule.kt create mode 100644 app/src/main/java/com/duckduckgo/app/browser/autoComplete/BrowserAutoCompleteSuggestionsAdapter.kt create mode 100644 app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt create mode 100644 app/src/main/res/drawable/ic_add_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_globe_white_24dp.xml create mode 100644 app/src/main/res/layout/item_autocomplete_no_suggestions.xml create mode 100644 app/src/main/res/layout/item_autocomplete_suggestion.xml create mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/app/build.gradle b/app/build.gradle index 56e3894f56a7..2a465ec5d4dd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -95,6 +95,9 @@ dependencies { implementation "com.google.dagger:dagger-android-support:$dagger" releaseImplementation 'com.faendir:acra:4.10.0' + // RxRelay + implementation "com.jakewharton.rxrelay2:rxrelay:2.0.0" + // Anko compile "org.jetbrains.anko:anko-commons:$ankoVersion" compile "org.jetbrains.anko:anko-design:$ankoVersion" @@ -109,6 +112,7 @@ dependencies { implementation "android.arch.persistence.room:runtime:$architectureComponents" kapt "android.arch.persistence.room:compiler:$architectureComponents" testImplementation "android.arch.persistence.room:testing:$architectureComponents" + androidTestImplementation "android.arch.persistence.room:testing:$architectureComponents" // Dagger kapt "com.google.dagger:dagger-android-processor:$dagger" diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index f07048c5b274..518a8f14ae77 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -23,6 +23,7 @@ import android.arch.lifecycle.Observer import android.arch.persistence.room.Room import android.net.Uri import android.support.test.InstrumentationRegistry +import com.duckduckgo.app.autocomplete.api.AutoCompleteApi import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.duckduckgo.app.bookmarks.ui.BookmarksActivity @@ -40,10 +41,12 @@ import com.duckduckgo.app.privacymonitor.store.PrivacyMonitorRepository import com.duckduckgo.app.privacymonitor.store.TermsOfServiceStore import com.duckduckgo.app.settings.db.AppConfigurationDao import com.duckduckgo.app.settings.db.AppConfigurationEntity +import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.trackerdetection.model.TrackerNetwork import com.duckduckgo.app.trackerdetection.model.TrackerNetworks import com.duckduckgo.app.trackerdetection.model.TrackingEvent -import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever import org.junit.After import org.junit.Assert.* import org.junit.Before @@ -51,8 +54,10 @@ import org.junit.Rule import org.junit.Test import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers +import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations class BrowserViewModelTest { @@ -73,13 +78,27 @@ class BrowserViewModelTest { return MutableLiveData>() } } - private lateinit var queryObserver: Observer - private lateinit var navigationObserver: Observer - private lateinit var termsOfServiceStore: TermsOfServiceStore - private lateinit var mockStringResolver: StringResolver + + @Mock + lateinit var mockQueryObserver: Observer + + @Mock + lateinit var mockNavigationObserver: Observer + + @Mock + lateinit var mockTermsOfServiceStore: TermsOfServiceStore + + @Mock + lateinit var mockSettingsStore: SettingsDataStore + + @Mock + lateinit var mockAutoCompleteApi: AutoCompleteApi + + @Mock + private lateinit var bookmarksDao: BookmarksDao + private lateinit var db: AppDatabase private lateinit var appConfigurationDao: AppConfigurationDao - private lateinit var bookmarksDao: BookmarksDao private val testOmnibarConverter: OmnibarEntryConverter = object : OmnibarEntryConverter { override fun convertUri(input: String): String = "duckduckgo.com" @@ -91,45 +110,43 @@ class BrowserViewModelTest { @Before fun before() { + MockitoAnnotations.initMocks(this) + db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), AppDatabase::class.java) .allowMainThreadQueries() .build() appConfigurationDao = db.appConfigurationDao() - mockStringResolver = mock() - queryObserver = mock() - navigationObserver = mock() - termsOfServiceStore = mock() - bookmarksDao = mock() - testee = BrowserViewModel( - testOmnibarConverter, - DuckDuckGoUrlDetector(), - termsOfServiceStore, - TrackerNetworks(), - PrivacyMonitorRepository(), - testStringResolver, - testNetworkLeaderboardDao, - bookmarksDao, - appConfigurationDao) - - testee.url.observeForever(queryObserver) - testee.command.observeForever(navigationObserver) + queryUrlConverter = testOmnibarConverter, + duckDuckGoUrlDetector = DuckDuckGoUrlDetector(), + termsOfServiceStore = mockTermsOfServiceStore, + trackerNetworks = TrackerNetworks(), + privacyMonitorRepository = PrivacyMonitorRepository(), + stringResolver = testStringResolver, + networkLeaderboardDao = testNetworkLeaderboardDao, + autoCompleteApi = mockAutoCompleteApi, + appSettingsPreferencesStore = mockSettingsStore, + bookmarksDao = bookmarksDao, + appConfigurationDao = appConfigurationDao) + + testee.url.observeForever(mockQueryObserver) + testee.command.observeForever(mockNavigationObserver) } @After fun after() { testee.onCleared() db.close() - testee.url.removeObserver(queryObserver) - testee.command.removeObserver(navigationObserver) + testee.url.removeObserver(mockQueryObserver) + testee.command.removeObserver(mockNavigationObserver) } @Test fun whenBookmarksResultCodeIsOpenUrlThenNavigate() { testee.receivedBookmarksResult(BookmarksActivity.OPEN_URL_RESULT_CODE, "www.example.com") val captor: ArgumentCaptor = ArgumentCaptor.forClass(Command::class.java) - verify(navigationObserver).onChanged(captor.capture()) + verify(mockNavigationObserver).onChanged(captor.capture()) assertNotNull(captor.value) assertTrue(captor.value is Navigate) } @@ -163,19 +180,19 @@ class BrowserViewModelTest { @Test fun whenEmptyInputQueryThenNoQueryMadeAvailableToActivity() { testee.onUserSubmittedQuery("") - verify(queryObserver, never()).onChanged(ArgumentMatchers.anyString()) + verify(mockQueryObserver, never()).onChanged(ArgumentMatchers.anyString()) } @Test fun whenBlankInputQueryThenNoQueryMadeAvailableToActivity() { testee.onUserSubmittedQuery(" ") - verify(queryObserver, never()).onChanged(ArgumentMatchers.anyString()) + verify(mockQueryObserver, never()).onChanged(ArgumentMatchers.anyString()) } @Test fun whenNonEmptyInputThenQueryMadeAvailableToActivity() { testee.onUserSubmittedQuery("foo") - verify(queryObserver).onChanged(ArgumentMatchers.anyString()) + verify(mockQueryObserver).onChanged(ArgumentMatchers.anyString()) } @Test @@ -235,7 +252,7 @@ class BrowserViewModelTest { fun whenSharedTextReceivedThenNavigationTriggered() { testee.onSharedTextReceived("http://example.com") val captor: ArgumentCaptor = ArgumentCaptor.forClass(Command::class.java) - verify(navigationObserver).onChanged(captor.capture()) + verify(mockNavigationObserver).onChanged(captor.capture()) assertNotNull(captor.value) assertTrue(captor.value is Navigate) } @@ -255,13 +272,13 @@ class BrowserViewModelTest { @Test fun whenUserDismissesKeyboardBeforeBrowserShownThenShouldNavigateToLandingPage() { testee.userDismissedKeyboard() - verify(navigationObserver).onChanged(ArgumentMatchers.any(LandingPage::class.java)) + verify(mockNavigationObserver).onChanged(ArgumentMatchers.any(LandingPage::class.java)) } @Test fun whenUserDismissesKeyboardAfterBrowserShownThenShouldNotNavigateToLandingPage() { testee.urlChanged("") - verify(navigationObserver, never()).onChanged(ArgumentMatchers.any(LandingPage::class.java)) + verify(mockNavigationObserver, never()).onChanged(ArgumentMatchers.any(LandingPage::class.java)) } @Test @@ -349,4 +366,32 @@ class BrowserViewModelTest { testee.onOmnibarInputStateChanged("", true) assertFalse(testee.viewState.value!!.showFireButton) } + + @Test + fun whenEnteringQueryWithAutoCompleteEnabledThenAutoCompleteSuggestionsShown() { + doReturn(true).whenever(mockSettingsStore).autoCompleteSuggestionsEnabled + testee.onOmnibarInputStateChanged("foo", true) + assertTrue(testee.viewState.value!!.showAutoCompleteSuggestions) + } + + @Test + fun whenEnteringQueryWithAutoCompleteDisabledThenAutoCompleteSuggestionsNotShown() { + doReturn(false).whenever(mockSettingsStore).autoCompleteSuggestionsEnabled + testee.onOmnibarInputStateChanged("foo", true) + assertFalse(testee.viewState.value!!.showAutoCompleteSuggestions) + } + + @Test + fun whenEnteringEmptyQueryWithAutoCompleteEnabledThenAutoCompleteSuggestionsNotShown() { + doReturn(true).whenever(mockSettingsStore).autoCompleteSuggestionsEnabled + testee.onOmnibarInputStateChanged("", true) + assertFalse(testee.viewState.value!!.showAutoCompleteSuggestions) + } + + @Test + fun whenEnteringEmptyQueryWithAutoCompleteDisabledThenAutoCompleteSuggestionsNotShown() { + doReturn(false).whenever(mockSettingsStore).autoCompleteSuggestionsEnabled + testee.onOmnibarInputStateChanged("", true) + assertFalse(testee.viewState.value!!.showAutoCompleteSuggestions) + } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt index de84226fb325..d94a67e70749 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt @@ -27,6 +27,7 @@ import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.AndroidStringResolver import com.duckduckgo.app.global.StringResolver import com.duckduckgo.app.settings.SettingsViewModel.Command +import com.duckduckgo.app.settings.db.SettingsDataStore import com.nhaarman.mockito_kotlin.KArgumentCaptor import com.nhaarman.mockito_kotlin.argumentCaptor import com.nhaarman.mockito_kotlin.verify @@ -52,6 +53,10 @@ class SettingsViewModelTest { @Mock private lateinit var commandObserver: Observer + + @Mock + private lateinit var mockAppSettingsDataStore: SettingsDataStore + private lateinit var commandCaptor: KArgumentCaptor @Before @@ -64,7 +69,7 @@ class SettingsViewModelTest { commandCaptor = argumentCaptor() - testee = SettingsViewModel(stringResolver) + testee = SettingsViewModel(stringResolver, mockAppSettingsDataStore) testee.command.observeForever(commandObserver) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2e2c8395bf7b..ccea69196802 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ android:label="@string/appName" android:supportsRtl="true" android:theme="@style/AppTheme" + android:networkSecurityConfig="@xml/network_security_config" tools:ignore="GoogleAppIndexingWarning"> { + + if (query.isBlank()) { + return Observable.just(AutoCompleteResult(query, emptyList())) + } + + return autoCompleteService.autoComplete(query) + .flatMapIterable { it -> it } + .map { AutoCompleteSuggestion(it.phrase, queryUrlConverter.isWebUrl(it.phrase)) } + .toList() + .onErrorReturn { emptyList() } + .map { AutoCompleteResult(query = query, suggestions = it) } + .toObservable() + } + + data class AutoCompleteResult(val query: String, + val suggestions: List) + + data class AutoCompleteSuggestion(val phrase: String, val isUrl: Boolean) + +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteService.kt b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteService.kt new file mode 100644 index 000000000000..7b43c2dd4167 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteService.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.autocomplete.api + +import io.reactivex.Observable +import retrofit2.http.GET +import retrofit2.http.Query + + +interface AutoCompleteService { + + @GET("https://duckduckgo.com/ac/") + fun autoComplete(@Query("q") query: String) : Observable> +} + +data class AutoCompleteServiceRawResult(val phrase:String) \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index 20ca61fae751..cdc75bdb8f0e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -25,6 +25,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle +import android.support.v7.widget.LinearLayoutManager import android.text.Editable import android.view.KeyEvent.KEYCODE_ENTER import android.view.Menu @@ -35,6 +36,7 @@ import android.webkit.WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE import android.widget.TextView import android.widget.Toast import com.duckduckgo.app.bookmarks.ui.BookmarksActivity +import com.duckduckgo.app.browser.autoComplete.BrowserAutoCompleteSuggestionsAdapter import com.duckduckgo.app.browser.omnibar.OnBackKeyListener import com.duckduckgo.app.global.DuckDuckGoActivity import com.duckduckgo.app.global.ViewModelFactory @@ -47,6 +49,7 @@ import kotlinx.android.synthetic.main.activity_browser.* import org.jetbrains.anko.doAsync import org.jetbrains.anko.toast import org.jetbrains.anko.uiThread +import timber.log.Timber import javax.inject.Inject class BrowserActivity : DuckDuckGoActivity() { @@ -55,6 +58,8 @@ class BrowserActivity : DuckDuckGoActivity() { @Inject lateinit var webChromeClient: BrowserChromeClient @Inject lateinit var viewModelFactory: ViewModelFactory + private lateinit var autoCompleteSuggestionsAdapter: BrowserAutoCompleteSuggestionsAdapter + private var acceptingRenderUpdates = true private var addBookmarkMenuItem: MenuItem? = null @@ -115,12 +120,26 @@ class BrowserActivity : DuckDuckGoActivity() { configureWebView() configureOmnibarTextInput() configureDummyViewTouchHandler() + configureAutoComplete() if (savedInstanceState == null) { consumeSharedTextExtra() } } + private fun configureAutoComplete() { + autoCompleteSuggestionsList.layoutManager = LinearLayoutManager(this) + autoCompleteSuggestionsAdapter = BrowserAutoCompleteSuggestionsAdapter( + immediateSearchClickListener = { + userEnteredQuery(it.phrase) + }, + editableSearchClickListener = { + viewModel.onUserSelectedToEditQuery(it.phrase) + } + ) + autoCompleteSuggestionsList.adapter = autoCompleteSuggestionsAdapter + } + private fun consumeSharedTextExtra() { val sharedText = intent.getStringExtra(SHARED_TEXT_EXTRA) if (sharedText != null) { @@ -130,6 +149,8 @@ class BrowserActivity : DuckDuckGoActivity() { private fun render(viewState: BrowserViewModel.ViewState) { + Timber.v("Rendering view state: $viewState") + if (!acceptingRenderUpdates) return when (viewState.browserShowing) { @@ -145,6 +166,9 @@ class BrowserActivity : DuckDuckGoActivity() { addBookmarkMenuItem?.setEnabled(viewState.canAddBookmarks) if (shouldUpdateOmnibarTextInput(viewState, viewState.omnibarText)) { omnibarTextInput.setText(viewState.omnibarText) + + // ensures caret sits at the end of the query + omnibarTextInput.post { omnibarTextInput.setSelection(omnibarTextInput.text.length) } appBarLayout.setExpanded(true, true) } @@ -157,6 +181,15 @@ class BrowserActivity : DuckDuckGoActivity() { privacyGradeMenu?.isVisible = viewState.showPrivacyGrade fireMenu?.isVisible = viewState.showFireButton + + when (viewState.showAutoCompleteSuggestions) { + false -> autoCompleteSuggestionsList.gone() + true -> { + autoCompleteSuggestionsList.show() + val results = viewState.autoCompleteSearchResults.suggestions + autoCompleteSuggestionsAdapter.updateData(results) + } + } } private fun showClearButton() { @@ -205,10 +238,10 @@ class BrowserActivity : DuckDuckGoActivity() { clearOmnibarInputButton.setOnClickListener { omnibarTextInput.setText("") } } - private fun userEnteredQuery() { - viewModel.onUserSubmittedQuery(omnibarTextInput.text.toString()) + private fun userEnteredQuery(query: String) { omnibarTextInput.hideKeyboard() focusDummy.requestFocus() + viewModel.onUserSubmittedQuery(query) } @SuppressLint("SetJavaScriptEnabled") @@ -244,7 +277,7 @@ class BrowserActivity : DuckDuckGoActivity() { omnibarTextInput.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, keyEvent -> if (actionId == IME_ACTION_DONE || keyEvent.keyCode == KEYCODE_ENTER) { - userEnteredQuery() + userEnteredQuery(omnibarTextInput.text.toString()) return@OnEditorActionListener true } false diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 27e8c6099542..2a7e2b474394 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -24,6 +24,8 @@ import android.support.annotation.AnyThread import android.support.annotation.VisibleForTesting import android.support.annotation.WorkerThread import com.duckduckgo.app.about.AboutDuckDuckGoActivity.Companion.RESULT_CODE_LOAD_ABOUT_DDG_WEB_PAGE +import com.duckduckgo.app.autocomplete.api.AutoCompleteApi +import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteResult import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.duckduckgo.app.bookmarks.ui.BookmarksActivity.Companion.OPEN_URL_RESULT_CODE @@ -44,9 +46,14 @@ import com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardActivity.Companion.R import com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardActivity.Companion.TOSDR_RESULT_CODE import com.duckduckgo.app.settings.db.AppConfigurationDao import com.duckduckgo.app.settings.db.AppConfigurationEntity +import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.trackerdetection.model.TrackerNetworks import com.duckduckgo.app.trackerdetection.model.TrackingEvent +import com.jakewharton.rxrelay2.PublishRelay +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers import timber.log.Timber +import java.util.concurrent.TimeUnit class BrowserViewModel( private val queryUrlConverter: OmnibarEntryConverter, @@ -57,6 +64,8 @@ class BrowserViewModel( private val stringResolver: StringResolver, private val networkLeaderboardDao: NetworkLeaderboardDao, private val bookmarksDao: BookmarksDao, + private val autoCompleteApi: AutoCompleteApi, + private val appSettingsPreferencesStore: SettingsDataStore, appConfigurationDao: AppConfigurationDao) : WebViewClientListener, ViewModel() { data class ViewState( @@ -68,7 +77,9 @@ class BrowserViewModel( val showClearButton: Boolean = false, val showPrivacyGrade: Boolean = false, val showFireButton: Boolean = true, - val canAddBookmarks: Boolean = false + val canAddBookmarks: Boolean = false, + val showAutoCompleteSuggestions: Boolean = false, + val autoCompleteSearchResults: AutoCompleteResult = AutoCompleteResult("", emptyList()) ) sealed class Command { @@ -96,6 +107,8 @@ class BrowserViewModel( } private val appConfigurationObservable = appConfigurationDao.appConfigurationStatus() + private val autoCompletePublishSubject = PublishRelay.create() + private var siteMonitor: SiteMonitor? = null private var appConfigurationDownloaded = false @@ -103,8 +116,28 @@ class BrowserViewModel( viewState.value = ViewState(canAddBookmarks = false) privacyMonitorRepository.privacyMonitor = MutableLiveData() appConfigurationObservable.observeForever(appConfigurationObserver) + + configureAutoComplete() } + private fun configureAutoComplete() { + autoCompletePublishSubject + .debounce(300, TimeUnit.MILLISECONDS) + .distinctUntilChanged() + .switchMap { autoCompleteApi.autoComplete(it) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ result -> + onAutoCompleteResultReceived(result) + }, { t: Throwable? -> Timber.w(t, "Failed to get search results") }) + } + + private fun onAutoCompleteResultReceived(result: AutoCompleteResult) { + val results = result.suggestions.take(6) + viewState.value = currentViewState().copy(autoCompleteSearchResults = AutoCompleteResult(result.query, results)) + } + + @VisibleForTesting public override fun onCleared() { super.onCleared() @@ -121,7 +154,11 @@ class BrowserViewModel( return } url.value = buildUrl(input) - viewState.value = currentViewState().copy(showClearButton = false, omnibarText = input) + viewState.value = currentViewState().copy( + showClearButton = false, + omnibarText = input, + showAutoCompleteSuggestions = false, + autoCompleteSearchResults = AutoCompleteResult("", emptyList())) } private fun buildUrl(input: String): String { @@ -221,12 +258,32 @@ class BrowserViewModel( fun onOmnibarInputStateChanged(query: String, hasFocus: Boolean) { val showClearButton = hasFocus && query.isNotEmpty() + + val currentViewState = currentViewState() + + // determine if empty list to be shown, or existing search results + val autoCompleteSearchResults = if (query.isBlank()) { + AutoCompleteResult(query, emptyList()) + } else { + currentViewState.autoCompleteSearchResults + } + + val hasQueryChanged = (currentViewState.omnibarText != query) + val autoCompleteSuggestionsEnabled = appSettingsPreferencesStore.autoCompleteSuggestionsEnabled + viewState.value = currentViewState().copy( isEditing = hasFocus, showClearButton = showClearButton, showPrivacyGrade = appConfigurationDownloaded && !hasFocus, - showFireButton = !hasFocus + showFireButton = !hasFocus, + showAutoCompleteSuggestions = hasFocus && query.isNotBlank() && hasQueryChanged && autoCompleteSuggestionsEnabled, + autoCompleteSearchResults = autoCompleteSearchResults ) + + if(hasQueryChanged && hasFocus && autoCompleteSuggestionsEnabled) { + autoCompletePublishSubject.accept(query.trim()) + } + } fun onSharedTextReceived(input: String) { @@ -282,6 +339,9 @@ class BrowserViewModel( command.value = Navigate(url) } + fun onUserSelectedToEditQuery(query: String) { + viewState.value = currentViewState().copy(isEditing = false, showAutoCompleteSuggestions = false, omnibarText = query) + } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/autoComplete/BrowserAutoCompleteModule.kt b/app/src/main/java/com/duckduckgo/app/browser/autoComplete/BrowserAutoCompleteModule.kt new file mode 100644 index 000000000000..c2aaca42b4d9 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/autoComplete/BrowserAutoCompleteModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.autoComplete + +import android.content.Context +import com.duckduckgo.app.settings.db.AppSettingsPreferencesStore +import com.duckduckgo.app.settings.db.SettingsDataStore +import dagger.Module +import dagger.Provides + + +@Module +class BrowserAutoCompleteModule { + + @Provides + fun settingsDataStore(context: Context): SettingsDataStore = AppSettingsPreferencesStore(context) + +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/autoComplete/BrowserAutoCompleteSuggestionsAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/autoComplete/BrowserAutoCompleteSuggestionsAdapter.kt new file mode 100644 index 000000000000..320131b1861c --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/autoComplete/BrowserAutoCompleteSuggestionsAdapter.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2017 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.autoComplete + +import android.support.annotation.UiThread +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.browser.autoComplete.BrowserAutoCompleteSuggestionsAdapter.AutoCompleteViewHolder +import com.duckduckgo.app.browser.autoComplete.BrowserAutoCompleteSuggestionsAdapter.AutoCompleteViewHolder.EmptySuggestionViewHolder +import com.duckduckgo.app.browser.autoComplete.BrowserAutoCompleteSuggestionsAdapter.AutoCompleteViewHolder.SuggestionViewHolder +import kotlinx.android.synthetic.main.item_autocomplete_suggestion.view.* +import javax.inject.Inject + +class BrowserAutoCompleteSuggestionsAdapter @Inject constructor( + private val immediateSearchClickListener: (AutoCompleteSuggestion) -> Unit, + private val editableSearchClickListener: (AutoCompleteSuggestion) -> Unit) : RecyclerView.Adapter() { + + private val suggestions: MutableList = ArrayList() + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AutoCompleteViewHolder { + + val inflater = LayoutInflater.from(parent.context) + + return if (suggestions.isEmpty()) { + EmptySuggestionViewHolder(inflater.inflate(R.layout.item_autocomplete_no_suggestions, parent, false)) + } else { + SuggestionViewHolder(inflater.inflate(R.layout.item_autocomplete_suggestion, parent, false)) + } + } + + override fun getItemViewType(position: Int): Int { + if(suggestions.isEmpty()) { + return EMPTY_TYPE + } + + return SUGGESTION_TYPE + } + + override fun onBindViewHolder(holder: AutoCompleteViewHolder, position: Int) { + when (holder) { + is EmptySuggestionViewHolder -> { + // nothing required + } + is SuggestionViewHolder -> { + val suggestion = suggestions[position] + holder.bind(suggestion, immediateSearchClickListener, editableSearchClickListener) + } + } + } + + override fun getItemCount(): Int { + if (suggestions.isNotEmpty()) { + return suggestions.size + } + + // if there are no suggestions, we'll use a recycler row to display "no suggestions" + return 1 + } + + @UiThread + fun updateData(newSuggestions: List) { + suggestions.clear() + suggestions.addAll(newSuggestions) + notifyDataSetChanged() + } + + sealed class AutoCompleteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + class SuggestionViewHolder(itemView: View) : AutoCompleteViewHolder(itemView) { + fun bind(item: AutoCompleteSuggestion, + immediateSearchListener: (AutoCompleteSuggestion) -> Unit, + editableSearchClickListener: (AutoCompleteSuggestion) -> Unit) = with(itemView) { + + phrase.text = item.phrase + + val phraseOrUrlImage = if (item.isUrl) R.drawable.ic_globe_white_24dp else R.drawable.ic_search_white_24dp + phraseOrUrlIndicator.setImageResource(phraseOrUrlImage) + + editQueryImage.setOnClickListener { editableSearchClickListener(item) } + setOnClickListener { immediateSearchListener(item) } + } + } + + class EmptySuggestionViewHolder(itemView: View) : AutoCompleteViewHolder(itemView) + } + + companion object { + private const val EMPTY_TYPE = 1 + private const val SUGGESTION_TYPE = 2 + } +} + diff --git a/app/src/main/java/com/duckduckgo/app/di/AppComponent.kt b/app/src/main/java/com/duckduckgo/app/di/AppComponent.kt index faba83f4384f..9f9658b82714 100644 --- a/app/src/main/java/com/duckduckgo/app/di/AppComponent.kt +++ b/app/src/main/java/com/duckduckgo/app/di/AppComponent.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.di import android.app.Application +import com.duckduckgo.app.browser.autoComplete.BrowserAutoCompleteModule import com.duckduckgo.app.global.DuckDuckGoApplication import dagger.BindsInstance import dagger.Component @@ -36,7 +37,8 @@ import javax.inject.Singleton (PrivacyModule::class), (DatabaseModule::class), (JsonModule::class), - (StringModule::class) + (StringModule::class), + (BrowserAutoCompleteModule::class) ]) interface AppComponent : AndroidInjector { diff --git a/app/src/main/java/com/duckduckgo/app/di/NetworkModule.kt b/app/src/main/java/com/duckduckgo/app/di/NetworkModule.kt index 277890bac5d6..25605c9749b6 100644 --- a/app/src/main/java/com/duckduckgo/app/di/NetworkModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/NetworkModule.kt @@ -17,6 +17,7 @@ package com.duckduckgo.app.di import android.content.Context +import com.duckduckgo.app.autocomplete.api.AutoCompleteService import com.duckduckgo.app.browser.R import com.duckduckgo.app.httpsupgrade.api.HttpsUpgradeListService import com.duckduckgo.app.trackerdetection.api.TrackerListService @@ -63,6 +64,10 @@ class NetworkModule { fun httpsUpgradeListService(retrofit: Retrofit): HttpsUpgradeListService = retrofit.create(HttpsUpgradeListService::class.java) + @Provides + fun autoCompleteService(retrofit: Retrofit): AutoCompleteService = + retrofit.create(AutoCompleteService::class.java) + companion object { private const val CACHE_SIZE: Long = 10 * 1024 * 1024 // 10MB } 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 cbefa965434c..d4430b550e34 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -18,11 +18,13 @@ package com.duckduckgo.app.global import android.arch.lifecycle.ViewModel import android.arch.lifecycle.ViewModelProvider +import com.duckduckgo.app.autocomplete.api.AutoCompleteApi import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.duckduckgo.app.bookmarks.ui.BookmarksViewModel import com.duckduckgo.app.browser.BrowserViewModel import com.duckduckgo.app.browser.DuckDuckGoUrlDetector import com.duckduckgo.app.browser.omnibar.QueryUrlConverter +import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.ui.OnboardingViewModel import com.duckduckgo.app.privacymonitor.db.NetworkLeaderboardDao @@ -34,6 +36,8 @@ import com.duckduckgo.app.privacymonitor.ui.PrivacyPracticesViewModel import com.duckduckgo.app.privacymonitor.ui.ScorecardViewModel import com.duckduckgo.app.privacymonitor.ui.TrackerNetworksViewModel import com.duckduckgo.app.settings.SettingsViewModel +import com.duckduckgo.app.settings.db.AppSettingsPreferencesStore +import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.settings.db.AppConfigurationDao import com.duckduckgo.app.trackerdetection.model.TrackerNetworks import javax.inject.Inject @@ -51,21 +55,36 @@ class ViewModelFactory @Inject constructor( private val stringResolver: StringResolver, private val appConfigurationDao: AppConfigurationDao, private val networkLeaderboardDao: NetworkLeaderboardDao, - private val bookmarksDao: BookmarksDao + private val bookmarksDao: BookmarksDao, + private val autoCompleteApi: AutoCompleteApi, + private val appSettingsPreferencesStore: SettingsDataStore ) : ViewModelProvider.NewInstanceFactory() { override fun create(modelClass: Class) = with(modelClass) { when { isAssignableFrom(OnboardingViewModel::class.java) -> OnboardingViewModel(onboaringStore) - isAssignableFrom(BrowserViewModel::class.java) -> BrowserViewModel(queryUrlConverter, duckDuckGoUrlDetector, termsOfServiceStore, trackerNetworks, privacyMonitorRepository, stringResolver, networkLeaderboardDao, bookmarksDao, appConfigurationDao) + isAssignableFrom(BrowserViewModel::class.java) -> browserViewModel() isAssignableFrom(PrivacyDashboardViewModel::class.java) -> PrivacyDashboardViewModel(privacySettingsStore, networkLeaderboardDao) isAssignableFrom(ScorecardViewModel::class.java) -> ScorecardViewModel(privacySettingsStore) isAssignableFrom(TrackerNetworksViewModel::class.java) -> TrackerNetworksViewModel() isAssignableFrom(PrivacyPracticesViewModel::class.java) -> PrivacyPracticesViewModel() - isAssignableFrom(SettingsViewModel::class.java) -> SettingsViewModel(stringResolver) + isAssignableFrom(SettingsViewModel::class.java) -> SettingsViewModel(stringResolver, appSettingsPreferencesStore) isAssignableFrom(BookmarksViewModel::class.java) -> BookmarksViewModel(bookmarksDao) else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") } } as T + + private fun browserViewModel(): ViewModel = BrowserViewModel( + queryUrlConverter = queryUrlConverter, + duckDuckGoUrlDetector = duckDuckGoUrlDetector, + termsOfServiceStore = termsOfServiceStore, + trackerNetworks = trackerNetworks, + privacyMonitorRepository = privacyMonitorRepository, + stringResolver = stringResolver, + networkLeaderboardDao = networkLeaderboardDao, + bookmarksDao = bookmarksDao, + appSettingsPreferencesStore = appSettingsPreferencesStore, + appConfigurationDao = appConfigurationDao, + autoCompleteApi = autoCompleteApi) } diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt index 1491cbb0389b..273068d15384 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt @@ -54,12 +54,14 @@ class SettingsActivity : DuckDuckGoActivity() { private fun configureUiEventHandlers() { about.setOnClickListener { startActivityForResult(AboutDuckDuckGoActivity.intent(this), REQUEST_CODE_ABOUT_DDG) } provideFeedback.setOnClickListener { viewModel.userRequestedToSendFeedback() } + autocompleteEnabledSetting.setOnClickListener { viewModel.userRequestedToChangeAutocompleteSetting(autocompleteEnabledSetting.isChecked) } } private fun observeViewModel() { viewModel.viewState.observe(this, Observer { viewState -> viewState?.let { version.text = it.version + autocompleteEnabledSetting.isChecked = it.autoCompleteSuggestionsEnabled } }) diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt index e5a2e854b4c2..0b9ea1efd8c4 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt @@ -23,13 +23,18 @@ import android.os.Build import com.duckduckgo.app.browser.BuildConfig import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.StringResolver +import com.duckduckgo.app.settings.db.SettingsDataStore +import timber.log.Timber import javax.inject.Inject -class SettingsViewModel @Inject constructor(private val stringResolver: StringResolver) : ViewModel() { +class SettingsViewModel @Inject constructor( + private val stringResolver: StringResolver, + private val settingsDataStore: SettingsDataStore) : ViewModel() { data class ViewState( val loading: Boolean = true, - val version: String = "" + val version: String = "", + val autoCompleteSuggestionsEnabled: Boolean = true ) sealed class Command { @@ -46,7 +51,10 @@ class SettingsViewModel @Inject constructor(private val stringResolver: StringRe } fun start() { - viewState.value = currentViewState.copy(loading = false, version = obtainVersion()) + val autoCompleteEnabled = settingsDataStore.autoCompleteSuggestionsEnabled + Timber.i("Is auto complete enabled? $autoCompleteEnabled") + + viewState.value = currentViewState.copy(autoCompleteSuggestionsEnabled = autoCompleteEnabled, loading = false, version = obtainVersion()) } fun userRequestedToSendFeedback() { @@ -58,6 +66,11 @@ class SettingsViewModel @Inject constructor(private val stringResolver: StringRe command.value = Command.SendEmail(Uri.parse(uri)) } + fun userRequestedToChangeAutocompleteSetting(checked: Boolean) { + Timber.i("User changed autocomplete setting, is now enabled: $checked") + settingsDataStore.autoCompleteSuggestionsEnabled = checked + } + private fun buildEmailBody(): String { return "App Version: ${obtainVersion()}\n" + "Android Version: ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})\n" + @@ -72,4 +85,5 @@ class SettingsViewModel @Inject constructor(private val stringResolver: StringRe private inline fun encode(f: () -> String): String = Uri.encode(f()) + } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt b/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt new file mode 100644 index 000000000000..232920b01554 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.settings.db + +import android.content.Context +import android.content.SharedPreferences +import javax.inject.Inject + + +interface SettingsDataStore { + var autoCompleteSuggestionsEnabled: Boolean +} + +class AppSettingsPreferencesStore @Inject constructor(private val context: Context) : SettingsDataStore { + + companion object { + val SHARED_PREFERENCES_FILENAME = "com.duckduckgo.app.settings_activity.settings" + val KEY_AUTOCOMPLETE_ENABLED = "AUTOCOMPLETE_ENABLED" + } + + override var autoCompleteSuggestionsEnabled: Boolean + get() = preferences.getBoolean(KEY_AUTOCOMPLETE_ENABLED, true) + set(enabled) { + preferences.edit() + .putBoolean(KEY_AUTOCOMPLETE_ENABLED, enabled) + .apply() + } + + private val preferences: SharedPreferences + get() = context.getSharedPreferences(SHARED_PREFERENCES_FILENAME, Context.MODE_PRIVATE) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_white_24dp.xml b/app/src/main/res/drawable/ic_add_white_24dp.xml new file mode 100644 index 000000000000..cb0b217c554b --- /dev/null +++ b/app/src/main/res/drawable/ic_add_white_24dp.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_globe_white_24dp.xml b/app/src/main/res/drawable/ic_globe_white_24dp.xml new file mode 100644 index 000000000000..b20e5f052734 --- /dev/null +++ b/app/src/main/res/drawable/ic_globe_white_24dp.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_search_white_24dp.xml b/app/src/main/res/drawable/ic_search_white_24dp.xml index 9eeb471668db..2f1e8a225936 100644 --- a/app/src/main/res/drawable/ic_search_white_24dp.xml +++ b/app/src/main/res/drawable/ic_search_white_24dp.xml @@ -15,11 +15,11 @@ --> + android:width="18dp" + android:height="18dp" + android:viewportHeight="18.0" + android:viewportWidth="18.0"> + android:pathData="M6.69,11.32a4.625,4.625 0,0 1,-4.632 -4.63A4.625,4.625 0,0 1,6.69 2.058a4.625,4.625 0,0 1,4.63 4.632,4.625 4.625,0 0,1 -4.63,4.63zM12.864,11.32h-0.813l-0.288,-0.277A6.66,6.66 0,0 0,13.38 6.69a6.69,6.69 0,1 0,-6.69 6.69,6.66 6.66,0 0,0 4.354,-1.617l0.278,0.288v0.813L16.467,18 18,16.467l-5.136,-5.146z" /> diff --git a/app/src/main/res/layout/activity_browser.xml b/app/src/main/res/layout/activity_browser.xml index f3f0e46cdd4f..fe79e4c0c7a3 100644 --- a/app/src/main/res/layout/activity_browser.xml +++ b/app/src/main/res/layout/activity_browser.xml @@ -133,6 +133,7 @@ + + + + + diff --git a/app/src/main/res/layout/content_settings.xml b/app/src/main/res/layout/content_settings.xml index e5434d579b38..a84de7bd8dfd 100644 --- a/app/src/main/res/layout/content_settings.xml +++ b/app/src/main/res/layout/content_settings.xml @@ -27,6 +27,13 @@ android:layout_height="wrap_content" android:orientation="vertical"> + + diff --git a/app/src/main/res/layout/item_autocomplete_no_suggestions.xml b/app/src/main/res/layout/item_autocomplete_no_suggestions.xml new file mode 100644 index 000000000000..531a436308ba --- /dev/null +++ b/app/src/main/res/layout/item_autocomplete_no_suggestions.xml @@ -0,0 +1,35 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_autocomplete_suggestion.xml b/app/src/main/res/layout/item_autocomplete_suggestion.xml new file mode 100644 index 000000000000..d50f4d84f789 --- /dev/null +++ b/app/src/main/res/layout/item_autocomplete_suggestion.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2283cb06f95b..74cbc0a34fde 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,6 +24,10 @@ No compatible app installed Add Bookmark + + Edit query before searching + Autcomplete Suggestions + Privacy Dashboard PRIVACY PROTECTION ENABLED @@ -110,6 +114,7 @@ DuckDuckGo Privacy Browser provides all the privacy essentials you need to protect yourself as you search and browse the web, including tracker blocking, smarter encryption, and DuckDuckGo private search.\n\ After all, the internet shouldn\'t feel so creepy, and getting the privacy you deserve online should be as simple closing the blinds. More at duckduckgo.com/about + No Suggestions Bookmarks diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ef13d25e0020..dcd517f0c496 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -125,4 +125,19 @@ @color/white + + + + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000000..aa64aba5fd72 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + \ No newline at end of file From aef05ca180fa9dfc7beff1337c3481bc64572631 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Thu, 18 Jan 2018 20:35:13 +0000 Subject: [PATCH 13/16] fixed crash caused by domain with only one part (#122) --- .../app/httpsupgrade/HttpsUpgraderTest.kt | 18 +++++++++++++++--- .../app/httpsupgrade/HttpsUpgrader.kt | 5 +++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/HttpsUpgraderTest.kt b/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/HttpsUpgraderTest.kt index 7a2435ecb630..c958cf985570 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/HttpsUpgraderTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/HttpsUpgraderTest.kt @@ -18,9 +18,7 @@ package com.duckduckgo.app.httpsupgrade import android.net.Uri import com.duckduckgo.app.httpsupgrade.db.HttpsUpgradeDomainDao -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.verify -import com.nhaarman.mockito_kotlin.whenever +import com.nhaarman.mockito_kotlin.* import org.junit.Assert.* import org.junit.Before import org.junit.Test @@ -36,6 +34,20 @@ class HttpsUpgraderTest { testee = HttpsUpgrader(mockDao) } + @Test + fun whenHostContainsTwoPartsThenUpgradeStillChecksWildcard() { + whenever(mockDao.hasDomain("macpro.localhost")).thenReturn(false) + assertFalse(testee.shouldUpgrade(Uri.parse("http://macpro.localhost"))) + verify(mockDao).hasDomain("*.localhost") + } + + @Test + fun whenHostContainsSinglePartThenUpgradeStillChecksWildcard() { + whenever(mockDao.hasDomain("localhost")).thenReturn(false) + assertFalse(testee.shouldUpgrade(Uri.parse("http://localhost"))) + verify(mockDao, times(1)).hasDomain(any()) + } + @Test fun whenMixedCaseDomainIsASubdominOfAWildCardInTheDatabaseThenShouldUpgrade() { whenever(mockDao.hasDomain("www.example.com")).thenReturn(false) diff --git a/app/src/main/java/com/duckduckgo/app/httpsupgrade/HttpsUpgrader.kt b/app/src/main/java/com/duckduckgo/app/httpsupgrade/HttpsUpgrader.kt index 47b6f3e3df11..b4ee24636b6e 100644 --- a/app/src/main/java/com/duckduckgo/app/httpsupgrade/HttpsUpgrader.kt +++ b/app/src/main/java/com/duckduckgo/app/httpsupgrade/HttpsUpgrader.kt @@ -21,6 +21,7 @@ import android.support.annotation.WorkerThread import com.duckduckgo.app.global.UrlScheme import com.duckduckgo.app.global.isHttps import com.duckduckgo.app.httpsupgrade.db.HttpsUpgradeDomainDao +import timber.log.Timber import javax.inject.Inject class HttpsUpgrader @Inject constructor(private val dao: HttpsUpgradeDomainDao) { @@ -50,8 +51,8 @@ class HttpsUpgrader @Inject constructor(private val dao: HttpsUpgradeDomainDao) } } - domains.removeAt(0) - domains.removeAt(domains.size - 1) + domains.asReversed().removeAt(0) + Timber.d("domains $domains") for (domain in domains) { if (dao.hasDomain("*$domain")) { From cb81977f999529b8423a541ce7ceb088bf23ca9a Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Thu, 18 Jan 2018 20:49:04 +0000 Subject: [PATCH 14/16] favourites and saved searches migration (#120) --- .../app/migration/LegacyMigrationTest.kt | 144 ++++++++ .../java/com/duckduckgo/app/di/StoreModule.kt | 2 +- .../app/global/DuckDuckGoApplication.kt | 15 + .../app/migration/LegacyMigration.kt | 110 ++++++ .../app/migration/legacy/LegacyDb.java | 313 ++++++++++++++++++ .../migration/legacy/LegacyDbContracts.java | 104 ++++++ .../migration/legacy/LegacyFeedObject.java | 129 ++++++++ .../app/migration/legacy/LegacyUtils.java | 69 ++++ .../app/migration/legacy/package-info.java | 26 ++ 9 files changed, 911 insertions(+), 1 deletion(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/migration/LegacyMigrationTest.kt create mode 100644 app/src/main/java/com/duckduckgo/app/migration/LegacyMigration.kt create mode 100644 app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyDb.java create mode 100644 app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyDbContracts.java create mode 100644 app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyFeedObject.java create mode 100644 app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyUtils.java create mode 100644 app/src/main/java/com/duckduckgo/app/migration/legacy/package-info.java diff --git a/app/src/androidTest/java/com/duckduckgo/app/migration/LegacyMigrationTest.kt b/app/src/androidTest/java/com/duckduckgo/app/migration/LegacyMigrationTest.kt new file mode 100644 index 000000000000..ad8a3bffcfd8 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/migration/LegacyMigrationTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.migration + +import android.arch.lifecycle.LiveData +import android.arch.persistence.room.Room +import android.content.ContentValues +import android.support.test.InstrumentationRegistry +import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.browser.DuckDuckGoRequestRewriter +import com.duckduckgo.app.browser.DuckDuckGoUrlDetector +import com.duckduckgo.app.browser.omnibar.QueryUrlConverter +import com.duckduckgo.app.global.db.AppDatabase +import com.duckduckgo.app.migration.legacy.LegacyDb +import com.duckduckgo.app.migration.legacy.LegacyDbContracts +import org.junit.After +import org.junit.Assert.* +import org.junit.Test + +class LegacyMigrationTest { + + // target context else we can't write a db file + val context = InstrumentationRegistry.getTargetContext() + val urlConverter = QueryUrlConverter(DuckDuckGoRequestRewriter(DuckDuckGoUrlDetector())) + + var appDatabase: AppDatabase = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build() + var bookmarksDao = MockBookmarksDao() + + @After + fun after() { + deleteLegacyDb() + } + + @Test + fun whenNoLegacyDbExistsThenMigrationCompletesWithZeroMigratedItems() { + + deleteLegacyDb() + + val migration = LegacyMigration(appDatabase, bookmarksDao, context, urlConverter); + + migration.start { favourites, searches -> + assertEquals(0, favourites) + assertEquals(0, searches) + } + + assertEquals(0, bookmarksDao.bookmarks.size) + + } + + @Test + fun whenLegacyDbExistsThenMigrationCompletesWithCorrectNumberOfMigratedItems() { + + populateLegacyDB() + + // migrate + val migration = LegacyMigration(appDatabase, bookmarksDao, context, urlConverter); + + migration.start { favourites, searches -> + assertEquals(1, favourites) + assertEquals(1, searches) + } + + assertEquals(2, bookmarksDao.bookmarks.size) + + migration.start { favourites, searches -> + assertEquals(0, favourites) + assertEquals(0, searches) + } + + } + + private fun deleteLegacyDb() { + context.getDatabasePath(LegacyDbContracts.DATABASE_NAME).delete() + } + + private fun populateLegacyDB() { + val db = LegacyDb(context) + + assertNotEquals(-1, db.sqLiteDB.insert(LegacyDbContracts.SAVED_SEARCH_TABLE.TABLE_NAME, null, searchEntry())) + assertNotEquals(-1, db.sqLiteDB.insert(LegacyDbContracts.FEED_TABLE.TABLE_NAME, null, favouriteEntry())) + assertNotEquals(-1, db.sqLiteDB.insert(LegacyDbContracts.FEED_TABLE.TABLE_NAME, null, notFavouriteEntry())) + + db.close() + } + + private fun notFavouriteEntry(): ContentValues { + val values = ContentValues() + values.put(LegacyDbContracts.FEED_TABLE._ID, "oops id") + values.put(LegacyDbContracts.FEED_TABLE.COLUMN_TITLE, "oops title") + values.put(LegacyDbContracts.FEED_TABLE.COLUMN_URL, "oops url") + values.put(LegacyDbContracts.FEED_TABLE.COLUMN_FAVORITE, "OOPS") + return values + } + + private fun favouriteEntry(): ContentValues { + val values = ContentValues() + values.put(LegacyDbContracts.FEED_TABLE._ID, "favourite id") + values.put(LegacyDbContracts.FEED_TABLE.COLUMN_TITLE, "favourite title") + values.put(LegacyDbContracts.FEED_TABLE.COLUMN_URL, "favourite url") + values.put(LegacyDbContracts.FEED_TABLE.COLUMN_FAVORITE, "F") + return values + } + + private fun searchEntry(): ContentValues { + val values = ContentValues() + values.put(LegacyDbContracts.SAVED_SEARCH_TABLE.COLUMN_TITLE, "search title") + values.put(LegacyDbContracts.SAVED_SEARCH_TABLE.COLUMN_QUERY, "search query") + return values + } + + class MockBookmarksDao(): BookmarksDao { + + var bookmarks = mutableListOf() + + override fun insert(bookmark: BookmarkEntity) { + bookmarks.add(bookmark) + } + + override fun bookmarks(): LiveData> { + throw UnsupportedOperationException() + } + + override fun delete(bookmark: BookmarkEntity) { + throw UnsupportedOperationException() + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt b/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt index 0b41a49bf0f5..72ed39cff5cb 100644 --- a/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt @@ -26,7 +26,7 @@ import dagger.Provides class StoreModule { @Provides - fun providesOnboaridngStore(context: Context): OnboardingStore { + fun providesOnboardingStore(context: Context): OnboardingStore { return OnboardingSharedPreferences(context) } diff --git a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt index 07f6b6b9e565..ddf4b46eb2f9 100644 --- a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt +++ b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt @@ -22,12 +22,14 @@ import android.app.Service import com.duckduckgo.app.browser.BuildConfig import com.duckduckgo.app.di.DaggerAppComponent import com.duckduckgo.app.job.AppConfigurationSyncer +import com.duckduckgo.app.migration.LegacyMigration import com.duckduckgo.app.trackerdetection.TrackerDataLoader import dagger.android.AndroidInjector import dagger.android.DispatchingAndroidInjector import dagger.android.HasActivityInjector import dagger.android.HasServiceInjector import io.reactivex.schedulers.Schedulers +import org.jetbrains.anko.doAsync import timber.log.Timber import javax.inject.Inject @@ -48,6 +50,9 @@ class DuckDuckGoApplication : HasActivityInjector, HasServiceInjector, Applicati @Inject lateinit var appConfigurationSyncer: AppConfigurationSyncer + @Inject + lateinit var migration: LegacyMigration + override fun onCreate() { super.onCreate() @@ -57,6 +62,16 @@ class DuckDuckGoApplication : HasActivityInjector, HasServiceInjector, Applicati loadTrackerData() configureDataDownloader() + + migrateLegacyDb() + } + + private fun migrateLegacyDb() { + doAsync { + migration.start { favourites, searches -> + Timber.d("Migrated $favourites favourites, $searches") + } + } } private fun loadTrackerData() { diff --git a/app/src/main/java/com/duckduckgo/app/migration/LegacyMigration.kt b/app/src/main/java/com/duckduckgo/app/migration/LegacyMigration.kt new file mode 100644 index 000000000000..9a1147b2e589 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/migration/LegacyMigration.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.migration + +import android.content.Context +import android.support.annotation.WorkerThread +import android.webkit.URLUtil +import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.browser.omnibar.QueryUrlConverter +import com.duckduckgo.app.global.db.AppDatabase +import com.duckduckgo.app.migration.legacy.LegacyDb +import com.duckduckgo.app.migration.legacy.LegacyDbContracts +import timber.log.Timber +import javax.inject.Inject + +class LegacyMigration @Inject constructor( + val database: AppDatabase, + val bookmarksDao: BookmarksDao, + val context: Context, + val queryUrlConverter: QueryUrlConverter) { + + @WorkerThread + fun start(completion: (favourites: Int, searches: Int) -> Unit) { + + LegacyDb(context.applicationContext).use { + migrate(it, completion) + } + + } + + private fun migrate(legacyDb:LegacyDb, completion: (favourites: Int, searches: Int) -> Unit) { + + var favourites = 0 + var searches = 0 + + database.runInTransaction { + favourites = migrateFavourites(legacyDb) + searches = migrateSavedSearches(legacyDb) + legacyDb.deleteAll() + } + + completion(favourites, searches) + } + + private fun migrateSavedSearches(db: LegacyDb): Int { + + var count = 0 + db.cursorSavedSearch.use { + + if (!it.moveToFirst()) { + Timber.d("No saved searches found") + return 0 + } + + val titleColumn = it.getColumnIndex(LegacyDbContracts.SAVED_SEARCH_TABLE.COLUMN_TITLE) + val queryColumn = it.getColumnIndex(LegacyDbContracts.SAVED_SEARCH_TABLE.COLUMN_QUERY) + + do { + + val title = it.getString(titleColumn) + val query = it.getString(queryColumn) + + val url = if (URLUtil.isNetworkUrl(query)) query else queryUrlConverter.convertQueryToUri(query).toString() + + bookmarksDao.insert(BookmarkEntity(title = title, url = url)) + + count += 1 + } while (it.moveToNext()) + } + + return count + } + + private fun migrateFavourites(db: LegacyDb) : Int { + val feedObjects = db.selectAll() ?: return 0 + + var count = 0 + for (feedObject in feedObjects) { + + if (!db.isSaved(feedObject.id)) { + continue + } + + val title = feedObject.title + val url = feedObject.url + + bookmarksDao.insert(BookmarkEntity(title = title, url = url)) + + count += 1 + } + + return count + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyDb.java b/app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyDb.java new file mode 100644 index 000000000000..285ea7ebf650 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyDb.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.migration.legacy; + +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.preference.PreferenceManager; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class LegacyDb implements Closeable { + + private SQLiteDatabase db; + + + public LegacyDb(Context context) { + OpenHelper openHelper = new OpenHelper(context); + this.db = openHelper.getWritableDatabase(); + } + + public void deleteAll() { + this.db.delete(LegacyDbContracts.FEED_TABLE.TABLE_NAME, null, null); + this.db.delete(LegacyDbContracts.SAVED_SEARCH_TABLE.TABLE_NAME, null, null); + } + + private LegacyFeedObject getFeedObject(Cursor c) { + final String id = c.getString(c.getColumnIndex(LegacyDbContracts.FEED_TABLE._ID)); + final String title = c.getString(c.getColumnIndex(LegacyDbContracts.FEED_TABLE.COLUMN_TITLE)); + final String description = c.getString(c.getColumnIndex(LegacyDbContracts.FEED_TABLE.COLUMN_DESCRIPTION)); + final String feed = c.getString(c.getColumnIndex(LegacyDbContracts.FEED_TABLE.COLUMN_FEED)); + final String url = c.getString(c.getColumnIndex(LegacyDbContracts.FEED_TABLE.COLUMN_URL)); + final String imageurl = c.getString(c.getColumnIndex(LegacyDbContracts.FEED_TABLE.COLUMN_IMAGE_URL)); + final String favicon = c.getString(c.getColumnIndex(LegacyDbContracts.FEED_TABLE.COLUMN_FAVICON)); + final String timestamp = c.getString(c.getColumnIndex(LegacyDbContracts.FEED_TABLE.COLUMN_TIMESTAMP)); + final String category = c.getString(c.getColumnIndex(LegacyDbContracts.FEED_TABLE.COLUMN_CATEGORY)); + final String type = c.getString(c.getColumnIndex(LegacyDbContracts.FEED_TABLE.COLUMN_TYPE)); + final String articleurl = c.getString(c.getColumnIndex(LegacyDbContracts.FEED_TABLE.COLUMN_ARTICLE_URL)); + final String hidden = c.getString(c.getColumnIndex(LegacyDbContracts.FEED_TABLE.COLUMN_HIDDEN)); + return new LegacyFeedObject(id, title, description, feed, url, imageurl, favicon, timestamp, category, type, articleurl, "", hidden); + } + + public ArrayList selectAll() { + ArrayList feeds = null; + Cursor c = null; + try { + c = this.db.query(LegacyDbContracts.FEED_TABLE.TABLE_NAME, null, "", null, null, null, null); + if (c.moveToFirst()) { + feeds = new ArrayList<>(30); + do { + feeds.add(getFeedObject(c)); + } while (c.moveToNext()); + } + } finally { + if (c != null) { + c.close(); + } + } + return feeds; + } + + public Cursor getCursorSavedSearch() { + return this.db.query(LegacyDbContracts.SAVED_SEARCH_TABLE.TABLE_NAME, null, null, null, null, null, LegacyDbContracts.SAVED_SEARCH_TABLE._ID + " DESC"); + } + + public boolean isSaved(String id) { + boolean out = false; + Cursor c = null; + try { + c = this.db.query(LegacyDbContracts.FEED_TABLE.TABLE_NAME, null, LegacyDbContracts.FEED_TABLE._ID + "=? AND " + LegacyDbContracts.FEED_TABLE.COLUMN_FAVORITE + "!='F'", new String[]{id}, null, null, null); + out = c.moveToFirst(); + } finally { + if (c != null) { + c.close(); + } + } + return out; + } + + private static class OpenHelper extends SQLiteOpenHelper { + + private final Context appContext; + + OpenHelper(Context context) { + super(context, LegacyDbContracts.DATABASE_NAME, null, LegacyDbContracts.DATABASE_VERSION); + this.appContext = context.getApplicationContext(); + } + + private void dropTables(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + LegacyDbContracts.FEED_TABLE.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + LegacyDbContracts.APP_TABLE.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + LegacyDbContracts.HISTORY_TABLE.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + LegacyDbContracts.SAVED_SEARCH_TABLE.TABLE_NAME); + } + + private void createFeedTable(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + "(" + + LegacyDbContracts.FEED_TABLE._ID + " VARCHAR(300) UNIQUE, " + + LegacyDbContracts.FEED_TABLE.COLUMN_TITLE + " VARCHAR(300), " + + LegacyDbContracts.FEED_TABLE.COLUMN_DESCRIPTION + " VARCHAR(300), " + + LegacyDbContracts.FEED_TABLE.COLUMN_FEED + " VARCHAR(300), " + + LegacyDbContracts.FEED_TABLE.COLUMN_URL + " VARCHAR(300), " + + LegacyDbContracts.FEED_TABLE.COLUMN_IMAGE_URL + " VARCHAR(300), " + + LegacyDbContracts.FEED_TABLE.COLUMN_FAVICON + " VARCHAR(300), " + + LegacyDbContracts.FEED_TABLE.COLUMN_TIMESTAMP + " VARCHAR(300), " + + LegacyDbContracts.FEED_TABLE.COLUMN_CATEGORY + " VARCHAR(300), " + + LegacyDbContracts.FEED_TABLE.COLUMN_TYPE + " VARCHAR(300), " + + LegacyDbContracts.FEED_TABLE.COLUMN_ARTICLE_URL + " VARCHAR(300), " + //+"hidden CHAR(1)" + + LegacyDbContracts.FEED_TABLE.COLUMN_HIDDEN + " CHAR(1), " + + LegacyDbContracts.FEED_TABLE.COLUMN_FAVORITE + " VARCHAR(300)" + + ")" + ); + + db.execSQL("CREATE INDEX idx_id ON " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + " (" + LegacyDbContracts.FEED_TABLE._ID + ") "); + db.execSQL("CREATE INDEX idx_idtype ON " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + " (" + LegacyDbContracts.FEED_TABLE._ID + ", " + LegacyDbContracts.FEED_TABLE.COLUMN_TYPE + ") "); + } + + private void createAppTable(SQLiteDatabase db) { + + // Generates warning/error when inline to execSQL method + String sql = "CREATE VIRTUAL TABLE " + LegacyDbContracts.APP_TABLE.TABLE_NAME + " USING FTS3 (" + + LegacyDbContracts.APP_TABLE.COLUMN_TITLE + " VARCHAR(300), " + + LegacyDbContracts.APP_TABLE.COLUMN_PACKAGE + " VARCHAR(300) " + + ")"; + + db.execSQL(sql); + } + + private void createHistoryTable(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + LegacyDbContracts.HISTORY_TABLE.TABLE_NAME + "(" + + LegacyDbContracts.HISTORY_TABLE._ID + " INTEGER PRIMARY KEY, " + + LegacyDbContracts.HISTORY_TABLE.COLUMN_TYPE + " VARCHAR(300), " + + LegacyDbContracts.HISTORY_TABLE.COLUMN_DATA + " VARCHAR(300), " + + LegacyDbContracts.HISTORY_TABLE.COLUMN_URL + " VARCHAR(300), " + + LegacyDbContracts.HISTORY_TABLE.COLUMN_EXTRA_TYPE + " VARCHAR(300), " + + LegacyDbContracts.HISTORY_TABLE.COLUMN_FEED_ID + " VARCHAR(300)" + + ")" + ); + } + + private void createSavedSearchTable(SQLiteDatabase db) { + + // Generates warning/error when inline to execSQL method + String sql = "CREATE TABLE " + LegacyDbContracts.SAVED_SEARCH_TABLE.TABLE_NAME + "(" + + LegacyDbContracts.SAVED_SEARCH_TABLE._ID + " INTEGER PRIMARY KEY, " + + LegacyDbContracts.SAVED_SEARCH_TABLE.COLUMN_TITLE + " VARCHAR(300), " + + LegacyDbContracts.SAVED_SEARCH_TABLE.COLUMN_QUERY + " VARCHAR(300) UNIQUE)"; + db.execSQL(sql); + } + + @Override + public void onCreate(SQLiteDatabase db) { + createFeedTable(db); + createAppTable(db); + createHistoryTable(db); + createSavedSearchTable(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion == 4 && newVersion >= 12) { + ContentValues contentValues = new ContentValues(); + + // shape old FEED_TABLE like the new, and rename it as FEED_TABLE_old + db.execSQL("DROP INDEX IF EXISTS idx_id"); + db.execSQL("DROP INDEX IF EXISTS idx_idtype"); + db.execSQL("ALTER TABLE " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + " RENAME TO " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + "_old"); + + dropTables(db); + onCreate(db); + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext); + + // ***** recent queries ******* + List recentQueries = LegacyUtils.loadList(sharedPreferences, "recentsearch"); + Collections.reverse(recentQueries); + for (String query : recentQueries) { + // insertRecentSearch + contentValues.clear(); + contentValues.put(LegacyDbContracts.HISTORY_TABLE.COLUMN_TYPE, "R"); + contentValues.put(LegacyDbContracts.HISTORY_TABLE.COLUMN_DATA, query); + contentValues.put(LegacyDbContracts.HISTORY_TABLE.COLUMN_URL, ""); + contentValues.put(LegacyDbContracts.HISTORY_TABLE.COLUMN_EXTRA_TYPE, ""); + contentValues.put(LegacyDbContracts.HISTORY_TABLE.COLUMN_FEED_ID, ""); + db.insert(LegacyDbContracts.HISTORY_TABLE.TABLE_NAME, null, contentValues); + } + // **************************** + + // ****** saved search ******** + Cursor c = db.query(LegacyDbContracts.FEED_TABLE.TABLE_NAME + "_old", new String[]{"url"}, LegacyDbContracts.FEED_TABLE.COLUMN_FEED + "=''", null, null, null, null); + while (c.moveToNext()) { + final String url = c.getString(0); + final String query = LegacyUtils.getQueryIfSerp(url); + if (query == null) + continue; + contentValues.clear(); + contentValues.put(LegacyDbContracts.SAVED_SEARCH_TABLE.COLUMN_QUERY, query); + db.insert(LegacyDbContracts.SAVED_SEARCH_TABLE.TABLE_NAME, null, contentValues); + } + // ***************************** + + // ***** saved feed items ***** + db.execSQL("DELETE FROM " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + "_old WHERE " + LegacyDbContracts.FEED_TABLE.COLUMN_FEED + "='' "); + db.execSQL("INSERT INTO " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + " SELECT *,'','F' FROM " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + "_old"); + db.execSQL("DROP TABLE IF EXISTS " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + "_old"); + // **************************** + + } else if (oldVersion == 12 && newVersion >= 14) { + // shape old FEED_TABLE like the new, and rename it as FEED_TABLE_old + db.execSQL("DROP INDEX IF EXISTS idx_id"); + db.execSQL("DROP INDEX IF EXISTS idx_idtype"); + db.execSQL("ALTER TABLE " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + " RENAME TO " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + "_old"); + + db.execSQL("DROP TABLE IF EXISTS " + LegacyDbContracts.FEED_TABLE.TABLE_NAME); + createFeedTable(db); + + // ***** saved feed items ***** + db.execSQL("DELETE FROM " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + "_old WHERE " + LegacyDbContracts.FEED_TABLE.COLUMN_FEED + "='' "); + db.execSQL("INSERT INTO " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + " SELECT " + + LegacyDbContracts.FEED_TABLE._ID + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_TITLE + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_DESCRIPTION + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_FEED + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_URL + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_IMAGE_URL + "," + + LegacyDbContracts.FEED_TABLE.COLUMN_FAVICON + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_TIMESTAMP + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_CATEGORY + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_TYPE + ", " + + "'' AS " + LegacyDbContracts.FEED_TABLE.COLUMN_ARTICLE_URL + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_HIDDEN + " FROM " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + "_old"); + db.execSQL("DROP TABLE IF EXISTS " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + "_old"); + // **************************** + } else if (oldVersion == 14 && newVersion >= 15) { + // shape old FEED_TABLE like the new, and rename it as FEED_TABLE_old + db.execSQL("DROP INDEX IF EXISTS idx_id"); + db.execSQL("DROP INDEX IF EXISTS idx_idtype"); + db.execSQL("ALTER TABLE " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + " RENAME TO " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + "_old"); + + db.execSQL("DROP TABLE IF EXISTS " + LegacyDbContracts.FEED_TABLE.TABLE_NAME); + createFeedTable(db); + + // ***** saved feed items ***** + db.execSQL("DELETE FROM " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + "_old WHERE " + LegacyDbContracts.FEED_TABLE.COLUMN_FEED + "='' "); + db.execSQL("INSERT INTO " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + " SELECT " + + LegacyDbContracts.FEED_TABLE._ID + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_TITLE + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_DESCRIPTION + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_FEED + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_URL + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_IMAGE_URL + "," + + LegacyDbContracts.FEED_TABLE.COLUMN_FAVICON + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_TIMESTAMP + ", " + "" + + LegacyDbContracts.FEED_TABLE.COLUMN_CATEGORY + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_TYPE + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_ARTICLE_URL + ", " + + LegacyDbContracts.FEED_TABLE.COLUMN_HIDDEN + ", " + + "'F' FROM " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + "_old"); + db.execSQL("DROP TABLE IF EXISTS " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + "_old"); + //***** set new favlue for favorite ***** + String newFavoriteValue = String.valueOf(System.currentTimeMillis()); + db.execSQL("UPDATE " + LegacyDbContracts.FEED_TABLE.TABLE_NAME + " SET " + LegacyDbContracts.FEED_TABLE.COLUMN_FAVORITE + "=" + newFavoriteValue + " WHERE " + LegacyDbContracts.FEED_TABLE.COLUMN_HIDDEN + "='F'"); + // **************************** + } else if (oldVersion == 15 && newVersion >= 16) { + db.execSQL("ALTER TABLE " + LegacyDbContracts.SAVED_SEARCH_TABLE.TABLE_NAME + " RENAME TO " + LegacyDbContracts.SAVED_SEARCH_TABLE.TABLE_NAME + "_old"); + db.execSQL("DROP TABLE IF EXISTS " + LegacyDbContracts.SAVED_SEARCH_TABLE.TABLE_NAME); + createSavedSearchTable(db); + + // Generates warning/error when inline to execSQL method + String sql = "INSERT INTO " + LegacyDbContracts.SAVED_SEARCH_TABLE.TABLE_NAME + " SELECT " + + LegacyDbContracts.SAVED_SEARCH_TABLE._ID + ", " + + LegacyDbContracts.SAVED_SEARCH_TABLE.COLUMN_QUERY + ", " + + LegacyDbContracts.SAVED_SEARCH_TABLE.COLUMN_QUERY + " FROM " + LegacyDbContracts.SAVED_SEARCH_TABLE.TABLE_NAME + "_old"; + db.execSQL(sql); + + db.execSQL("DROP TABLE IF EXISTS " + LegacyDbContracts.SAVED_SEARCH_TABLE.TABLE_NAME + "_old"); + } else { + dropTables(db); + onCreate(db); + } + } + } + + public void close() { + db.close(); + } + + public SQLiteDatabase getSQLiteDB() { + return db; + } + +} diff --git a/app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyDbContracts.java b/app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyDbContracts.java new file mode 100644 index 000000000000..6b7f14eeadad --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyDbContracts.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.migration.legacy; + +import android.provider.BaseColumns; + +public final class LegacyDbContracts { + public static final String DATABASE_NAME = "ddg.db"; + public static final int DATABASE_VERSION = 16; + + public static final class FEED_TABLE implements BaseColumns { + public static final String TABLE_NAME = "feed"; + + // THE TITLE OF THE FEED + public static final String COLUMN_TITLE = "title"; + + // DESCRIPTION OF THE FEED + public static final String COLUMN_DESCRIPTION = "description"; + + // FEED + public static final String COLUMN_FEED = "feed"; + + // URL OF THE FEED + public static final String COLUMN_URL = "url"; + + // IMAGE URL OF THE FEED + public static final String COLUMN_IMAGE_URL = "imageurl"; + + // FAVICON OF THE FEED + public static final String COLUMN_FAVICON = "favicon"; + + // TIMESTAMP OF THE FEED + public static final String COLUMN_TIMESTAMP = "timestamp"; + + // CATEGORY OF THE FEED + public static final String COLUMN_CATEGORY = "category"; + + // TYPE OF THE FEED + public static final String COLUMN_TYPE = "type"; + + // ARTICLE URL OF THE FEED + public static final String COLUMN_ARTICLE_URL = "articleurl"; + + // WHETHER FEED IS HIDDEN + public static final String COLUMN_HIDDEN = "hidden"; + + // WHETHER FEED IS FAVORITE + public static final String COLUMN_FAVORITE = "favorite"; + + } + + public static final class APP_TABLE { + public static final String TABLE_NAME = "apps"; + + // THE TITLE OF THE APP + public static final String COLUMN_TITLE = "title"; + + // PACKAGE NAME OF THE APP + public static final String COLUMN_PACKAGE = "package"; + } + + public static final class HISTORY_TABLE implements BaseColumns { + public static final String TABLE_NAME = "history"; + + // TYPE OF THE FEED + public static final String COLUMN_TYPE = "type"; + + // URL OF THE FEED + public static final String COLUMN_URL = "url"; + + // DATA OF THE FEED + public static final String COLUMN_DATA = "data"; + + // EXTRA TYPE OF THE FEED + public static final String COLUMN_EXTRA_TYPE = "extraType"; + + // ID OF THE FEED + public static final String COLUMN_FEED_ID = "feedId"; + } + + public static final class SAVED_SEARCH_TABLE implements BaseColumns { + public static final String TABLE_NAME = "saved_search"; + + // THE TITLE OF THE SEARCH + public static final String COLUMN_TITLE = "title"; + + // SEARCH QUERY + public static final String COLUMN_QUERY = "query"; + } +} diff --git a/app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyFeedObject.java b/app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyFeedObject.java new file mode 100644 index 000000000000..7bdb4736badd --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyFeedObject.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.migration.legacy; + +public class LegacyFeedObject { + + private final String feed; + private final String favicon; + private final String description; + private final String timestamp; + private final String url; + private final String title; + private final String id; + private final String category; + private final String imageUrl; + private final String type; + private final String articleUrl; + private final String html; + private final String hidden; + + + public LegacyFeedObject(String id, String title, String description, String feed, String url, String imageUrl, + String favicon, String timestamp, String category, String type, String articleUrl, String html, String hidden) { + this.id = id; + this.title = title; + this.description = description; + this.feed = feed; + this.url = url; + this.imageUrl = imageUrl; + this.favicon = favicon; + this.timestamp = timestamp; + this.category = category; + this.type = type; + this.articleUrl = articleUrl; + this.html = html; + this.hidden = hidden; + } + + @Override + public String toString() { + String string = "{"; + + string = string.concat("feed:" + this.feed + "\n"); + string = string.concat("favicon:" + this.favicon + "\n"); + string = string.concat("description:" + this.description + "\n"); + string = string.concat("timestamp:" + this.timestamp + "\n"); + string = string.concat("url:" + this.url + "\n"); + string = string.concat("title:" + this.title + "\n"); + string = string.concat("id:" + this.id + "\n"); + string = string.concat("category:" + this.category + "\n"); + string = string.concat("image: " + this.imageUrl + "\n"); + string = string.concat("type: " + this.type + "\n"); + string = string.concat("article_url:" + this.articleUrl + "\n"); + string = string.concat("html:" + this.html + "\n"); + string = string.concat("hidden: " + this.hidden + "}"); + + return string; + } + + public String getFeed() { + return feed; + } + + public String getFavicon() { + return favicon; + } + + public String getTimestamp() { + return timestamp; + } + + public String getDescription() { + return description; + } + + public String getUrl() { + return url; + } + + public String getTitle() { + return title; + } + + public String getId() { + return id; + } + + public String getCategory() { + return category; + } + + public String getImageUrl() { + return imageUrl; + } + + public String getType() { + return type; + } + + public String getHtml() { + return html; + } + + public String getArticleUrl() { + return articleUrl; + } + + public String getHidden() { + return hidden; + } + + public boolean hasPossibleReadability() { + return getArticleUrl().length() != 0; + } +} diff --git a/app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyUtils.java b/app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyUtils.java new file mode 100644 index 000000000000..3380272da6fc --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/migration/legacy/LegacyUtils.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.migration.legacy; + +import java.util.LinkedList; +import android.content.SharedPreferences; +import android.net.Uri; + +/** + * This class contains utility static methods, such as loading preferences as an array or decoding bitmaps. + */ +public final class LegacyUtils { + + public static LinkedList loadList(SharedPreferences prefs, String listName) { + int size = prefs.getInt(listName + "_size", 0); + LinkedList list = new LinkedList(); + for(int i=0;i Date: Fri, 19 Jan 2018 10:58:08 +0000 Subject: [PATCH 15/16] Add Browsing menu --- .../duckduckgo/app/browser/BrowserActivity.kt | 110 ++++++++++-------- .../app/browser/BrowserPopupMenu.kt | 62 ++++++++++ .../duckduckgo/app/global/ViewModelFactory.kt | 4 +- .../com/duckduckgo/app/home/HomeActivity.kt | 43 +++++-- .../ui/PrivacyDashboardActivity.kt | 1 - app/src/main/res/color/browser_menu_icon.xml | 5 + app/src/main/res/color/browser_menu_text.xml | 5 + .../main/res/drawable/ic_arrow_back_24px.xml | 25 ++++ .../res/drawable/ic_arrow_forward_24px.xml | 25 ++++ .../main/res/drawable/ic_bookmark_24px.xml | 25 ++++ .../res/drawable/ic_clear_browsnish_24px.xml | 25 ++++ .../main/res/drawable/ic_overflow_24px.xml | 25 ++++ app/src/main/res/layout/activity_home.xml | 1 + .../res/layout/popup_window_brower_menu.xml | 97 +++++++++++++++ .../main/res/layout/sheet_fire_clear_data.xml | 4 +- .../main/res/menu/menu_browser_activity.xml | 28 +---- app/src/main/res/menu/menu_home_activity.xml | 15 +-- app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 20 +++- 20 files changed, 427 insertions(+), 96 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/BrowserPopupMenu.kt create mode 100644 app/src/main/res/color/browser_menu_icon.xml create mode 100644 app/src/main/res/color/browser_menu_text.xml create mode 100644 app/src/main/res/drawable/ic_arrow_back_24px.xml create mode 100644 app/src/main/res/drawable/ic_arrow_forward_24px.xml create mode 100644 app/src/main/res/drawable/ic_bookmark_24px.xml create mode 100644 app/src/main/res/drawable/ic_clear_browsnish_24px.xml create mode 100644 app/src/main/res/drawable/ic_overflow_24px.xml create mode 100644 app/src/main/res/layout/popup_window_brower_menu.xml diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index cdc75bdb8f0e..092bf7a4bc2d 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -18,7 +18,7 @@ package com.duckduckgo.app.browser import android.annotation.SuppressLint import android.app.DownloadManager -import android.app.DownloadManager.Request.* +import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED import android.arch.lifecycle.Observer import android.arch.lifecycle.ViewModelProviders import android.content.Context @@ -46,22 +46,25 @@ import com.duckduckgo.app.privacymonitor.renderer.icon import com.duckduckgo.app.privacymonitor.ui.PrivacyDashboardActivity import com.duckduckgo.app.settings.SettingsActivity import kotlinx.android.synthetic.main.activity_browser.* +import kotlinx.android.synthetic.main.popup_window_brower_menu.view.* import org.jetbrains.anko.doAsync import org.jetbrains.anko.toast import org.jetbrains.anko.uiThread import timber.log.Timber import javax.inject.Inject + class BrowserActivity : DuckDuckGoActivity() { @Inject lateinit var webViewClient: BrowserWebViewClient @Inject lateinit var webChromeClient: BrowserChromeClient @Inject lateinit var viewModelFactory: ViewModelFactory + private lateinit var popupMenu: BrowserPopupMenu + private lateinit var autoCompleteSuggestionsAdapter: BrowserAutoCompleteSuggestionsAdapter private var acceptingRenderUpdates = true - private var addBookmarkMenuItem: MenuItem? = null private val viewModel: BrowserViewModel by lazy { ViewModelProviders.of(this, viewModelFactory).get(BrowserViewModel::class.java) @@ -76,6 +79,7 @@ class BrowserActivity : DuckDuckGoActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_browser) + popupMenu = BrowserPopupMenu(layoutInflater) viewModel.viewState.observe(this, Observer { it?.let { render(it) } @@ -163,7 +167,6 @@ class BrowserActivity : DuckDuckGoActivity() { false -> pageLoadingIndicator.hide() } - addBookmarkMenuItem?.setEnabled(viewState.canAddBookmarks) if (shouldUpdateOmnibarTextInput(viewState, viewState.omnibarText)) { omnibarTextInput.setText(viewState.omnibarText) @@ -181,6 +184,10 @@ class BrowserActivity : DuckDuckGoActivity() { privacyGradeMenu?.isVisible = viewState.showPrivacyGrade fireMenu?.isVisible = viewState.showFireButton + popupMenu.contentView.backPopupMenuItem.isEnabled = viewState.browserShowing && webView.canGoBack() + popupMenu.contentView.forwardPopupMenuItem.isEnabled = viewState.browserShowing && webView.canGoForward() + popupMenu.contentView.refreshPopupMenuItem.isEnabled = viewState.browserShowing + popupMenu.contentView.addBookmarksPopupMenuItem?.isEnabled = viewState.canAddBookmarks when (viewState.showAutoCompleteSuggestions) { false -> autoCompleteSuggestionsList.gone() @@ -211,7 +218,6 @@ class BrowserActivity : DuckDuckGoActivity() { private fun configureToolbar() { setSupportActionBar(toolbar) - supportActionBar?.let { it.title = null } @@ -262,7 +268,7 @@ class BrowserActivity : DuckDuckGoActivity() { webView.setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> val request = DownloadManager.Request(Uri.parse(url)) request.allowScanningByMediaScanner() - request.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED ) + request.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED) val manager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager manager.enqueue(request) Toast.makeText(applicationContext, getString(R.string.webviewDownload), Toast.LENGTH_LONG).show() @@ -306,8 +312,6 @@ class BrowserActivity : DuckDuckGoActivity() { override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_browser_activity, menu) - addBookmarkMenuItem = menu?.findItem(R.id.add_bookmark_menu_item) - addBookmarkMenuItem?.setEnabled(false) return true } @@ -321,50 +325,13 @@ class BrowserActivity : DuckDuckGoActivity() { launchFire() return true } - R.id.refresh_menu_item -> { - webView.reload() - return true - } - R.id.back_menu_item -> { - webView.goBack() - return true - } - R.id.forward_menu_item -> { - webView.goForward() - return true - } - R.id.add_bookmark_menu_item -> { - addBookmark() - return true - } - R.id.bookmarks_menu_item -> { - launchBookmarksView() - return true - } - R.id.settings_menu_item -> { - launchSettingsView() - return true + R.id.browser_popup_menu_item -> { + launchPopupMenu() } } return false } - private fun addBookmark() { - val title = webView.title - val url = webView.url - doAsync { - viewModel.addBookmark(title, url) - uiThread { - toast(R.string.bookmarkAddedFeedback) - } - } - } - - private fun finishActivityAnimated() { - clearViewPriorToAnimation() - supportFinishAfterTransition() - } - private fun launchPrivacyDashboard() { startActivityForResult(PrivacyDashboardActivity.intent(this), DASHBOARD_REQUEST_CODE) } @@ -377,6 +344,52 @@ class BrowserActivity : DuckDuckGoActivity() { }).show() } + private fun launchPopupMenu() { + val anchorView = findViewById(R.id.browser_popup_menu_item) + popupMenu.show(rootView, anchorView) + } + + fun onGoForwardClicked(view: View) { + webView.goForward() + popupMenu.dismiss() + } + + fun onGoBackClicked(view: View) { + webView.goBack() + popupMenu.dismiss() + } + + fun onRefreshClicked(view: View) { + webView.reload() + popupMenu.dismiss() + } + + fun onBookmarksClicked(view: View) { + launchBookmarksView() + popupMenu.dismiss() + } + + fun onAddBookmarkClicked(view: View) { + addBookmark() + popupMenu.dismiss() + } + + fun onSettingsClicked(view: View) { + launchSettingsView() + popupMenu.dismiss() + } + + private fun addBookmark() { + val title = webView.title + val url = webView.url + doAsync { + viewModel.addBookmark(title, url) + uiThread { + toast(R.string.bookmarkAddedFeedback) + } + } + } + private fun launchSettingsView() { startActivityForResult(SettingsActivity.intent(this), SETTINGS_REQUEST_CODE) } @@ -403,6 +416,11 @@ class BrowserActivity : DuckDuckGoActivity() { super.onBackPressed() } + private fun finishActivityAnimated() { + clearViewPriorToAnimation() + supportFinishAfterTransition() + } + private fun clearViewPriorToAnimation() { acceptingRenderUpdates = false omnibarTextInput.text.clear() diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserPopupMenu.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserPopupMenu.kt new file mode 100644 index 000000000000..dbaed03fcc50 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserPopupMenu.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Build.VERSION.SDK_INT +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.PopupWindow + +class BrowserPopupMenu : PopupWindow { + + constructor(layoutInflater: LayoutInflater, view: View = BrowserPopupMenu.inflate(layoutInflater)) + : super(view, WRAP_CONTENT, WRAP_CONTENT, true) { + + if (SDK_INT <= 21) { + // popupwindow gets stuck on the screen on API 21 without a background color. + // Adding it however garbles the elevation so we cannot have elevation here + setBackgroundDrawable(ColorDrawable(Color.WHITE)) + } else { + elevation = 6.toFloat() + } + + animationStyle = android.R.style.Animation_Dialog + } + + fun show(rootView: View, anchorView: View) { + val anchorLocation = IntArray(2) + anchorView.getLocationOnScreen(anchorLocation) + val x = margin + val y = anchorLocation[1] + margin + showAtLocation(rootView, Gravity.TOP or Gravity.END, x, y) + } + + companion object { + + val margin = 30 + + fun inflate(layoutInflater: LayoutInflater): View { + return layoutInflater.inflate(R.layout.popup_window_brower_menu, null) + } + + } +} + 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 d4430b550e34..5113d65d2e3e 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -24,7 +24,6 @@ import com.duckduckgo.app.bookmarks.ui.BookmarksViewModel import com.duckduckgo.app.browser.BrowserViewModel import com.duckduckgo.app.browser.DuckDuckGoUrlDetector import com.duckduckgo.app.browser.omnibar.QueryUrlConverter -import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.ui.OnboardingViewModel import com.duckduckgo.app.privacymonitor.db.NetworkLeaderboardDao @@ -36,9 +35,8 @@ import com.duckduckgo.app.privacymonitor.ui.PrivacyPracticesViewModel import com.duckduckgo.app.privacymonitor.ui.ScorecardViewModel import com.duckduckgo.app.privacymonitor.ui.TrackerNetworksViewModel import com.duckduckgo.app.settings.SettingsViewModel -import com.duckduckgo.app.settings.db.AppSettingsPreferencesStore -import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.settings.db.AppConfigurationDao +import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.trackerdetection.model.TrackerNetworks import javax.inject.Inject diff --git a/app/src/main/java/com/duckduckgo/app/home/HomeActivity.kt b/app/src/main/java/com/duckduckgo/app/home/HomeActivity.kt index 33b1afb4497c..7871afc28613 100644 --- a/app/src/main/java/com/duckduckgo/app/home/HomeActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/home/HomeActivity.kt @@ -23,23 +23,29 @@ import android.support.v4.app.ActivityOptionsCompat import android.support.v7.app.AppCompatActivity import android.view.Menu import android.view.MenuItem +import android.view.View import com.duckduckgo.app.about.AboutDuckDuckGoActivity import com.duckduckgo.app.bookmarks.ui.BookmarksActivity import com.duckduckgo.app.browser.BrowserActivity +import com.duckduckgo.app.browser.BrowserPopupMenu import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.intentText import com.duckduckgo.app.global.view.FireDialog import com.duckduckgo.app.settings.SettingsActivity import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.content_home.* +import kotlinx.android.synthetic.main.popup_window_brower_menu.view.* import org.jetbrains.anko.toast class HomeActivity : AppCompatActivity() { + private lateinit var popupMenu: BrowserPopupMenu + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_home) configureToolbar() + configurePopupMenu() searchInputBox.setOnClickListener { showSearchActivity() } @@ -53,6 +59,14 @@ class HomeActivity : AppCompatActivity() { supportActionBar?.setDisplayShowTitleEnabled(false) } + private fun configurePopupMenu() { + popupMenu = BrowserPopupMenu(layoutInflater) + popupMenu.contentView.backPopupMenuItem.isEnabled = false + popupMenu.contentView.forwardPopupMenuItem.isEnabled = false + popupMenu.contentView.refreshPopupMenuItem.isEnabled = false + popupMenu.contentView.addBookmarksPopupMenuItem.isEnabled = false + } + override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) consumeSharedText(intent) @@ -77,18 +91,14 @@ class HomeActivity : AppCompatActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { - R.id.settings_menu_item -> { - startActivityForResult(SettingsActivity.intent(this), SETTINGS_REQUEST_CODE) - true - } - R.id.bookmarks_menu_item -> { - startActivityForResult(BookmarksActivity.intent(this), BOOKMARKS_REQUEST_CODE) - true - } R.id.fire_menu_item -> { launchFire() true } + R.id.browser_popup_menu_item -> { + launchPopupMenu() + true + } else -> return super.onOptionsItemSelected(item) } } @@ -99,6 +109,21 @@ class HomeActivity : AppCompatActivity() { }).show() } + private fun launchPopupMenu() { + val anchorView = findViewById(R.id.browser_popup_menu_item) as View + popupMenu.show(rootView, anchorView) + } + + fun onBookmarksClicked(view: View) { + startActivityForResult(BookmarksActivity.intent(this), BOOKMARKS_REQUEST_CODE) + popupMenu.dismiss() + } + + fun onSettingsClicked(view: View) { + startActivityForResult(SettingsActivity.intent(this), SETTINGS_REQUEST_CODE) + popupMenu.dismiss() + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { SETTINGS_REQUEST_CODE -> onHandleSettingsResult(resultCode) @@ -108,7 +133,7 @@ class HomeActivity : AppCompatActivity() { } private fun onHandleBookmarksResult(resultCode: Int, data: Intent?) { - when(resultCode) { + when (resultCode) { BookmarksActivity.OPEN_URL_RESULT_CODE -> { openUrl(data?.action ?: return) } diff --git a/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/PrivacyDashboardActivity.kt b/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/PrivacyDashboardActivity.kt index b34fb0d08d74..baf2c689df21 100644 --- a/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/PrivacyDashboardActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacymonitor/ui/PrivacyDashboardActivity.kt @@ -22,7 +22,6 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.support.v4.content.ContextCompat -import android.view.MenuItem import android.view.View import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.DuckDuckGoActivity diff --git a/app/src/main/res/color/browser_menu_icon.xml b/app/src/main/res/color/browser_menu_icon.xml new file mode 100644 index 000000000000..ecfd09f07df6 --- /dev/null +++ b/app/src/main/res/color/browser_menu_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/browser_menu_text.xml b/app/src/main/res/color/browser_menu_text.xml new file mode 100644 index 000000000000..17354f512045 --- /dev/null +++ b/app/src/main/res/color/browser_menu_text.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_back_24px.xml b/app/src/main/res/drawable/ic_arrow_back_24px.xml new file mode 100644 index 000000000000..718059a952ed --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_24px.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_forward_24px.xml b/app/src/main/res/drawable/ic_arrow_forward_24px.xml new file mode 100644 index 000000000000..492de03182a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_forward_24px.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_bookmark_24px.xml b/app/src/main/res/drawable/ic_bookmark_24px.xml new file mode 100644 index 000000000000..dfb29ff12013 --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_24px.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_clear_browsnish_24px.xml b/app/src/main/res/drawable/ic_clear_browsnish_24px.xml new file mode 100644 index 000000000000..70e4a85b80a2 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_browsnish_24px.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_overflow_24px.xml b/app/src/main/res/drawable/ic_overflow_24px.xml new file mode 100644 index 000000000000..265db77c2a75 --- /dev/null +++ b/app/src/main/res/drawable/ic_overflow_24px.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml index c1ebfb7e5ebc..a37619be9fa3 100644 --- a/app/src/main/res/layout/activity_home.xml +++ b/app/src/main/res/layout/activity_home.xml @@ -17,6 +17,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/sheet_fire_clear_data.xml b/app/src/main/res/layout/sheet_fire_clear_data.xml index 76f4e0ef2302..45901adcaf09 100644 --- a/app/src/main/res/layout/sheet_fire_clear_data.xml +++ b/app/src/main/res/layout/sheet_fire_clear_data.xml @@ -40,14 +40,14 @@ android:textSize="16sp" android:textStyle="normal" /> + - - - - - - - - + android:id="@+id/browser_popup_menu_item" + android:icon="@drawable/ic_overflow_24px" + android:title="@string/browserPopupMenu" + app:showAsAction="ifRoom" /> \ No newline at end of file diff --git a/app/src/main/res/menu/menu_home_activity.xml b/app/src/main/res/menu/menu_home_activity.xml index 597ea6168ff8..d06d65fd5081 100644 --- a/app/src/main/res/menu/menu_home_activity.xml +++ b/app/src/main/res/menu/menu_home_activity.xml @@ -1,5 +1,4 @@ - - + Browser popup menu Refresh Back Forward diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index dcd517f0c496..6f3258891252 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -138,6 +138,24 @@ 12dp - + + + From b8e44c3d726f46099feb82e9178432b1c4fd0dac Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Fri, 19 Jan 2018 11:10:00 +0000 Subject: [PATCH 16/16] Bumping version in preparation for beta release --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 2a465ec5d4dd..5ff2a6dee334 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ apply plugin: 'kotlin-kapt' apply from: '../versioning.gradle' ext { - VERSION_NAME = "0.10.0" + VERSION_NAME = "4.0.0" } android {